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
| Method | HTTP |
|---|---|
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
| Method | HTTP |
|---|---|
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
| Method | HTTP |
|---|---|
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
| Method | HTTP |
|---|---|
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
| Method | HTTP |
|---|---|
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:
| Method | Problem | Workaround |
|---|---|---|
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 headers | Sign and post yourself, see Webhooks |
Everything else maps one-to-one onto a documented endpoint.