AACSearch

Embed session

POST /api/v1/embed/session — exchange an API key, server-side, for a short-lived, search-only session token that is safe to put in a page. Plus the browser-facing /embed/* endpoints that accept it.

POST /api/v1/embed/session
Authorization: Bearer aac_live_…

An aac_live_… key must never reach a browser. This endpoint exists to avoid that: it trades the key for an embed session token — scoped to one index, one action (search:read) and an expiry. The token is what goes into the page (data-aac-token); the key stays on your server.

There is no GET /embed/bootstrap and no ?key= form. A key in a query string, a data- attribute or the page HTML is exactly the leak this design removes: the endpoint reads the key from the Authorization header only and answers 401 UNAUTHORIZED otherwise.

Call it from your own backend — a route handler, an SSR pass or a build step — and hand the resulting token to the page.

Mint a session

curl -sS -X POST "$AAC_API/embed/session" \
  -H "Authorization: Bearer $AAC_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "index": "documentation", "expires_in": 900 }'
Body fieldTypeDefaultMeaning
indexstring ≤ 64the key's index, or the tenant's oldest indexIndex slug to scope the session to. A key bound to one index cannot mint a session for a sibling
expires_ininteger, seconds900 (15 minutes)Requested lifetime, clamped to 60–3600

The body is optional. A non-numeric expires_in or an oversized index is 422 VALIDATION_ERROR.

{
	"ok": true,
	"token": "aac_sess_…",
	"token_type": "embed_session",
	"scope": ["search:read"],
	"index": { "slug": "documentation", "name": "Documentation" },
	"expires_at": "2026-10-23T11:15:02.000Z",
	"expires_in": 900,
	"settings": {
		"placeholder": "Search the docs…",
		"perPage": 8,
		"queryBy": "title,content",
		"facets": ["category"],
		"showStats": true,
		"showRecent": true,
		"showVoice": false,
		"showImage": false,
		"showRelated": false,
		"accent": "#2563eb",
		"dark": false,
		"sortOptions": [
			{ "label": "Relevance", "value": "default" },
			{ "label": "Newest first", "value": "created_at:desc" }
		]
	},
	"ui": { "name": "AACSearch UI", "version": "1.0.0" },
	"request_id": "req_4a6c8e0b2d4f6a8c0e2b4d6f"
}

What the response guarantees:

  • The token is search-only. It carries search:read and nothing else, so it cannot write, delete or reindex. Compromising the page yields a credential that expires on its own and can only read what the page already shows.
  • The token is pinned to one index and tenant. The tenant filter is applied server-side on every request; the browser never sees a the search service key, host or collection name.
  • It expires. Default 900 seconds, at most 3600. Mint a new one before expiry for long-lived pages; an expired token is 401 EMBED_SESSION_EXPIRED.
  • It is not cached. Responses are cache-control: no-store; every issuance is written to the audit log (embed.session.issue).
  • Settings are live. settings comes from SearchIndex.settings.embed, so the console's appearance panel changes the widget on the next page load — no redeploy.

Origin allowlist

If the key has allowedOrigins, a browser call from any other origin is 403 ORIGIN_NOT_ALLOWED and receives no CORS headers. An empty list means "any origin". Because the session endpoint is meant to be called from your backend, a server-to-server call has no Origin header and is unaffected.

Settings

KeyTypeDefaultEffect
placeholderstringSearch the docs…Input placeholder
perPagenumber8Hits per page
queryBystringtitle,contentquery_by sent with each search
facetsstring[][]Facet fields rendered as checkbox lists (the console limits you to 6)
showStatsbooleantrueResult count line
showRecentbooleantrueRecent-query chips, stored locally per index
showVoicebooleanfalseBrowser voice input via InstantSearch voiceSearch
showImagebooleanfalseShow a hit image when the document has one
showRelatedbooleanfalse“For you” strip under results (needs recommendations enabled on the index)
accentstring#2563ebAccent colour for buttons, focus ring and links
darkbooleanfalseDark palette
sortOptionsarrayrelevance + created_at:descSort dropdown; the first entry must use default

Failures

{
	"error": {
		"code": "UNAUTHORIZED",
		"message": "Send the API key as an Authorization header; embed sessions cannot be minted from a query parameter",
		"request_id": "req_7b9d1f3a5c7e9b1d3f5a7c9e"
	}
}
StatusCodeWhen
401UNAUTHORIZEDMissing, malformed, unknown or revoked key; key sent as a query parameter
403ORIGIN_NOT_ALLOWEDBrowser origin outside the key's allowedOrigins
404INDEX_NOT_FOUNDNo index is available for this key, or index names an index the key cannot use
422VALIDATION_ERRORInvalid body
429RATE_LIMITEDPer-address limit on /embed/* (anonymous embed traffic has a smaller budget)

Endpoints that accept the token

Once the page holds a token, the widget talks only to these endpoints. They all authenticate with the token — in the JSON body (token), or as Authorization: Bearer aac_sess_… where noted — never with an API key. Each also answers a CORS preflight (OPTIONS) with 204.

EndpointPurpose
POST /api/v1/embed/configSettings for an already-minted token: { "token": "…" } returns index, scope, expires_at and settings
POST /api/v1/embed/searchSearch proxy. Body { token, index, client_id?, personalize?, searches: [...] } with up to 5 searches; the token may also be sent as a Bearer header. page ≤ 20, per_page clamped to 50
POST /api/v1/embed/eventsBehaviour events: type is one of view, click, conversion, add_to_cart, impression (the widget itself sends view, click and conversion), with document_id, position, query, query_id, session_id, value_cents, items
POST /api/v1/embed/recommendationsRelated and personalised items for a visitor: client_id, seed_document_id, seed_query, limit (≤ 40)
POST /api/v1/embed/widget-sessionFor saved widgets in the console: a public widget id (wgt_…) plus an allowed origin yields a session, with no key anywhere on the page

Search through the proxy goes through the same pipeline as the public API (query logs, rules, defaults, relaxation, did-you-mean), so analytics see widget traffic. Every embed search counts against the tenant's searches quota.

Token failures on these endpoints:

StatusCodeMeaning
401EMBED_SESSION_REQUIREDNo token was sent
401EMBED_SESSION_EXPIREDThe token's expiry has passed — mint a new one
401EMBED_SESSION_INVALIDMalformed or tampered token
403FORBIDDENToken lacks the search:read scope
403INDEX_MISMATCHThe request names an index other than the token's

Browser access

Use POST /embed/session and /embed/search for the shipped widget or a custom browser interface. Your server exchanges its API key for a short-lived token; the browser receives neither infrastructure credentials nor storage names. The advanced /scoped-key endpoint is not used by the widget and is not a substitute for the platform search API.

On this page