# HTTP API

> Every endpoint the SDKs use, for calling Findlane from any language.

## Requests

The API is at `https://api.findlane.dev`. Requests and responses are JSON: send `Content-Type: application/json` with every request body.

Index endpoints are under the workspace and index:

```text
/api/teams/{workspaceId}/indexes/{index}
```

Authenticate with an API key as a bearer token:

```sh
API=https://api.findlane.dev
curl "$API/api/teams/$FINDLANE_WORKSPACE_ID/indexes/products/search" \
  -H "Authorization: Bearer $FINDLANE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "q": "trail shoe", "facets": ["brand.name"], "limit": 10 }'
```

## Errors

Failed requests answer with an HTTP status and a message:

```json
{ "error": "Filter field color is not configured" }
```

| Status | Meaning                                                                                                    |
| ------ | ---------------------------------------------------------------------------------------------------------- |
| `400`  | The request isn't valid. The message says why.                                                             |
| `401`  | The API key is missing or invalid.                                                                         |
| `402`  | A plan limit: records, indexes, or monthly searches.                                                       |
| `403`  | The key lacks the scope or index, or the origin isn't allowed.                                             |
| `404`  | The index, or a staged replacement, doesn't exist.                                                         |
| `409`  | A conflict, such as a record version that changed or another write during a staged replacement.            |
| `415`  | The request body isn't sent as `application/json`.                                                         |
| `429`  | A public search rate limit: 240 a minute per visitor, 6,000 per index. Wait for the `Retry-After` seconds. |
| `5xx`  | A temporary problem. Retry with a backoff.                                                                 |

## Search

| Method | Path      | Scope    |                   |
| ------ | --------- | -------- | ----------------- |
| `POST` | `/search` | `search` | Search the index. |

The body is a [search request](/docs/browser-sdk.md#request), with up to 100 hits a request, and the response is a [search response](/docs/browser-sdk.md#response).

### Browser search

```text
POST /api/public/indexes/{publicId}/search
```

Browser search needs no key. It answers only requests whose `Origin` header is one of the index's allowed origins, returns up to 50 hits a request, and follows the index's [public search policy](/docs/security.md#what-browser-search-can-read). It supports CORS, so browsers call it directly.

## Records

| Method   | Path                  | Scope   | Body                                                                    |
| -------- | --------------------- | ------- | ----------------------------------------------------------------------- |
| `POST`   | `/records`            | `write` | `{ "records": [...] }`: add or replace 1 to 1,000 records.              |
| `PUT`    | `/records`            | `write` | `{ "records": [...] }`: replace the catalog with up to 100,000 records. |
| `DELETE` | `/records/{objectID}` | `write` |                                                                         |
| `GET`    | `/records/{objectID}` | either  |                                                                         |
| `POST`   | `/record-writes`      | `write` | A conditional write, below.                                             |

Writes answer `202` with a receipt, and are searchable when they do:

```json
{ "acceptedRevision": 42, "indexedRevision": 42, "recordCount": 18230 }
```

`GET /records/{objectID}` answers `{ "objectID", "version", "record" }`, where `record` is `null` if the record doesn't exist.

### Conditional writes

```json
{
  "operation": "upsert",
  "objectID": "sku-1042",
  "record": { "objectID": "sku-1042", "title": "Trail running shoe", "price": 119 },
  "expectedVersion": 3,
  "idempotencyKey": "price-update-8f14e45f"
}
```

The write applies only if the record's version is `expectedVersion` (`0` for a record that doesn't exist), and answers `409` otherwise. `operation` is `upsert` or `delete`; a delete has no `record`. Repeating a request with the same `idempotencyKey` (1 to 64 letters, digits, `_`, or `-`) returns the first receipt instead of writing again.

### Staged replacements

A staged replacement uploads a catalog in batches and switches the index to it in one commit. `stageId` and `batchId` are 1 to 64 letters, digits, `_`, or `-`.

The paths are under `/staged-replacements/{stageId}`, and need the `write` scope:

| Method   | Path                             | Body                                                  |
| -------- | -------------------------------- | ----------------------------------------------------- |
| `POST`   | `/staged-replacements/{stageId}` | Start the stage, or get its status.                   |
| `POST`   | `…/batches/{batchId}`            | `{ "records": [...] }`: 1 to 1,000 records.           |
| `POST`   | `…/commit`                       | `{ "expectedBatches": 12, "expectedRecords": 11408 }` |
| `DELETE` | `/staged-replacements/{stageId}` | Abort the stage.                                      |

Uploading a batch again with the same records has no effect; with different records it fails with `409`. The commit checks the batch and record counts, and fails with `409` if other writes reached the index since the stage started. Committing again returns the first receipt.

## Settings

| Method | Path        | Scope    |                                                                                |
| ------ | ----------- | -------- | ------------------------------------------------------------------------------ |
| `GET`  | `/config`   | either   | The [settings](/docs/settings.md), with a change in progress in `rebuild`.        |
| `PUT`  | `/config`   | `write`  | Save the full settings. The response adds `applied`, `rebuilt`, and `rebuild`. |
| `GET`  | `/fields`   | `search` | Field paths in the index's records, with their types.                          |
| `GET`  | `/indexing` | either   | `{ "acceptedRevision", "indexedRevision", "recordCount" }`                     |

## Browser search access

| Method   | Path                    | Scope   |                                                                                       |
| -------- | ----------------------- | ------- | ------------------------------------------------------------------------------------- |
| `GET`    | `/public-access`        | `write` | `{ "publicId", "allowedOrigins", "policy" }`                                          |
| `PUT`    | `/public-access/policy` | `write` | `{ "policy": { ... } }`: see [Security](/docs/security.md#what-browser-search-can-read). |
| `DELETE` | `/public-access/policy` | `write` | Remove the policy.                                                                    |

Indexes, API keys, and allowed origins are managed in the dashboard.
