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.

APIBase URL in the compose stackAuthenticationCalled by
Query APIhttp://127.0.0.1:7800NoneStorefronts, browsers, agents
Management APIhttp://127.0.0.1:8000/api/v1Authorization: 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, 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

IdExample from the home storeWhat it is
Release idb25a6aaac6a46319b4ee3d25Named by the release's content: the same files are the same release. Treat it as an opaque string.
Snapshot id753be49b9a9a503b3affac97Named by the uploaded bytes: the same export is the same snapshot. Opaque.
Import, index run1Integers the control plane counts up.
Query id01a11223-7c46-72b3-…Names one answered search. Opaque.
Entity referenceproduct:SOFA-ASKA-2An 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:

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 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.

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 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.

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), and pages reach the first 1,000, which each section states as reachable.
  • 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 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); a publish takes only the way its change needs, and GET /api/v1/releases/{id}/publication says 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, /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, 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 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.

On this page