Skip to navigation

Product Search

Product Search is in beta with design partners. Endpoints roll out per tenant.

Captain Product Search powers the search box inside an application or website: product search on a store, listing search on a marketplace, article search on a help center, or record search inside a SaaS product.

Captain File Search is a separate product for files and their contents. It uses collections, documents and chunks. Product Search uses indexes and records.

Vocabulary

TermMeaning
TenantOne customer’s dedicated Product Search deployment, with its own endpoint.
IndexA searchable set of records, such as products, stores or articles.
RecordOne product, article, listing or other entity in an index.
Query, filter, facetThe usual search terms: the text searched, the conditions every result must meet, and the counts per field value.

How a request finds its data

Each customer gets a dedicated deployment and a dedicated hostname, for example https://acme.captain.dev.

  • The hostname selects the tenant.
  • The path selects the index: /v1/indexes/products/search.
  • The key grants access to that tenant’s indexes and operations.

Requests carry no tenant id, and there is no /tenants/... path. Two tenants may each have an index called products. A key issued for one tenant does not work against another tenant’s endpoint.

What Captain manages

Captain creates each tenant and its keys, and tunes ranking and index settings with the customer. The customer’s application creates indexes, keeps their records up to date, searches, and sends click and conversion events. Keys lists which key calls which operation.

Endpoint

Onboarding provides two things: the tenant endpoint, for example https://acme.captain.dev, and the keys for this tenant. The Quickstart creates a first index from there.

Every Product Search route sits under that endpoint:

https://acme.captain.dev/v1/indexes/products/search
https://acme.captain.dev/v1/indexes/articles/search

Keys

KeyUse it forWhere it lives
Search keySearching, reading records, sending events and reading analytics.Server side. It reads your analytics, which include what your shoppers search for.
Scoped keySearching, reading records and sending events from a browser or an app. Restrict it to your store’s origins with allowed_origins. On an index with per-record access control, mint a short-lived one per signed-in user. It can’t read analytics.Safe in a browser, once minted.
Issuer keyMinting short-lived scoped keys, for example one per signed-in user. It can’t search.Server side only.
Write keyCreating, listing, copying and deleting indexes and aliases, changing settings, synonyms and rules, adding, updating and deleting records, importing, reading and cancelling jobs, and managing keys. It cannot search.Server side only.

Captain issues a search key, a write key and an issuer key at onboarding, each for every index, and keeps the admin key. Keep all three on your server. In a browser, use a scoped key restricted to your store’s origins, as the Search widget guide shows. Access-control and audit settings go through Captain. Index settings lists what you set and what Captain sets.

Only a search key reads analytics. A scoped, issuer or write key gets 403. On an index with per-record access control, analytics need Captain’s admin key, so Captain provides them.

Which key calls which operation

OperationSearch keyScoped keyIssuer keyWrite key
Search, typeahead, browse, similar, multiple searchesYesYesNoNo
Get recordsYesYesNoNo
List filtersYesYesNoNo
Send eventsYesYesNoNo
Read analyticsYesNoNoNo
Create an indexNoNoNoYes, with a write key for every index
List indexesNoNoNoYes
Add, replace, update and delete recordsNoNoNoYes
Delete records by filter, empty an indexNoNoNoYes
Get an index’s status and update statusNoNoNoYes
Import from AlgoliaNoNoNoYes. Into a new index, with a write key for every index.
List jobs, get a job, cancel a queued jobNoNoNoYes
Change settings, synonyms and rulesNoNoNoYes, except acl, tenant_field and audit
Set and delete aliases, split shoppers between copies of an indexNoNoNoYes
Copy an indexNoNoNoYes, with a write key for every index
Create keysNoNoScoped keys onlyYes, with a write key for every index
List, get and revoke keysNoNoNoYes, with a write key for every index
Read usageNoNoNoYes
Delete an indexNoNoNoYes. Keys limited to that index alone are revoked. See Delete an index.

A tenant’s keys work on every index in that tenant, including indexes created later. A key can also be limited to some indexes. A request for any other index returns 403, the same answer as for an index that does not exist.

Manage your keys

The write key you got at onboarding covers every index, and it manages keys too. Create Key mints a search, scoped, write or issuer key, for every index (["*"]) or for the indexes you name. The new key’s secret is in that reply only. Captain stores a hash and can’t show it again.

