AACSearch

Errors and limits

The error envelope, every code the API returns with its status, rate-limit semantics, plan quotas and what happens when the wallet runs dry.

The error envelope

Every failure — validation, auth, quota, engine — uses the same shape:

{
	"error": {
		"code": "QUOTA_EXCEEDED",
		"message": "Monthly searches quota exhausted (500000/500000 for 2026-10)",
		"request_id": "req_3c5e7a9b1d3f5a7c9e1b3d5f",
		"details": {
			"metric": "searches",
			"limit": 500000,
			"used": 500000,
			"period": "2026-10",
			"resetsAt": "2026-11-01T00:00:00.000Z"
		}
	}
}

code is a stable identifier you can branch on; message is for humans and may change; request_id also comes back in the x-request-id header. details is present only when there is something useful to say.

curl -sS -i "$AAC_API/indexes/documentation/search?q=pricing&num_typos=9" \
  -H "Authorization: Bearer $AAC_KEY"
{
	"error": {
		"code": "VALIDATION_ERROR",
		"message": "Request payload is invalid",
		"request_id": "req_7d9f1b3c5e7a9b1d3f5c7a9b",
		"details": {
			"issues": [
				{
					"code": "too_big",
					"path": ["num_typos"],
					"message": "Number must be less than or equal to 3"
				}
			]
		}
	}
}

Codes by status

400

CodeRaised by
INVALID_QUERYBad filter, sort, field list or vector_query syntax; filter_by mentioning organization_id; missing filter_by on bulk delete; index without an auto-embedding field when semantic=true; more than 20 searches in multi_search
FIELD_NOT_IN_SCHEMAfilter_by, sort_by, facet_by, query_by or group_by names a field the index schema does not declare (documents may still carry it). details.field is the field from your request, details.param the parameter. Add the field in Schema and reindex
INVALID_JSONWebhook body that is not JSON
PRIVATE_FIELDquery_by, filter_by, sort_by, facet_by, include_fields or group_by names a field the index marks private and the key lacks documents:read_private (also /embed/search). details.fields, details.params. See Private fields
INVALID_REQUESTPOST /billing/invoices/generate with a period that is not YYYY-MM or lies in the future
PRIVATE_FIELDS_PRESENTPOST /indexes/{slug}/scoped-key for an index with private fields, by a key without documents:read_private. details.fields

401

CodeRaised by
UNAUTHORIZEDMissing/malformed Authorization header, unknown or revoked key, missing webhook signature headers
KEY_ROTATEDThe key was rotated and its grace period has ended. Deploy the successor issued at rotation; the error never names it
EMBED_SESSION_REQUIRED / EMBED_SESSION_EXPIRED / EMBED_SESSION_INVALID/embed/search, /embed/events, /embed/config, /embed/recommendations called with no, an expired, or an invalid embed session token. Mint a new one with POST /embed/session
EMBED_SESSION_REVOKEDThe API key or saved widget that minted the embed session was revoked, expired or disabled; the token stops working within about 30 seconds of the change
STALE_SIGNATUREWebhook timestamp outside the 300-second window
INVALID_SIGNATUREWebhook HMAC mismatch

402

CodeRaised by
PAYMENT_REQUIREDThe plan does not include the feature (details.feature, details.upgradeTo), or the wallet balance cannot cover the next overage block (details.blockCents). Both mean “pay or change plan”, not “retry later”
PLAN_LIMIT_REACHEDCreating another index or API key, or importing documents, would exceed a plan cap (indexes, keys, or documents)
FEATURE_NOT_AVAILABLEThe organization is suspended after a failed subscription payment (dunning): it runs on Free and the paid feature is refused with details.reason = "past_due" and details.upgradeTo = null — pay the overdue invoice, do not upgrade. See Billing

403

