Node SDK
@zentiq/node calls every operation of the API from your server, with types generated from the OpenAPI document. It runs on Node 18 or later, with no dependencies.
npm install @zentiq/node@^1Coming to npm
@zentiq/node 1.0.0 is being released with the 2026-10-01 contract. npm has 0.2.0, an earlier version for the 2026-07-08 contract. The command above installs 1.0.0 or a later 1.x only, so it works as soon as 1.0.0 is out.The client#
import { RateLimitError, UpgradeRequiredError, Zentiq, ZentiqError } from "@zentiq/node";
const zentiq = new Zentiq({
apiKey: process.env.ZENTIQ_API_KEY, // a server secret: never in a web page or an app people download
// apiVersion: "2026-10-01", // the contract to ask for; the SDK sends the newest by default
// maxRetries: 2, // safe retries on network errors, 429 and 5xx (the default)
});| Option | Default | What it does |
|---|---|---|
apiKey | process.env.ZENTIQ_API_KEY | Your organisation API key. A server secret. |
baseUrl | https://app.zentiqai.com/v1 | Another address, for a proxy of your own. |
apiVersion | 2026-10-01 | The contract every request asks for (Zentiq-Version). |
maxRetries | 2 | Retries of a failed request, where a retry is safe. |
timeoutMs | 90000 | How long one attempt may take. |
fetch | the global fetch | Your own, for tests or a custom agent. |
Operations#
Every operation is a method named after its operation id (the reference shows each one). Path parameters come first, then the body (or, for a GET, the query), then the options:
const persona = await zentiq.createPersona({ name: "Ava", systemPrompt: "You are Ava…" });
const same = await zentiq.getPersona(persona.id);
await zentiq.updatePersona(persona.id, { greeting: "Hello!" });
const page = await zentiq.listPersonas({ limit: 20 }); // { data, nextCursor }
await zentiq.deletePersona(persona.id);A method answers with the envelope's data, typed; a list answers { data, nextCursor }. The last argument takes options for one call: idempotencyKey, apiVersion, timeoutMs, signal (an AbortSignal) and headers.
zentiq.request("createPersona", { body }) calls any operation by its id, and zentiq.raw(…) answers with the whole response (its status, headers and body) instead of throwing on an error.
Pages#
// One page at a time…
const first = await zentiq.listPersonas({ limit: 50 });
if (first.nextCursor) await zentiq.listPersonas({ limit: 50, cursor: first.nextCursor });
// …or every item, fetching pages as you go.
for await (const persona of zentiq.paginate("listPersonas", { limit: 100 })) console.log(persona.id, persona.name);Errors#
Every failure throws a ZentiqError, or one of its kinds:
| Class | When |
|---|---|
InvalidRequestError | 400 invalid_request: param names the field. |
ConsentError | 400 consent_required, consent_invalid, consent_outdated, and 409 consent_withdrawn. |
AuthenticationError | 401: the key is missing, wrong or revoked. |
InsufficientMinutesError | 402 insufficient_minutes. |
PermissionError | 403 forbidden (a missing scope, named), candidate_interview, learner_conversation. |
UpgradeRequiredError | 403 upgrade_required: feature, plan, limit, from either contract's shape. |
FeatureNotInPlanError | 403 feature_not_in_plan, account_paused. |
NotFoundError | 404 not_found. |
ConflictError | 409 conflict, session_ended, on_hold: reason says why. |
VersionConflictError | 409 version_conflict: currentVersion. |
DuoNotLiveError | 409 duo_not_live. |
IdempotencyError | 409 idempotency_conflict, idempotency_in_progress. |
GoLiveRefusedError | 422 go_live_refused: problems and warnings. |
RateLimitError | 429 rate_limited, concurrency_limit: retryAfter in seconds. |
ServerError | 5xx. |
ConnectionError | No answer: the network failed or the attempt timed out. |
try {
await zentiq.createVoice({
name: "Dana",
audioBase64: "data:audio/wav;base64,UklGRi…",
consent: { attested: true, subject: "someone_else", subjectName: "Dana Lee" },
});
} catch (e) {
if (e instanceof UpgradeRequiredError) {
console.log(`Not on this plan: ${e.feature}. ${e.plan ? `The ${e.plan} plan includes it.` : ""}`);
} else if (e instanceof RateLimitError) {
console.log(`Slow down: try again in ${e.retryAfter} seconds.`);
} else if (e instanceof ZentiqError) {
console.log(e.status, e.code, e.message, e.param, e.requestId); // quote requestId to support
} else throw e;
}Waiting for work to finish#
waitUntilReady polls a resource until it is done, and throws if it failed or took too long:
const doc = await zentiq.createDocument(pack.id, { url: "https://www.example.com/help" });
await zentiq.waitUntilReady("getDocument", doc.id); // ready, or throws on failedIt works with any operation that reads one resource by id and has a status: a document (ready or failed), a duo (live or needs_changes).
Uploads#
zentiq.uploadDocument(packId, { file, filename }) sends a file (PDF, Word, text, Markdown or CSV) to a knowledge pack as a multipart upload: file is a Blob, a Buffer or bytes.
Webhooks#
import { verifyWebhook, WebhookSignatureError, Zentiq, type ZentiqWebhookEvent } from "@zentiq/node";
const zentiq = new Zentiq({ apiKey: process.env.ZENTIQ_API_KEY });
const secret = process.env.ZENTIQ_WEBHOOK_SECRET ?? "";
const seen = new Set<string>(); // use your database in production
http
.createServer(async (req, res) => {
let raw = "";
for await (const chunk of req) raw += chunk; // the raw body, exactly as it arrived
let event: ZentiqWebhookEvent;
try {
event = verifyWebhook(raw, req.headers, secret);
} catch (e) {
res.writeHead(e instanceof WebhookSignatureError ? 400 : 500).end();
return;
}
res.writeHead(204).end(); // acknowledge first; do the work after
if (seen.has(event.id)) return; // the same event can arrive more than once
seen.add(event.id);
await handle(event);
})
.listen(4000);verifyWebhook(rawBody, headers, secret) checks X-Zentiq-Signature against the raw body and refuses a timestamp more than five minutes old (toleranceSeconds changes it), then returns the event. Its data is typed by type, so switch (event.type) gives you each event's fields. It throws WebhookSignatureError on any mismatch.