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. 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 | http://127.0.0.1:7800 | None | Storefronts, browsers, agents |
| 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).
JSON, times and sources
- JSON in, JSON out. Requests and answers are
application/json, errorsapplication/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
releaseand thesnapshotit 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:
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 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.
{
"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 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.
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"] } }'GETorPOSTsearch.GET /v1/searchtakes the plain fields as URL parameters, enough for a link orcurl. Filters,dismissedandcontextneed the JSON body ofPOST /v1/search.- Paging.
pagecounts from 1;per_pagedefaults to 24 and goes up to 100. Each section'stotalcounts every match exactly unless the section saysupper_bound(Upper bounds), and pages reach the first 1,000, which each section states asreachable. - The trace.
debug=1(or"debug": true) adds the 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
timeis refused with a 400, so nobody sees a scheduled rule early. - Before the first index run every endpoint but
/healthanswers 503 with a problem that says what is missing.
Management API
- Catalog uploads are files, not JSON.
PUT /api/v1/catalogandPOST /api/v1/importstake the shop's export as the body and keep it byte for byte. The import recognizes the format from the bytes, so theContent-Typeonly documents the file. - Releases are JSON.
POST /api/v1/releasestakes{ "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 anextsentence 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, andnextsays what is missing. FollowGET /api/v1/index-runs/{id}until the run succeeds or fails (Catalog); a publish takes only the way its change needs, andGET /api/v1/releases/{id}/publicationsays which before it does (Releases). - 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,/releasesand/index-runsanswer newest first, 15 per page unless?per_page=(1 to 100) says otherwise, with?page=counting from 1. The answer holdsdata,links(first,last, andprevandnextwhere they exist) andmeta(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, the 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 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
serversare 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, taggedinternal, are not part of the public API: they are served on a port only the control plane reaches inside the stack.