# Conventions

What the Query API and the management API share, from JSON and ids to keys, problems and the OpenAPI document.

OrbSearch has two HTTP APIs. The **Query API** answers storefronts: searches, product pages, URLs, and the clicks and views of the [Events API](/docs/reference/events-api). The **management API** belongs to the control plane, the service that also serves the panel, and takes the catalog and the search configuration. One OpenAPI 3.1 document describes both.

| API                                         | Base URL in the compose stack  | Authentication                | Called by                         |
| ------------------------------------------- | ------------------------------ | ----------------------------- | --------------------------------- |
| [Query API](/docs/reference/query-api)           | `http://127.0.0.1:7800`        | None                          | Storefronts, browsers, agents     |
| [Management API](/docs/reference/management-api) | `http://127.0.0.1:8000/api/v1` | `Authorization: Bearer <key>` | The merchant's scripts and agents |

Both listen on 127.0.0.1 for a proxy on the same machine that terminates TLS, so in production the base URLs are the addresses your proxy gives them ([Self-hosting](/docs/self-hosting#tls)).

## JSON, times and sources

- **JSON in, JSON out.** Requests and answers are `application/json`, errors `application/problem+json`. The one exception is a shop's export, which is sent as the file the shop wrote.
- **Absent means left out.** A value that is not there is missing from the object, never `null`.
- **Times** are RFC 3339 strings with an offset, such as `2026-11-27T00:00:00+01:00`.
- **Answers name their sources.** Every search, product and page answer carries the `release` and the `snapshot` it came from.

## Ids

| Id                | Example from the home store | What it is                                                                                         |
| ----------------- | --------------------------- | -------------------------------------------------------------------------------------------------- |
| Release id        | `b25a6aaac6a46319b4ee3d25`  | Named by the release's content: the same files are the same release. Treat it as an opaque string. |
| Snapshot id       | `753be49b9a9a503b3affac97`  | Named by the uploaded bytes: the same export is the same snapshot. Opaque.                         |
| Import, index run | `1`                         | Integers the control plane counts up.                                                              |
| Query id          | `01a11223-7c46-72b3-…`      | Names one answered search. Opaque.                                                                 |
| Entity reference  | `product:SOFA-ASKA-2`       | An entity's type and the merchant's own id, which OrbSearch never rewrites.                        |

## Authentication

The Query API takes no key: it only reads, and search results are public data. The management API changes what shoppers find, so every call sends the management key, `ORBSEARCH_MANAGEMENT_KEY` from the stack's `.env`, as a bearer token. With `$key` and `$api` from the [Quickstart](/docs/quickstart#keep-the-management-key-at-hand):

```sh title="Terminal"
curl -s -H "Authorization: Bearer $key" "$api/live"
```

A missing or wrong key answers 401 with `WWW-Authenticate: Bearer`; while the control plane has no key set, it refuses every call. Keep the key on servers and never ship it to a browser: a storefront needs only the Query API. [Self-hosting](/docs/self-hosting#security) says how to rotate it.

## Problems

Every error of either API is an RFC 9457 problem. `detail` says what happened and what to do next; for invalid input, each entry of `violations` names the field with a JSON `pointer` and, for a release, the file.

```json title="GET /v1/search?query=sofa&per_page=500"
{
    "type": "about:blank",
    "title": "Bad Request",
    "status": 400,
    "detail": "The request cannot be answered as sent; each violation names the field.",
    "violations": [{ "pointer": "/per_page", "message": "A page holds 1 to 100 hits." }]
}
```

`type` is always `about:blank`, and `title` is the status's own phrase. Tell problems apart by `status`, and show `detail` and each violation's `message` to the person who can fix it. [Problems](/docs/reference/problems) lists every status each endpoint answers with, and when.

## Query API

The Query API is public and read-only. Any origin may call it from a browser: CORS allows `GET` and `POST` with a `Content-Type` header and without credentials. Answers arrive compressed with zstd or gzip when the client accepts them.

```sh title="Terminal"
curl -s 'http://127.0.0.1:7800/v1/search?query=sofa&debug=1'

curl -s http://127.0.0.1:7800/v1/search -H 'Content-Type: application/json' \
  -d '{ "query": "sofa", "filters": { "color": ["grey"] } }'
```

- **`GET` or `POST` search.** `GET /v1/search` takes the plain fields as URL parameters, enough for a link or `curl`. Filters, `dismissed` and `context` need the JSON body of `POST /v1/search`.
- **Paging.** `page` counts from 1; `per_page` defaults to 24 and goes up to 100. Each section's `total` counts every match exactly unless the section says `upper_bound` ([Upper bounds](/docs/facets#upper-bounds)), and pages reach the first 1,000, which each section states as `reachable`.
- **The trace.** `debug=1` (or `"debug": true`) adds the [trace](/docs/reference/trace): one entry per step, explaining why the answer is what it is.
- **The time is stamped on arrival.** A request that brings its own `time` is refused with a 400, so nobody sees a scheduled rule early.
- **Before the first index run** every endpoint but `/health` answers 503 with a problem that says what is missing.

## Management API

- **Catalog uploads are files, not JSON.** `PUT /api/v1/catalog` and `POST /api/v1/imports` take the shop's export as the body and keep it byte for byte. The import recognizes the format from the bytes, so the `Content-Type` only documents the file.
- **Releases are JSON.** `POST /api/v1/releases` takes `{ "files": { "rules.json": { … } } }`, each file as its JSON or as text. Storing checks the release and queues nothing; the same files make the same release, 201 the first time and 200 after.
- **Work happens in index runs.** A catalog upload, the publish of a release or of an import, and a rebuild (`POST /api/v1/index-runs`) answer 202 with a `next` sentence that says what happens, and with the queued index run. An upload before any release is published, or a publish before any catalog, queues none, and `next` says what is missing. Follow `GET /api/v1/index-runs/{id}` until the run succeeds or fails ([Catalog](/docs/import-report#index-run-states)); a publish takes only the way its change needs, and `GET /api/v1/releases/{id}/publication` says which before it does ([Releases](/docs/releases#how-a-publish-goes-live)).
- **Scripts and agents, not browsers.** The management API sends no CORS headers, so a page on another site cannot call it with a key a browser holds.
- **Repeating is safe.** The same bytes make the same snapshot and the same files the same release, though uploading a catalog or publishing a release again still queues a fresh index run.
- **Lists are paged.** `GET /api/v1/imports`, `/releases` and `/index-runs` answer newest first, 15 per page unless `?per_page=` (1 to 100) says otherwise, with `?page=` counting from 1. The answer holds `data`, `links` (`first`, `last`, and `prev` and `next` where they exist) and `meta` (`current_page`, `last_page`, `per_page`, `total`).

## The OpenAPI document

The contract is written once, as Rust types in `crates/contract`. The OpenAPI document, the JSON Schemas of the [release files](/docs/reference/release-files/channels), the [limits](/docs/reference/limits) and the types of `@orbsearch/client` are generated from it, the Query API builds its routes from the same table, and a test holds the control plane's routes to the document. Every other page of this reference is generated from them too. [`/docs/openapi.json`](/docs/openapi.json) serves the document with these docs; in the repository it is `contracts/openapi.json`. Any OpenAPI 3.1 generator makes a client in another language from it. Three things to know when you do:

- Its `servers` are the local stack's addresses. Point the client at your own.
- It declares no security scheme, so a generated client sends no key: add `Authorization: Bearer <key>` to management calls yourself.
- Its `/internal/...` operations, tagged `internal`, are not part of the public API: they are served on a port only the control plane reaches inside the stack.
