AACSearch

TypeScript SDK

The generated @repo/sdk client — constructor options, every method, retry behaviour and the AACSearchError type.

@repo/sdk is a typed client generated from packages/api/routes/v1/openapi.json (regenerate with pnpm sdk:generate). It is an ESM package that uses the global fetch, so it runs in Node 18+, Bun, Deno and edge runtimes.

import { AACSearchClient, AACSearchError } from "@repo/sdk";

const client = new AACSearchClient({
	apiKey: process.env.AAC_KEY!,
	baseUrl: "https://search.example.com/api/v1",
});

const result = await client.search("documentation", { q: "pricing", per_page: 5 });
console.log(result.found, result.hits[0]?.document.title);

Constructor

interface AACSearchClientOptions {
	/** aac_live_… key. Required. */
	apiKey: string;
	/** Defaults to http://localhost:3000/api/v1 */
	baseUrl?: string;
	/** Retries for 429 and 5xx. Default 2. */
	maxRetries?: number;
	/** Custom fetch implementation, e.g. for tracing or a proxy. */
	fetch?: typeof fetch;
}

Passing an empty apiKey throws immediately, before any request:

new AACSearchClient({ apiKey: "" });
// AACSearchError: apiKey is required (status 401, code MISSING_API_KEY)

A trailing slash on baseUrl is stripped, so https://host/api/v1/ and https://host/api/v1 are equivalent.

Errors and retries

Every failure is an AACSearchError, which carries the parsed envelope plus the HTTP status:

class AACSearchError extends Error {
	name: "AACSearchError";
	status: number; // 401, 403, 422, 429, 502…
	code: string; // UNAUTHORIZED, QUOTA_EXCEEDED, VALIDATION_ERROR…
	details?: unknown; // details from the error body
	requestId?: string; // quote this to support
}
try {
	await client.upsertDocuments("documentation", {
		documents: [{ external_id: "pricing", content: "Pro is 29 USD per month." }],
		action: "create",
	});
} catch (error) {
	if (error instanceof AACSearchError) {
		if (error.code === "QUOTA_EXCEEDED") {
			console.error(
				"Quota exhausted, retry after",
				(error.details as { resetsAt: string }).resetsAt,
			);
		} else if (error.status === 429) {
			// rate limited — back off, the SDK already retried maxRetries times
		} else {
			console.error(error.code, error.message, error.requestId);
		}
	}
	throw error;
}

Retry behaviour: 429 and any 5xx are retried up to maxRetries times (default 2) with exponential backoff of 250 ms, 500 ms, 1 s. 4xx other than 429 is never retried. When the retries are exhausted, the last response is parsed and thrown. If the body has no error object, code falls back to REQUEST_FAILED and the message to Request failed with <status>.

Timeouts are not imposed by the client — pass your own signal-aware fetch if you need one.

Methods

Queries and bodies are plain objects; the client only serialises defined, non-empty values, so undefined and "" are dropped rather than sent as empty parameters.

Search and discovery

MethodHTTP
search(index, query)GET /indexes/{index}/search
searchPost(index, body)POST /indexes/{index}/search
suggest(index, query)GET /indexes/{index}/suggest
multiSearch(body)POST /indexes/multi_search
nlSearch(index, body)POST /indexes/{index}/nl-search
indexStats(index, query?)GET /indexes/{index}/stats
health()GET /health
capabilities()GET /capabilities
openapi()GET /openapi.json
metrics()GET /metrics
const suggestions = await client.suggest("documentation", { q: "pri", limit: 5 });
const stats = await client.indexStats("documentation");
const caps = await client.capabilities();
if (!caps.features.autoEmbedding) throw new Error("semantic search unavailable on this node");

Documents

MethodHTTP
upsertDocuments(index, body)POST /indexes/{index}/documents
getDocument(index, externalId)GET /indexes/{index}/documents/{externalId}
deleteDocument(index, externalId)DELETE /indexes/{index}/documents/{externalId}
deleteDocuments(index, query)DELETE /indexes/{index}/documents?filter_by=…
bulkUpdateDocuments(index, body)POST /indexes/{index}/documents/bulk
exportDocuments(index)GET /indexes/{index}/documents/export
await client.upsertDocuments("documentation", {
	documents: [
		{
			external_id: "pricing",
			title: "Pricing",
			content: "Pro is 29 USD per month with 500,000 searches.",
			tags: ["billing"],
		},
	],
	action: "upsert",
});

const doc = await client.getDocument("documentation", "pricing");
await client.deleteDocument("documentation", "pricing");
await client.deleteDocuments("documentation", { filter_by: "source_type:=connector" });

AI

MethodHTTP
answer(index, body)POST /indexes/{index}/answer — one-shot RAG with citations
assistantAsk(index, body)POST /indexes/{index}/assistant — conversation turn
assistantStream(index, body, onEvent)POST /indexes/{index}/assistant/stream — SSE
assistantNative(index, body)POST /indexes/{index}/assistant/native — engine conversation model
assistantThreads(index)GET /indexes/{index}/assistant
assistantThread(index, conversationId)GET /indexes/{index}/assistant/{conversationId}
conversationModels(index)GET /indexes/{index}/conversation-models
voiceSearch(index, body)POST /indexes/{index}/voice
imageSearch(index, body)POST /indexes/{index}/image
const answer = await client.answer("documentation", {
	question: "How much does the pro plan cost?",
	per_page: 4,
});
console.log(answer.answer, answer.citations);

await client.assistantStream("documentation", { question: "And the limits?" }, (event) => {
	if (event.type === "delta")
		process.stdout.write(String((event.payload as { text?: string }).text ?? ""));
});

assistantStream reads the response body itself, splits frames on a blank line, parses each data: line and hands you { type, payload }. Event types are citations, delta, done and error. It resolves when the stream closes; an empty body throws AACSearchError with code SSE_EMPTY.

Relevance and operations

MethodHTTP
listSynonyms(index) / putSynonym(index, body) / deleteSynonym(index, id)/indexes/{index}/synonyms
listCurations(index) / putCuration(index, body) / deleteCuration(index, id)/indexes/{index}/curations
trackEvent(index, body)POST /indexes/{index}/events
reindex(index)POST /indexes/{index}/reindex
scopedKey(index, body)POST /indexes/{index}/scoped-key
triggerSnapshot()POST /admin/snapshot
runJobs(query)POST /admin/jobs/run
exportOrganization()GET /organization/export
purgeOrganization(query)DELETE /organization/data

Analytics and billing

MethodHTTP
analyticsOverview(query)GET /analytics/overview
analyticsOverviewCsv(query)GET /analytics/overview.csv
slowQueries(query)GET /analytics/slow-queries
trendingQueries(query)GET /analytics/trending
usage()GET /billing/usage
wallet()GET /billing/wallet
const { overview } = await client.analyticsOverview({ days: 7 });
console.log(overview.totals.searches, overview.totals.zeroResultRate);

const { usage } = await client.usage();
console.log(`${usage.searches.used}/${usage.searches.limit} searches this period`);

Known gaps in the generated client

The client is generated mechanically from the OpenAPI document, and three methods do not match the server exactly. They are listed here so you do not lose an afternoon to them:

MethodProblemWorkaround
multiSuggest()Sends no body, but the endpoint requires { indexes, q }fetch POST /indexes/suggest/multi
exportDocuments(index)Parses the NDJSON export with response.json()fetch the export endpoint and read text
webhookIngest(index)Sends no body and no signature headersSign and post yourself, see Webhooks

Everything else maps one-to-one onto a documented endpoint.

On this page