AACsearch
Troubleshooting

Auth errors

401 / 403 responses from the AACsearch API — diagnose missing headers, wrong key prefix, revoked keys, scoped-token expiry, and origin restrictions.

SymptomLikely cause
401 missing_bearer_tokenAuthorization header absent or malformed
401 unauthorizedEmpty or non-bearer token format
401 token_expiredScoped token TTL elapsed
403 forbiddenWrong key prefix for the operation, or key has been revoked
403 invalid_or_revoked_keyConnector token revoked or never matched
403 origin_not_allowedBrowser request origin not in allowedOrigins
403 key_does_not_match_indexKey was issued for a different indexSlug

Decision tree

401 missing_bearer_token
  → check Authorization header is set and starts with `Bearer `

401 unauthorized
  → token format wrong; verify it begins with ss_search_, ss_connector_,
    ss_scoped_, or aa_admin_ and contains no whitespace

401 token_expired (scoped tokens only)
  → scoped token's exp passed; mint a new one server-side

403 forbidden / invalid_or_revoked_key
  → check Search → API Keys; was it revoked?
  → if not revoked: prefix mismatch — see table below

403 origin_not_allowed
  → request `Origin` header not in key's allowedOrigins[]
  → during dev, leave allowedOrigins empty

Pick the right key for the operation

OperationRequired prefix
POST /api/search/...ss_search_* or ss_scoped_*
POST /api/multi-searchss_search_* or ss_scoped_*
POST /api/connectors/handshake and other /api/connectors/...ss_connector_*
POST /api/projects/{projectId}/sync/...ss_connector_*
POST /api/webhooks/sync/{indexSlug}HMAC-SHA256 signature with index webhook secret (no Bearer)
POST /api/v1/... (management)aa_admin_* (server-only)

A ss_search_* key on a connector endpoint, or an ss_connector_* key on /api/search, both return 403 forbidden. The mapping is enforced by gatePublicSearchRequest() and gateConnectorRequest() in packages/api/modules/search/lib/.

Checks to run

  1. Authorization header is correct.

    curl -i -X POST https://app.aacsearch.com/api/search/products \
      -H "Authorization: Bearer ss_search_..." \
      -H "Content-Type: application/json" \
      -d '{ "q": "test" }'

    Look for 200. If 401, copy the response error field to the table above.

  2. Key has not been revoked. Open Search → API Keys. Revoked keys are filtered out of the default list — toggle "Show revoked" to confirm.

  3. Origin is allowed (browser only). Open dev tools → Network → click the failing request → check the Origin request header. It must match an entry in the key's allowedOrigins[].

    // To list allowed origins programmatically:
    const keys = await admin.listKeys();
    const yours = keys.find((k) => k.id === "key_...");
    console.log(yours.allowedOrigins);
  4. Scoped token has not expired. Decode the payload (it is base64url, not encrypted) and check expiresAt:

    const [, payloadB64] = scopedToken.replace("ss_scoped_", "").split(".");
    const payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString());
    console.log(payload.expiresAt, "vs now", Math.floor(Date.now() / 1000));
  5. Index slug matches. A key issued for indexSlug: "products" cannot search indexSlug: "categories". Re-create the key against the right index, or omit the slug at issuance to allow all org indexes.

Fix

DiagnosisFix
Header missingAdd Authorization: Bearer <key> header
Wrong prefixMint a new key of the correct type — see table above
Key revokedCreate a replacement: Search → API Keys → Create key
Origin not allowedAdd the storefront origin to allowedOrigins[] (or leave empty in dev)
Scoped token expiredMint a fresh scoped token server-side; client should request a new one on 401 token_expired
Slug mismatchReissue the key without an index lock, or with the right slug

Browser-side recovery

For widget-style integrations, treat 401 token_expired as an opportunity to refetch:

import { AacSearchClient, AacSearchError } from "@repo/search-client";

async function searchWithTokenRefresh(query: string) {
	try {
		return await client.search({ q: query });
	} catch (err) {
		if (err instanceof AacSearchError && err.code === "token_expired") {
			const fresh = await fetch("/api/search-token").then((r) => r.text());
			client.setApiKey(fresh);
			return await client.search({ q: query });
		}
		throw err;
	}
}

Server-side, expose a Server Action (or /api/search-token route) that calls orpc.search.createScopedToken.call({ ... }) with a fresh expiresInSeconds.

Diagnostics packet

FieldNotes
Organization IDrequired
Time (UTC)required
Request IDfrom response header X-Request-Id
Failing key prefixfirst 12 chars only — never the full key
Origin (if browser)from request headers
Endpoint hitfull path, e.g. POST /api/search/products
Response body{ "error": "...", "message": "..." }

Never paste a full API key into a ticket. The key prefix and last 4 characters are enough for support to identify it in the audit log.

On this page