Zentiq API
Put a lifelike, real-time conversational avatar in your product with a REST API and one WebRTC connection. You define a persona (face + voice + instructions), start a session, and your user talks to it face-to-face at 25 fps.
Authentication
Every request carries an organization API key as a Bearer token. Create keys in the console under Settings → API keys — the full key (zq_live_…) is shown once at creation; we store only a hash.
curl https://app.zentiqai.com/v1/me \
-H "Authorization: Bearer zq_live_XXXXXXXXXXXXXXXX"sessionToken to the browser.Quickstart
Three requests take you from nothing to a live avatar conversation.
- 1 · Pick a face and a voice
curl https://app.zentiqai.com/v1/avatars -H "Authorization: Bearer $ZENTIQ_API_KEY" curl https://app.zentiqai.com/v1/voices -H "Authorization: Bearer $ZENTIQ_API_KEY" - 2 · Create a persona
curl -X POST https://app.zentiqai.com/v1/personas \ -H "Authorization: Bearer $ZENTIQ_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Ava", "systemPrompt": "You are Ava, a friendly product specialist for Acme.", "greeting": "Hi, I'"'"'m Ava — ask me anything about Acme.", "avatarId": "<avatar id>", "voiceId": "<voice id>" }' - 3 · Start a live session
curl -X POST https://app.zentiqai.com/v1/sessions/tokens \ -H "Authorization: Bearer $ZENTIQ_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "personaId": "<persona id>" }'Connect to the returned room with the token — next section — and say hello.
Connecting a client
Sessions run over WebRTC on Zentiq’s realtime transport — install livekit-client (~40 kB) in your web app and connect with the url and sessionToken from the token call. Your backend starts the session; the browser connects with the short-lived token and streams the microphone up.
// Your backend route (Node / Next.js) — the API key never reaches the browser.
export async function POST() {
const r = await fetch("https://app.zentiqai.com/v1/sessions/tokens", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ZENTIQ_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ personaId: process.env.ZENTIQ_PERSONA_ID }),
});
return Response.json(await r.json(), { status: r.status });
}import { Room, RoomEvent, Track } from "livekit-client";
// 1) start a session from YOUR backend (keep the API key server-side)
const res = await fetch("/api/avatar-session", { method: "POST" });
const { sessionToken, url } = await res.json();
// 2) connect and attach the avatar's video + voice
const room = new Room();
room.on(RoomEvent.TrackSubscribed, (track, _pub, participant) => {
// the avatar publishes as participant "zentiq-avatar-…" — attach only that
if (!participant.identity.startsWith("zentiq-avatar")) return;
if (track.kind === Track.Kind.Video) track.attach(document.querySelector("#avatar-video"));
if (track.kind === Track.Kind.Audio) track.attach(document.querySelector("#avatar-audio"));
});
await room.connect(url, sessionToken);
// 3) let the avatar hear your user
await room.localParticipant.setMicrophoneEnabled(true);zentiq-avatar-… participant, as shown — the room can contain other service participants whose tracks are intentionally silent. When the user hangs up, call POST /sessions/:id/end so billing stops immediately. Captions: the live transcript streams on the data-channel topic "chat" (assistant words as { role, text, final }, plus user-transcription events) — render it for closed captions. The token response’s cc flag mirrors the persona’s settings.cc so you know whether to show them by default.SDKs
Two official packages wrap everything above. @zentiq/node is the fully-typed server client (every endpoint, response envelopes unwrapped, idempotency and webhook verification built in). @zentiq/client is the ~2 kB browser helper that connects a session, attaches the avatar’s audio + video, and surfaces speaking state and the live transcript. Both are published on npm.
npm i @zentiq/nodenpm i @zentiq/client livekit-clientimport { Zentiq } from "@zentiq/node";
const zentiq = new Zentiq({ apiKey: process.env.ZENTIQ_API_KEY });
// Your backend route — mint a short-lived client token for a persona.
export async function POST() {
const session = await zentiq.sessions.createToken({
personaId: process.env.ZENTIQ_PERSONA_ID,
});
// { sessionId, sessionToken, url, room, persona } — hand this to the browser
return Response.json(session);
}
// The whole API is typed and enveloping is handled for you:
// await zentiq.avatars.list(); await zentiq.voices.list();
// await zentiq.personas.create({ … }); await zentiq.me();
// Live-session control (mid-call):
// await zentiq.sessions.say(id, { text: "One moment while I check that." });
// await zentiq.sessions.steer(id, { language: "French" });
// await zentiq.sessions.transcript(id); await zentiq.sessions.end(id);import { startSession } from "@zentiq/client";
// url + sessionToken came from YOUR backend route above
const { url, sessionToken } = await (await fetch("/api/avatar-session", { method: "POST" })).json();
const session = await startSession({
url,
token: sessionToken,
video: document.querySelector("#avatar-video"), // avatar A/V attaches here
microphone: true, // let the avatar hear the user
onSpeaking: (who) => (badge.textContent = who), // "avatar" | "user" | "none"
onTranscript: (t) => console.log(t.role, t.text),
});
// later
await session.setMicrophoneEnabled(false); // mute
session.end(); // hang up (also stops billing)import { verifyWebhook } from "@zentiq/node";
// In your webhook handler — throws if the signature or timestamp is invalid.
const event = verifyWebhook(rawBody, req.headers, process.env.ZENTIQ_WEBHOOK_SECRET);
// event = { id, type, createdAt, data } e.g. type "session.ended" | "transcript.final"@zentiq/client lists livekit-client as a peer dependency, so it stays out of your bundle if you already use LiveKit. Every SDK method throws a typed ZentiqError carrying the code from the Errors table below, the HTTP status and the X-Request-Id.Errors
Success responses use the { data, error: null, requestId } envelope. Every error response — any 4xx/5xx — has exactly one shape: { error: { code, message } }, where code is a stable machine-readable string from the table below and the HTTP status is conventional. Every response also carries an X-Request-Id header — include it when you contact support.
Pagination. List endpoints accept ?limit (1–200, default 50) and ?cursor; the response includes a nextCursor (or null) to fetch the next page. An out-of-range limit or a malformed cursor is rejected with 400 invalid_request (not silently ignored). Idempotency. Send an Idempotency-Key header on a create call and a retry with the same key + body replays the original response instead of creating a duplicate.
Rate limits. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. Over the limit returns 429 rate_limited with a Retry-After header — back off until then.
IDs. Resource ids are prefixed and opaque — pa_… personas, av_… avatars, vo_… voices, sess_… sessions, key_… API keys, wh_… webhooks, kb_… knowledge bases and doc_… documents. Send them back exactly as received; endpoints also accept un-prefixed ids on input.
Languages. The primary set — English, French, German, Spanish, Italian, Portuguese, Japanese, Korean, Russian, Chinese (see GET /languages) — and Premier voices speak 36 languages in total from the same voice. Any voice speaks any of them — the voice is timbre only; you do notpick a different voice per language. Set the language on a persona’s settings.language or per session on /sessions/tokens — an ISO 639-1 code ("es") or full name ("Spanish"); the API returns the ISO code. Language switching is automatic — the avatar understands the user in any language and switches when asked; there is nothing to configure. The Global engine (settings.engine = "global") extends spoken output to 70+ languages, and accent speaks the base language with a target accent.
Language tutors — hear one language, speak another. The avatar hears the user natively: a learner saying “hola” is understood as Spanish automatically — in any language, with nothing to configure. Set settings.language to what the avatar speaks (e.g. English immersion) and let the learner talk in their practice language — there is no recognition language to configure.
{ "error": { "code": "not_found", "message": "Persona not found." } }unauthorized401Missing, invalid or revoked API key.
forbidden403The API key is missing the scope this endpoint requires.
invalid_request400 / 413A parameter is missing, malformed, or too large. The message says which.
unsupported_media_type415A knowledge document upload isn't an accepted type (pdf, txt, md, docx, csv).
not_found404The resource doesn't exist or belongs to another organization.
gone410The id existed but the resource has been deleted (distinct from a never-existed 404).
session_ended409say / steer targeted a session that has already ended or isn't live.
idempotency_conflict409An Idempotency-Key was reused with a different request body.
insufficient_minutes402The organization's monthly minute cap has been reached.
rate_limited429Per-key request rate limit. Respect Retry-After and the X-RateLimit-* headers.
concurrency_limit429Too many live sessions at once. End some (POST /sessions/end-idle) rather than just retrying.
capacity503Every avatar slot is busy. Retry with exponential backoff.
upstream_error502A realtime component didn't respond. Safe to retry.
server_error500 / 503Something is misconfigured on our side.
Minutes & billing
Usage is metered in live minutes — the time between a session starting and ending. Idle published personas cost nothing. Each plan includes a monthly minute allowance; beyond it, sessions keep working and additional minutes are metered at your plan's overage rate (see pricing).
Track consumption in real time with GET /usage or on the console's Billing page — both read the same meter.
Who am I
Verify a key and see which organization and plan it belongs to.
/meThe fastest smoke test that a key works. Returns the organization, its plan, and the key's metadata (never the secret).
curl https://app.zentiqai.com/v1/me \
-H "Authorization: Bearer $ZENTIQ_API_KEY"{
"data": {
"organization": { "id": "org_…", "displayName": "Acme Inc", "plan": "growth" },
"plan": { "key": "growth", "name": "Growth", "includedMinutes": 500 },
"apiKey": { "id": "key_…", "name": "Production", "prefix": "zq_live_ab12", "lastUsedAt": "…" }
}
}Avatars
The visual identity your persona renders as. Use a face from the 20-avatar stock gallery, or upload your own portrait.
/avatarsBuilt-in stock avatars (organization-independent) plus every custom avatar your organization has created.
curl https://app.zentiqai.com/v1/avatars \
-H "Authorization: Bearer $ZENTIQ_API_KEY"{
"data": [
{ "id": "av_…", "name": "Priya", "kind": "builtin", "previewUrl": "https://app.zentiqai.com/v1/avatars/av_…/preview", "createdAt": "…" },
{ "id": "av_…", "name": "Our CEO", "kind": "custom", "previewUrl": "https://app.zentiqai.com/v1/avatars/av_…/preview", "createdAt": "…" }
]
}/avatarsBring your own portrait. Best results: a close head-and-shoulders crop (head about half the frame height), mouth closed, on a plain green background — the renderer keys green per frame for a clean live cutout.
namestringDisplay name for the avatar. Defaults to “Custom avatar”.
imageBase64stringThe portrait as a data:image/… base64 URI (max ~6 MB). Provide this or imageUrl.
imageUrlstringAn https URL to fetch the portrait from instead of inlining it.
curl -X POST https://app.zentiqai.com/v1/avatars \
-H "Authorization: Bearer $ZENTIQ_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "Our CEO", "imageUrl": "https://example.com/ceo.png" }'{
"data": { "id": "av_…", "name": "Our CEO", "kind": "custom", "previewUrl": "https://app.zentiqai.com/v1/avatars/av_…/preview", "createdAt": "…" }
}/avatars/:idcurl https://app.zentiqai.com/v1/avatars/av_123 \
-H "Authorization: Bearer $ZENTIQ_API_KEY"{ "data": { "id": "av_123", "name": "Our CEO", "kind": "custom", "previewUrl": "https://app.zentiqai.com/v1/avatars/av_123/preview", "createdAt": "…" } }/avatars/:idBuilt-in stock avatars can't be deleted. Personas already referencing the avatar keep working until re-published.
curl -X DELETE https://app.zentiqai.com/v1/avatars/av_123 \
-H "Authorization: Bearer $ZENTIQ_API_KEY"{ "deleted": true, "id": "av_123" }Voices
Two voice engines, selected with settings.engine on the persona. "premier" (the default) is realtime speech-to-speech: every voice is a cross-lingual clone that speaks 36 languages in its own timbre — including voices you clone from a short sample. "global" covers 70+ languages with prebuilt world voices (no cloning) — pick one with settings.globalVoice. Pick a voice for how it sounds — set the language it speaks on the persona (settings.language) or per session, not by choosing a different voice. `GET /languages` lists the primary set.
/languagesThe primary languages with first-class ISO codes — any voice speaks any of them, and Premier voices speak 36 languages in total. The avatar understands the user in ANY language automatically and switches when asked; the Global tier extends spoken output to 70+ languages.
curl https://app.zentiqai.com/v1/languages \
-H "Authorization: Bearer $ZENTIQ_API_KEY"{ "data": { "languages": [ { "code": "en", "name": "English" }, { "code": "es", "name": "Spanish" }, { "code": "de", "name": "German" } ], "engines": { "premier": { "languageCount": 36, "voiceCloning": true, "default": true }, "global": { "languageCount": 70, "voiceCloning": false } }, "accents": [ { "key": "french", "label": "French" } ] } }/voicescurl https://app.zentiqai.com/v1/voices \
-H "Authorization: Bearer $ZENTIQ_API_KEY"{
"data": [
{ "id": "vo_…", "name": "Sofia", "language": "en", "languages": ["en","fr","de","es","it","pt","ja","ko","ru","zh"], "kind": "builtin", "createdAt": "…" },
{ "id": "vo_…", "name": "Founder voice", "language": "en", "languages": ["en","fr","de","es","it","pt","ja","ko","ru","zh"], "kind": "custom", "createdAt": "…" }
]
}/voicesSend ~10–60 seconds of clean, single-speaker audio. Multipart (name + sample file) or JSON with audioBase64. The clone is zero-shot: it immediately speaks all 36 supported languages in the cloned timbre.
namestringDisplay name for the voice. Defaults to “My voice”.
samplefile (multipart)The audio sample (wav/mp3/m4a…, max 25 MB). Use multipart/form-data.
audioBase64string (JSON)Alternative to multipart: the sample as base64 or a data:audio/… URI.
formatstring (JSON)Sample container when using bare base64, e.g. "mp3". Default "wav".
curl -X POST https://app.zentiqai.com/v1/voices \
-H "Authorization: Bearer $ZENTIQ_API_KEY" \
-F "name=Founder voice" \
-F "sample=@founder.wav"{
"data": { "id": "vo_…", "name": "Founder voice", "language": "en", "languages": ["en","fr","de","es","it","pt","ja","ko","ru","zh"], "kind": "custom", "createdAt": "…" }
}/voices/:idcurl https://app.zentiqai.com/v1/voices/vo_123 \
-H "Authorization: Bearer $ZENTIQ_API_KEY"{ "data": { "id": "vo_123", "name": "Founder voice", "language": "en", "languages": ["en","fr","de","es","it","pt","ja","ko","ru","zh"], "kind": "custom", "createdAt": "…" } }/voices/:idcurl -X DELETE https://app.zentiqai.com/v1/voices/vo_123 \
-H "Authorization: Bearer $ZENTIQ_API_KEY"{ "deleted": true, "id": "vo_123" }Personas
A persona is the complete character your users talk to: an avatar + a voice + a system prompt, greeting and behavior settings.
/personascurl https://app.zentiqai.com/v1/personas \
-H "Authorization: Bearer $ZENTIQ_API_KEY"{
"data": [
{
"id": "pa_…", "name": "Ava — Support", "systemPrompt": "You are Ava…",
"greeting": "Hi! How can I help today?", "avatarId": "av_…", "voiceId": "vo_…",
"settings": { "engine": "premier", "language": "en", "greet_mode": "single", "interruptible": true, "cc": false },
"avatar": { "id": "av_…", "name": "Priya", "kind": "builtin", "previewUrl": "https://app.zentiqai.com/v1/avatars/av_…/preview" },
"voice": { "id": "vo_…", "name": "Sofia", "kind": "builtin" },
"createdAt": "…", "updatedAt": "…"
}
]
}/personasnamestringrequiredThe persona's name — the avatar introduces itself with it.
systemPromptstringrequiredWho the persona is and how it behaves. This drives every reply.
greetingstringThe opening line spoken when a session starts.
avatarIdstringA stock or custom avatar id from GET /avatars. Defaults to the built-in face.
voiceIdstringA voice id from GET /voices.
settingsobjectBehaviour settings (all validated, sensible defaults). engine: "premier" (default — realtime speech-to-speech, 36 languages from any voice, cloning) | "global" (70+ languages, prebuilt voices) · globalVoice: the Global-tier prebuilt voice name (only when engine is "global") · language: ISO 639-1 or full name — what the avatar speaks (see GET /languages) · greet_mode: "single"|"rotate"|"ai" · greetings: string[] · skipGreeting: bool · interruptible: bool (barge-in, default true) · cc: bool (show live captions by default) · accent: speak ENGLISH with a native accent — ""|french|german|spanish|italian|portuguese|japanese|korean|russian|chinese (e.g. "spanish" = Spanish-accented English; only when language is English) · voice_accent: English regional accent — neutral|amgen|valley|south|rp|brixton|transat|aussie · company: string. Language switching is automatic — there is no recognition language to configure. Unknown fields are silently ignored.
curl -X POST https://app.zentiqai.com/v1/personas \
-H "Authorization: Bearer $ZENTIQ_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Ava",
"systemPrompt": "You are Ava, a friendly product specialist for Acme.",
"greeting": "Hi, I'"'"'m Ava — ask me anything about Acme.",
"avatarId": "av_123",
"voiceId": "vo_456",
"settings": { "engine": "premier", "language": "English", "greet_mode": "single", "interruptible": true, "cc": true, "company": "Acme Inc" }
}'{ "data": { "id": "pa_…", "name": "Ava", "avatarId": "av_123", "voiceId": "vo_456", "settings": { "engine": "premier", "language": "en", "…": "…" }, "createdAt": "…" } }/personas/:idcurl https://app.zentiqai.com/v1/personas/pa_123 \
-H "Authorization: Bearer $ZENTIQ_API_KEY"{ "data": { "id": "pa_123", "name": "Ava", "systemPrompt": "…", "avatar": { … }, "voice": { … } } }/personas/:idSend only the fields you want to change. Setting avatarId/voiceId/greeting to null clears them. To switch voice engines, set settings.engine (and settings.globalVoice for the Global tier) — omitting engine keeps the current one.
curl -X PATCH https://app.zentiqai.com/v1/personas/pa_123 \
-H "Authorization: Bearer $ZENTIQ_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "settings": { "engine": "global", "globalVoice": "Aoede" } }'{ "data": { "id": "pa_123", "settings": { "engine": "global", "globalVoice": "Aoede", "…": "…" }, "updatedAt": "…" } }/personas/:idSoft delete — session history for the persona is preserved.
curl -X DELETE https://app.zentiqai.com/v1/personas/pa_123 \
-H "Authorization: Bearer $ZENTIQ_API_KEY"{ "deleted": true, "id": "pa_123" }Sessions
A session is one live conversation. Create a token, hand it to your client, connect over WebRTC, and end it when the user hangs up.
/sessions/tokensProvisions a dedicated GPU render slot + realtime conversation engine for the persona, and returns a client token for the WebRTC room. Billing for the session's live minutes starts now. Returns 503 with code "capacity" when every slot is busy — retry with backoff.
personaIdstringrequiredThe persona to bring live.
identitystringYour user's identity in the room. Defaults to a generated id.
languagestringOverride the persona's conversation language for this session, e.g. "German".
overridesobjectPersonalize ONE base persona per session — no need to create a persona per user. { systemPrompt, greeting, variables }. {{name}} placeholders in the prompt/greeting are filled from variables at start.
curl -X POST https://app.zentiqai.com/v1/sessions/tokens \
-H "Authorization: Bearer $ZENTIQ_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"personaId": "pa_123",
"identity": "user-42",
"overrides": {
"greeting": "Hey {{name}}! Ready for lesson {{lesson}}?",
"variables": { "name": "Roger", "lesson": "3" }
}
}'{
"sessionId": "sess_…",
"sessionToken": "eyJhbGciOi…",
"url": "wss://…",
"room": "zentiq-…",
"persona": { "id": "pa_123", "name": "Ava" }
}/sessionslimitinteger1–200, default 50. Newest first.
statusstringFilter: ACTIVE | COMPLETED | ABANDONED.
curl "https://app.zentiqai.com/v1/sessions?status=COMPLETED&limit=20" \
-H "Authorization: Bearer $ZENTIQ_API_KEY"{
"data": [
{ "id": "sess_…", "personaId": "pa_…", "status": "COMPLETED", "source": "api", "minutesBilled": 4.27, "startedAt": "…", "endedAt": "…", "duration": 256 }
]
}/sessions/:idcurl https://app.zentiqai.com/v1/sessions/sess_123 \
-H "Authorization: Bearer $ZENTIQ_API_KEY"{ "data": { "id": "sess_123", "status": "ACTIVE", "startedAt": "…", "minutesBilled": null } }/sessions/:id/endReleases the GPU slot and stamps the final duration + billed minutes. Idempotent — ending an already-finished session returns it unchanged. DELETE /sessions/:id is an alias. Call this when your user hangs up; sessions whose room empties are also reclaimed automatically.
curl -X POST https://app.zentiqai.com/v1/sessions/sess_123/end \
-H "Authorization: Bearer $ZENTIQ_API_KEY"{ "data": { "id": "sess_123", "status": "COMPLETED", "duration": 256, "minutesBilled": 4.27, "endedAt": "…" } }/sessions/end-idleForce-end this org's dead or over-duration ACTIVE sessions — a client that never calls /end (tab close, crash) leaks its concurrency slot. Sessions also self-heal on the next mint and are swept server-side, so you rarely need this; it's here for an explicit sweep. Returns how many were reclaimed.
curl -X POST https://app.zentiqai.com/v1/sessions/end-idle \
-H "Authorization: Bearer $ZENTIQ_API_KEY"{ "data": { "reaped": 3 } }/sessions/:id/transcriptThe turn-by-turn transcript captured during the session — grade speech, update memory, moderate. It also streams live over the realtime data channel (assistant text on topic "chat", plus user-transcription events) if you want it in real time.
curl https://app.zentiqai.com/v1/sessions/sess_123/transcript \
-H "Authorization: Bearer $ZENTIQ_API_KEY"{
"data": {
"sessionId": "sess_123",
"turns": [
{ "role": "user", "text": "how do I reset my password?", "ts": 4120 },
{ "role": "assistant", "text": "Head to Settings → Security and click Reset…", "ts": 6890 }
]
}
}/sessions/:idAdjust a running session without restarting it — swap the system prompt and/or language for the next turns (raise immersion, switch topic, correct a recurring mistake). 409 if the session isn't live.
systemPromptstringNew instructions the avatar follows on subsequent turns.
languagestringSwitch the conversation language, e.g. "German".
variablesobjectArbitrary values your prompt can reference.
curl -X PATCH https://app.zentiqai.com/v1/sessions/sess_123 \
-H "Authorization: Bearer $ZENTIQ_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "systemPrompt": "Now reply only in German, keep it short." }'{ "data": { "id": "sess_123", "status": "ACTIVE" } }/sessions/:id/sayHave the avatar say exact text right now — scripted drills, "repeat after me", announcements, or your own LLM driving speech turn by turn. 409 if the session isn't live.
textstringrequiredWhat the avatar should say (max 2000 characters).
interruptbooleanCut off any in-progress speech first. Default true.
languagestringSpeak this line in a specific language.
curl -X POST https://app.zentiqai.com/v1/sessions/sess_123/say \
-H "Authorization: Bearer $ZENTIQ_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "text": "Repeat after me: guten Morgen." }'{ "data": { "ok": true } }Usage
Metered live minutes with plan context — the same numbers your dashboard billing page shows.
/usagefromISO dateCustom range start. Defaults to the first of the current month (UTC).
toISO dateCustom range end (exclusive). Defaults to the first of next month.
curl https://app.zentiqai.com/v1/usage \
-H "Authorization: Bearer $ZENTIQ_API_KEY"{
"data": {
"plan": { "key": "growth", "name": "Growth", "monthlyUsd": 199 },
"includedMinutes": 500, "minutesUsed": 231.4, "remainingMinutes": 268.6,
"overageMinutes": 0, "overagePerMinuteUsd": 0.25, "estimatedOverageUsd": 0,
"period": { "start": "2026-07-01T00:00:00.000Z", "end": "2026-08-01T00:00:00.000Z" },
"sessionCount": 58,
"bySource": { "dashboard": 12.1, "api": 219.3 },
"byDay": [ { "date": "2026-07-01", "minutes": 42.5, "sessions": 11 } ],
"byPersona": [ { "personaId": "pa_…", "personaName": "Ava", "minutes": 180.2, "sessions": 40 } ]
}
}Webhooks
Get notified server-side of session + transcript events. Each delivery POSTs to your url with headers X-Zentiq-Signature (sha256=HMAC of `${timestamp}.${body}`), X-Zentiq-Timestamp and X-Zentiq-Event, and a body of { id, type, createdAt, data }. Verify with @zentiq/node's verifyWebhook. Requires the webhooks:manage scope.
/webhooksSubscribe an https endpoint. `events` is a subset of the known types or ["*"] for all. Returns the signing `secret` ONCE — store it.
urlstringrequiredYour https endpoint.
eventsstring[]session.started · session.ended · transcript.final · voice.clone.completed · usage.threshold — or ["*"].
curl -X POST https://app.zentiqai.com/v1/webhooks \
-H "Authorization: Bearer $ZENTIQ_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com/zentiq", "events": ["session.ended", "transcript.final"] }'{ "data": { "id": "wh_…", "url": "https://example.com/zentiq", "events": ["session.ended","transcript.final"], "active": true, "secret": "whsec_…", "createdAt": "…" } }/webhookscurl https://app.zentiqai.com/v1/webhooks \
-H "Authorization: Bearer $ZENTIQ_API_KEY"{ "data": [ { "id": "wh_…", "url": "…", "events": ["*"], "active": true, "createdAt": "…" } ] }/webhooks/:idcurl -X DELETE https://app.zentiqai.com/v1/webhooks/wh_123 \
-H "Authorization: Bearer $ZENTIQ_API_KEY"{ "data": { "deleted": true, "id": "wh_123" } }API keys
Mint scoped keys and rotate them programmatically. Scopes: sessions:read/write · personas:read/write · voices:read/write · avatars:read/write · knowledge:read/write · usage:read · webhooks:manage. An empty scope list = full access. Only a full-access key can manage keys.
/keysOptionally scope it. Rotation = create a new key, then DELETE the old one. Returns the full `key` ONCE.
namestringLabel for the key.
scopesstring[]e.g. ["sessions:write", "personas:read"]. Omit or [] for full access.
curl -X POST https://app.zentiqai.com/v1/keys \
-H "Authorization: Bearer $ZENTIQ_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "server", "scopes": ["sessions:write", "personas:read"] }'{ "data": { "id": "key_…", "name": "server", "prefix": "zq_live_…", "scopes": ["sessions:write","personas:read"], "key": "zq_live_…", "createdAt": "…" } }/keyscurl https://app.zentiqai.com/v1/keys \
-H "Authorization: Bearer $ZENTIQ_API_KEY"{ "data": [ { "id": "key_…", "name": "…", "prefix": "zq_live_…", "scopes": [], "lastUsedAt": "…", "createdAt": "…" } ] }/keys/:idcurl -X DELETE https://app.zentiqai.com/v1/keys/key_123 \
-H "Authorization: Bearer $ZENTIQ_API_KEY"{ "data": { "revoked": true, "id": "key_123" } }Knowledge (RAG)
Give an avatar a knowledge base: create a base, add documents (they're chunked + embedded), attach the base to a persona, and every session turn is grounded on the retrieved passages — automatically, server-side. Documents ingest asynchronously (status processing → ready) and fire the knowledge.document.* webhooks. Scopes: knowledge:read / knowledge:write.
/knowledge/basesnamestringrequirede.g. "Product Docs".
curl -X POST https://app.zentiqai.com/v1/knowledge/bases \
-H "Authorization: Bearer $ZENTIQ_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "Product Docs" }'{ "data": { "id": "kb_…", "name": "Product Docs", "documentCount": 0, "createdAt": "…", "updatedAt": "…" } }/knowledge/baseslimitnumber1–200 (default 50).
cursorstringnextCursor from a previous page.
curl https://app.zentiqai.com/v1/knowledge/bases \
-H "Authorization: Bearer $ZENTIQ_API_KEY"{ "data": [ { "id": "kb_…", "name": "Product Docs", "documentCount": 12, "createdAt": "…" } ], "nextCursor": null }/knowledge/bases/:id/documentsThree modes: multipart `file`; JSON `{ url }`; or JSON `{ text }`. Returns the document immediately with status "processing" — poll GET /knowledge/documents/:id or subscribe to the knowledge.document.processed / .failed webhooks. Files: pdf, txt, md, docx, csv ≤ 25 MB.
curl -X POST https://app.zentiqai.com/v1/knowledge/bases/kb_123/documents \
-H "Authorization: Bearer $ZENTIQ_API_KEY" \
-F "file=@handbook.pdf"{ "data": { "id": "doc_…", "knowledgeBaseId": "kb_123", "name": "handbook.pdf", "type": "pdf", "status": "processing", "chunkCount": null, "createdAt": "…" } }/knowledge/documents/:idcurl https://app.zentiqai.com/v1/knowledge/documents/doc_123 \
-H "Authorization: Bearer $ZENTIQ_API_KEY"{ "data": { "id": "doc_123", "status": "ready", "chunkCount": 42, "type": "pdf", "bytes": 248210 } }/knowledge/documents/:id/downloadShort-TTL (10 min) signed URL — never a raw storage path.
curl https://app.zentiqai.com/v1/knowledge/documents/doc_123/download \
-H "Authorization: Bearer $ZENTIQ_API_KEY"{ "data": { "url": "https://…", "expiresAt": "…" } }/knowledge/documents/:idcurl -X DELETE https://app.zentiqai.com/v1/knowledge/documents/doc_123 \
-H "Authorization: Bearer $ZENTIQ_API_KEY"{ "data": { "deleted": true, "id": "doc_123" } }/knowledge/bases/:id/searchquerystringrequired≤ 4 KB.
topKnumber1–50 (default 5).
minScorenumber0–1 similarity floor (default 0).
curl -X POST https://app.zentiqai.com/v1/knowledge/bases/kb_123/search \
-H "Authorization: Bearer $ZENTIQ_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "query": "how do refunds work?", "topK": 5 }'{ "data": { "results": [ { "documentId": "doc_…", "text": "…retrieved chunk…", "score": 0.83, "metadata": { "page": 4 } } ] } }/personas/:idSet `knowledge.baseIds` on a persona (see Personas). Every session with that persona is then grounded automatically. `null` clears it.
knowledgeobject{ baseIds: ["kb_…"], topK?, minScore?, maxChars? }
curl -X PATCH https://app.zentiqai.com/v1/personas/pa_123 \
-H "Authorization: Bearer $ZENTIQ_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "knowledge": { "baseIds": ["kb_123"], "topK": 5 } }'{ "data": { "id": "pa_123", "knowledge": { "baseIds": ["kb_123"], "topK": 5, "minScore": 0 } } }Ready to build?
Create an account, design a persona in the studio, and mint your first API key in minutes.
Get your API key