Create Key takes a write key for every index or an issuer key. Any other key gets 403.

curl -X POST "https://acme.captain.dev/v1/keys" \
-H "Authorization: Bearer $CAPTAIN_WRITE_KEY" \
-H "Content-Type: application/json" \
-d '{"type": "search", "indexes": ["products-staging"], "description": "staging storefront"}'
Example: (201 Created)
{
"id": "key_8a1f3c0d9e2b4f67",
"type": "search",
"indexes": ["products-staging"],
"prefix": "cap_k3Jd9x",
"description": "staging storefront",
"created_by": "key_2c7e90a1b4d35f18",
"created_at": 1759433001,
"key": "cap_k3Jd9x...",
"note": "store this key now: it is shown once and kept only as a hash"
}

A key can also carry expires_at (unix seconds) and allowed_origins, the sites a browser may use it from.

Mint a scoped key with the issuer key

The issuer key you got at onboarding does one thing: it mints scoped keys. Use it on your server to give each visitor or signed-in user a short-lived key. A key it mints must be scoped, must name its indexes, and must carry an expires_at within the issuer’s max_ttl (one hour unless it was set otherwise). The issuer’s own filter is added to every key it mints. Its allowed_origins carry over too, unless the request sends its own.

curl -X POST "https://acme.captain.dev/v1/keys" \
-H "Authorization: Bearer $CAPTAIN_ISSUER_KEY" \
-H "Content-Type: application/json" \
-d '{"type": "scoped", "indexes": ["products"], "filter": {"status": "published"}, "expires_at": '"$(( $(date +%s) + 3600 ))"'}'

expires_at here is one hour from now, the longest an issuer key allows by default.

The reply has the same shape as above, with issued_by naming the issuer key. Revoking the issuer key revokes every key it minted.

List and revoke keys

List Keys (GET /v1/keys) returns every key without its secret, and marks the key making the call with current: true. Get Key (GET /v1/keys/{key_id}) returns one key. Revoke Key (DELETE /v1/keys/{key_id}) stops a key at once.

Revoking a key also revokes every key it created, and every key those created. The reply’s revoked lists every id it revoked. To revoke only the one key, send ?cascade=none.

A key can’t revoke itself, so you can’t lock yourself out. Revoking the key that makes the call answers 409 KEY_IS_CALLER. To replace a leaked write key:

  1. Get a new write key for every index that the old key didn’t create: ask Captain support for one, or mint it with another write key you trust. A key the old key minted would be revoked with it, so revoking the old key with such a key answers 409 CASCADE_INCLUDES_CALLER and revokes nothing.
  2. With the new key, mint replacements for every key the old key minted that your apps still use, and deploy them. Revoking the old key in the next step revokes those keys too, so an app still on one of them stops working.
  3. Revoke the old key with the new one. That also revokes every key the old key minted, including any an attacker minted with it. To revoke only the old key and keep the keys it minted, send ?cascade=none, but then any key an attacker minted keeps working until it’s revoked on its own.

Only a write key for every index lists, gets and revokes keys. A write key limited to some indexes gets 403, and so do search, scoped and issuer keys.

On a tenant served from several regions, keys are created and revoked in its primary region. A Create Key or Revoke Key request that reaches another region answers 409 KEYS_READ_ONLY, and its message names the primary region. A key created there works in every region within a few seconds.

List Keys also shows keys Captain issued on your tenant, such as a short-lived key support uses to look at your index with you. Those carry issued_by_captain: true, and revoking one with your key answers 403 KEY_ISSUED_BY_CAPTAIN. Ask support to revoke it.

Sending the key

Every endpoint takes its key the same way, as a bearer token in the Authorization header. Each endpoint’s reference page names the kind of key it needs.

curl -X POST "https://acme.captain.dev/v1/indexes/products/search" \
-H "Authorization: Bearer $CAPTAIN_SEARCH_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "waterproof hiking boots",
"k": 3
}'

A missing or unknown key returns 401. A key without access to the index or operation returns 403, and so does an index the key cannot see, whether or not that index exists.

Calling from a browser

A scoped key can be used straight from a web page. A search key works from a browser too, but it reads your analytics, so keep it on your server. Captain answers the browser’s preflight for any origin, without a key. A page may send these request headers:

  • Authorization (or X-Captain-Key)
  • Content-Type
  • X-Captain-Min-Seq