CodeRaised by
FORBIDDENKey lacks the required scope or is bound to a different index; embed session used for an operation other than search:read
FORBIDDEN (admin)/admin/* or POST /billing/invoices/generate without a valid x-platform-admin-token
ORIGIN_NOT_ALLOWEDA request whose Origin header is not in the key's allowedOrigins. Enforced on the server for every key-authenticated route (and POST /embed/session), not only through CORS headers; requests without an Origin header (server-to-server) are not affected
INDEX_MISMATCHEmbed session scoped to a different index than the one requested
KEY_IN_GRACEThe calling key was rotated and is in its grace window: it cannot rotate keys or issue scoped keys. Use its successor
KEY_LIFETIME_EXCEEDS_CALLERPOST /api-keys/{keyId}/rotate on a key that would outlive the calling key
FORBIDDEN (tenant-wide)GET /organization/export, DELETE /organization/data or /webhooks/* with a key bound to an index

An acl claim in a search request is ignored — not rejected — when the key has an allowedOrigins list or the request carries an Origin header: a browser-exposed credential cannot choose whose documents it sees. Such searches see only documents without an ACL.

404

CodeRaised by
INDEX_NOT_FOUNDUnknown index slug in the key's organization; embed session with no resolvable index. Engine-level “collection not found” is reported under this public code
DOCUMENT_NOT_FOUNDGET/DELETE on an unknown external_id
PROFILE_NOT_FOUND?profile= naming a profile the index does not have
CONVERSATION_NOT_FOUNDAssistant conversation id that does not belong to the index
NOT_FOUND/metrics without a valid x-metrics-token
JOB_NOT_FOUNDGET/DELETE /jobs/{jobId} with an id of another tenant or (index-bound key) another index
SNAPSHOT_NOT_FOUNDUnknown snapshot id, or one of another index or tenant

409

CodeRaised by
INDEX_ALREADY_EXISTSEngine-level duplicate when creating a collection (reindex and console paths); the physical collection name is never exposed
CONFLICTConflicting write detected by the service layer
IDEMPOTENCY_IN_FLIGHTA document import with the same Idempotency-Key is still running — retry after it finishes
JOB_CONFLICTA reindex or restore was requested (also with ?sync=true) while a reindex or restore is queued or running, or a whole-index re-embed while one is active. details.job_id names it
JOB_NOT_CANCELLABLEDELETE /jobs/{jobId} on a job that finished or already reached the alias swap
SNAPSHOT_LIMIT_REACHEDThe plan's snapshots-per-index limit is reached; retry with ?prune=true
KEY_ALREADY_ROTATEDPOST /api-keys/{keyId}/rotate on a key that was already rotated
KEY_NOT_ACTIVERotating a revoked or expired key

413

CodeRaised by
AUDIO_TOO_LARGEVoice clip over the accepted size

422

CodeRaised by
VALIDATION_ERRORZod schema rejection — includes details.issues
IDEMPOTENCY_KEY_REUSEDThe Idempotency-Key was already used for a different request (method, path or body differ)
INVALID_DOCUMENTMissing content, bad field value, more than 500 documents per import or 1000 per webhook
INVALID_AUDIOAudio payload that cannot be decoded
EMPTY_TRANSCRIPTIONVoice request where no speech was detected
CLIENT_ID_REQUIREDPOST /recent without x-aac-client-id
QUERY_TOO_SHORTPOST /recent with a query under 2 characters
INVALID_SNAPSHOT_NAMEDELETE /admin/snapshots with an unsafe filename

429

CodeRaised by
RATE_LIMITEDPer-key request rate exceeded
QUOTA_EXCEEDEDPlan allowance for the metric exhausted

5xx

CodeStatusRaised by
INTERNAL_ERROR500Unhandled exception — quote the request_id
FEATURE_UNSUPPORTED501Feature needs a newer search engine than the connected node (FEATURE_UNAVAILABLE for features not enabled on this deployment yet)
SEARCH_UNAVAILABLE502Engine unreachable or returned an unmapped failure
AI_UNAVAILABLE502Model provider failure
AI_EMPTY_RESULT502Model returned nothing usable
SNAPSHOT_FAILED502Engine snapshot request failed
SNAPSHOT_NOT_CONFIGURED503POST /admin/snapshot without SEARCH_SNAPSHOT_DIR on the deployment
SEARCH_UNAVAILABLE503Deployment has no search-engine credentials
AI_NOT_CONFIGURED503OPENAI_API_KEY missing for answers, image or transcription
NL_MODEL_NOT_CONFIGURED503The model required by this feature has not been configured
VOICE_MODEL_NOT_CONFIGURED503The model required by this feature has not been configured
CONVERSATION_MODEL_NOT_CONFIGURED503The model required by this feature has not been configured
WEBHOOK_NOT_CONFIGURED503No webhook secret on index or server
SEARCH_TIMEOUT504Engine did not answer in time

Engine and provider errors are mapped to this public contract before they leave the API, and internal names never appear in a response: an engine 404 becomes INDEX_NOT_FOUND, 409 becomes INDEX_ALREADY_EXISTS, 400/422 become INVALID_QUERY (an unknown field in filter_by/sort_by/facet_by/query_by/group_by becomes FIELD_NOT_IN_SCHEMA), timeouts become SEARCH_TIMEOUT, and everything else (including a missing engine configuration) becomes SEARCH_UNAVAILABLE. Other aliases: AI_PROVIDER_ERROR → AI_UNAVAILABLE, ENGINE_FEATURE_UNSUPPORTED → FEATURE_UNSUPPORTED, INDEX_HAS_NO_EMBEDDING_FIELD → INDEX_NOT_VECTOR_ENABLED. The raw engine message is logged server-side and correlated by request_id; quote that id to support.

Rate limits

Requests are limited per API key in a sliding window:

SettingDefaultEnv
Window60 secondsSEARCH_RATE_WINDOW_SECONDS
Requests per window600SEARCH_RATE_MAX_REQUESTS

Every authenticated response carries x-ratelimit-limit, x-ratelimit-remaining and x-ratelimit-reset (Unix seconds), including successful ones, so a client can pace itself. Exceeding the limit is a 429 with a reset time:

curl -sS -i "$AAC_API/indexes/documentation/search?q=pricing" \
  -H "Authorization: Bearer $AAC_KEY"
{
	"error": {
		"code": "RATE_LIMITED",
		"message": "Rate limit exceeded",
		"request_id": "req_9b1d3f5a7c9e1b3d5f7a9c1e",
		"details": { "limit": 600, "reset_at": "2026-10-23T10:42:00.000Z" }
	}
}

The limiter is in-process by default and shared through Redis when REDIS_URL is set. Behind several instances without Redis, each instance enforces its own 600 — plan for that when sizing.

RATE_LIMITED and QUOTA_EXCEEDED are both 429 but mean different things: the first clears in seconds, the second only at the start of the next billing period or when the plan is upgraded. Read error.code, never the status alone.

Plan quotas

Metered metrics are searches, documents, ai_tokens and reindex. Allowances per billing period, from the billing catalog:

FreePro ($29/mo)Enterprise
Searches10,000500,0005,000,000
Documents1,00050,0001,000,000
AI tokens10,000100,0002,000,000
Reindex runs250500
Indexes510100
API keys310100
Connectors510100
Max results per page50250500
Query-log retention7 days90 days365 days

Overage prices per block:

MetricBlockFreeProEnterprise
Searches1,000—50¢25¢
Documents1,000—20¢10¢
AI tokens1,000—2¢1¢
Reindex1 run—5¢2¢

Free has no overage prices at all: reaching a limit returns 429 QUOTA_EXCEEDED and nothing is charged. Paid plans meter overage against the prepaid wallet.

Features gated by plan: Free includes keyword search, synonyms, stopwords, aiAnswer (RAG, within the 10,000 AI-token allowance) and scheduledSync, but not vectorSearch, hybridSearch, curations, grouping, geoSearch, multiSearch, reindex, exportDocuments, customSort or queryAnalytics. Using a gated feature returns 402 PAYMENT_REQUIRED from the metering layer; a plan that lacks the feature fails the request rather than silently degrading it. Missing API key scope still returns 403 FORBIDDEN.

Wallet and overage

  1. Usage is counted per period (YYYY-MM), resetting at 00:00 UTC on the first of the month.
  2. Up to the included allowance, a call costs nothing.
  3. Beyond it, each block is debited from the wallet: searches in 1,000-unit blocks, documents/ai_tokens likewise, reindex per run.
  4. With a zero balance the call fails with 402 PAYMENT_REQUIRED and top-up instructions in the message; a plan feature that needs payment fails the same way.
  5. Top-ups are prepaid credit packs — $10, $50, $200 — bought through the payment provider and reconciled into the ledger.

Read the state before it bites:

curl -sS "$AAC_API/billing/wallet" -H "Authorization: Bearer $AAC_KEY"
{
	"wallet": { "balanceCents": 4200, "currency": "USD" },
	"plan": "pro",
	"period": "2026-10",
	"resets_at": "2026-11-01T00:00:00.000Z",
	"transactions": [
		{
			"id": "txn_01HZYA",
			"type": "credit",
			"amount_cents": 5000,
			"balance_after_cents": 5000,
			"description": "Top-up credits_50",
			"metric": null,
			"created_at": "2026-10-02T09:12:00.000Z"
		},
		{
			"id": "txn_01HZYB",
			"type": "debit",
			"amount_cents": -800,
			"balance_after_cents": 4200,
			"description": "search overage: 16 blocks",
			"metric": "searches",
			"created_at": "2026-10-21T18:03:11.000Z"
		}
	],
	"request_id": "req_4f6a8c0e2b4d6f8a0c2e4b6d"
}
curl -sS "$AAC_API/billing/usage" -H "Authorization: Bearer $AAC_KEY"
{
	"usage": {
		"plan": "pro",
		"planName": "Pro",
		"period": "2026-10",
		"resetsAt": "2026-11-01T00:00:00.000Z",
		"searches": { "used": 500000, "limit": 500000 },
		"documents": { "used": 51204, "limit": 50000 },
		"aiTokens": { "used": 24800, "limit": 100000 },
		"indexes": { "used": 3, "limit": 10 },
		"apiKeys": { "used": 2, "limit": 10 },
		"connectors": { "used": 1, "limit": 10 },
		"wallet": { "balanceCents": 4200, "currency": "USD" }
	},
	"request_id": "req_2b4d6f8a0c2e4b6d8f0a2c4e"
}

documents is counted from the document mirror, not from a counter, so it reflects what is actually in the index. wallet is null when the tenant has never had a credit.

Operational guidance

  • Back off on 429 only. Retry-After is not sent; use details.reset_at or x-ratelimit-reset.
  • Do not retry 4xx other than 429. A 403 will not become a 200 by repeating it; a 422 payload needs fixing.
  • Retry 5xx with jitter. The SDKs retry twice by default with exponential backoff.
  • Watch x-aac-quota-remaining. Failing at the quota boundary is avoidable if you alert at 10% remaining.
  • Alert on zero-result rate, not on 200s. A search that returns nothing is a success by HTTP and a failure by product.

On this page