Skip to the content
Menu: Node

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.

Install
npm install @zentiq/node@^1

Coming 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#

server.ts
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)
});
OptionDefaultWhat it does
apiKeyprocess.env.ZENTIQ_API_KEYYour organisation API key. A server secret.
baseUrlhttps://app.zentiqai.com/v1Another address, for a proxy of your own.
apiVersion2026-10-01The contract every request asks for (Zentiq-Version).
maxRetries2Retries of a failed request, where a retry is safe.
timeoutMs90000How long one attempt may take.
fetchthe global fetchYour 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:

TypeScript
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#

basics.ts
// 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:

ClassWhen
InvalidRequestError400 invalid_request: param names the field.
ConsentError400 consent_required, consent_invalid, consent_outdated, and 409 consent_withdrawn.
AuthenticationError401: the key is missing, wrong or revoked.
InsufficientMinutesError402 insufficient_minutes.
PermissionError403 forbidden (a missing scope, named), candidate_interview, learner_conversation.
UpgradeRequiredError403 upgrade_required: feature, plan, limit, from either contract's shape.
FeatureNotInPlanError403 feature_not_in_plan, account_paused.
NotFoundError404 not_found.
ConflictError409 conflict, session_ended, on_hold: reason says why.
VersionConflictError409 version_conflict: currentVersion.
DuoNotLiveError409 duo_not_live.
IdempotencyError409 idempotency_conflict, idempotency_in_progress.
GoLiveRefusedError422 go_live_refused: problems and warnings.
RateLimitError429 rate_limited, concurrency_limit: retryAfter in seconds.
ServerError5xx.
ConnectionErrorNo answer: the network failed or the attempt timed out.
basics.ts
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:

TypeScript
const doc = await zentiq.createDocument(pack.id, { url: "https://www.example.com/help" });
await zentiq.waitUntilReady("getDocument", doc.id); // ready, or throws on failed

It 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#

webhook-receiver.ts
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.