Answers to a search or scoped key allow the page’s origin and let page code read X-Captain-Seq, X-Captain-Applied-Seq and X-Request-Id.

A key can be limited to your own sites. Set that list in allowed_origins when you create the key with Create Key, and a request from any other origin then gets 403 ORIGIN_NOT_ALLOWED. The origin check stops other websites from using your key in their visitors’ browsers. It doesn’t stop a server that sends a fake origin. Limit what a leaked key can reach with its scope, and revoke it if it leaks.

Write keys stay on your server. Their answers carry no CORS headers, so a page can’t read them.

Errors

Every error reply has the same shape: a machine code in error and a readable message. That includes a path no endpoint answers (404 ROUTE_NOT_FOUND) and a method an endpoint doesn’t take (405 METHOD_NOT_ALLOWED).

{"error": "FILTER_INVALID", "message": "filter: unknown field colour", "position": 0, "expected": ["brand", "color", "price"]}
StatusCommon codesMeaning
400VALIDATION_ERROR, FILTER_INVALID, FACET_UNKNOWN, PAGE_TOO_DEEP, ID_CONFLICT, ALGOLIA_KEY_CAN_WRITEThe request is malformed or names something the index lacks. ID_CONFLICT: an Algolia object’s own id field differs from its objectID. ALGOLIA_KEY_CAN_WRITE: the Algolia key sent to Import From Algolia can write to Algolia. Send a key with read permissions only.
401UNAUTHORIZEDThe key is missing or unknown.
403FORBIDDEN, ACCESS_DENIED, KEY_ISSUED_BY_CAPTAINThe key may not use this index or operation. KEY_ISSUED_BY_CAPTAIN: the key to revoke was issued by Captain’s support or console, and only Captain revokes it.
404INDEX_NOT_FOUND, RECORD_NOT_FOUND, JOB_NOT_FOUND, ALIAS_NOT_FOUND, ROUTE_NOT_FOUNDThe index, record, job or alias does not exist, or no endpoint has that path. A RECORD_NOT_FOUND reply also carries legacy_error: "DOCUMENT_NOT_FOUND", its former name.
405METHOD_NOT_ALLOWEDThe endpoint exists but doesn’t take this method.
409INDEX_EXISTS, IMPORT_RUNNING, INDEX_NOT_READY, IDEMPOTENCY_IN_PROGRESS, JOB_NOT_CANCELLABLE, INDEX_HAS_ALIAS, ALIAS_NOT_INDEX, CASCADE_INCLUDES_CALLER, KEY_IS_CALLER, KEYS_READ_ONLYAn index or alias with that name already exists, an import into the index is already running, the index has no records yet, a request with the same idempotency key is still running, the job has already started, an alias still points at the index, the name is an alias, not an index, revoking the key would also revoke the key making the call, a key tried to revoke itself, or a key was created or revoked outside the tenant’s primary region.
413PAYLOAD_TOO_LARGE, INDEX_TOO_LARGEThe request body is too large, or the records would take the index past the tenant’s record limit, when one is set.
422IDEMPOTENCY_KEY_REUSEDAn idempotency key was reused with a different request.
429RATE_LIMITEDA request with no valid key, from an address sending many of them. Retry after the time in Retry-After. Requests with a valid key aren’t rate limited: use is fair use.
503UNAVAILABLEThe service is briefly unavailable. Retry with backoff.

A request whose connection closes before any answer arrives never reached Captain. Connections and retries shows when to send it again.

Limits

LimitValue
Records per add request1,000 as a JSON array, 100,000 as NDJSON or CSV
Records per update or delete request1,000
Records per get request100
Events per request1,000
Searches per multi-search request10

Machine-readable guides

Each tenant publishes its own reference:

https://acme.captain.dev/v1/openapi.json
https://acme.captain.dev/llms.txt

openapi.json lists the endpoints in this reference, as deployed on that tenant. llms.txt is a short plain-text guide for coding assistants.

Request ids

Every reply carries an X-Request-Id header. Quote it when you contact Captain about a request. A request that sends its own X-Request-Id (8 to 128 letters, digits, ., _ or -) gets the same id back.

Next steps