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.

Base URL  https://app.zentiqai.com/v1JSON in, JSON out · TLS onlyInteractive API reference ↗OpenAPI spec ↗

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"
Keep keys on your server. Never ship an API key in web or mobile client code — start sessions from your backend and hand only the short-lived sessionToken to the browser.

Quickstart

Three requests take you from nothing to a live avatar conversation.

  1. 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. 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. 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.

Backend — token exchange
// 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 });
}
Browser — connect & talk
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);
Attach media only from the 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.
Prefer not to hand-wire this? Use the official SDKs — the same flow in a few lines, with the avatar tracks, speaking state and live transcript handled for you.

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.

Server
npm i @zentiq/node
Browser
npm i @zentiq/client livekit-client
Backend — mint a session token
import { 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);
Browser — connect & talk
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)
Verify a webhook
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." } }
unauthorized401

Missing, invalid or revoked API key.

forbidden403

The API key is missing the scope this endpoint requires.

invalid_request400 / 413

A parameter is missing, malformed, or too large. The message says which.

unsupported_media_type415

A knowledge document upload isn't an accepted type (pdf, txt, md, docx, csv).

not_found404

The resource doesn't exist or belongs to another organization.

gone410

The id existed but the resource has been deleted (distinct from a never-existed 404).

session_ended409

say / steer targeted a session that has already ended or isn't live.

idempotency_conflict409

An Idempotency-Key was reused with a different request body.

insufficient_minutes402

The organization's monthly minute cap has been reached.

rate_limited429

Per-key request rate limit. Respect Retry-After and the X-RateLimit-* headers.

concurrency_limit429

Too many live sessions at once. End some (POST /sessions/end-idle) rather than just retrying.

capacity503

Every avatar slot is busy. Retry with exponential backoff.

upstream_error502

A realtime component didn't respond. Safe to retry.

server_error500 / 503

Something 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.

API reference

Who am I

Verify a key and see which organization and plan it belongs to.

GET/me
Identify the calling API key

The fastest smoke test that a key works. Returns the organization, its plan, and the key's metadata (never the secret).

Request
curl https://app.zentiqai.com/v1/me \
  -H "Authorization: Bearer $ZENTIQ_API_KEY"
Response
{
  "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.

GET/avatars
List avatars

Built-in stock avatars (organization-independent) plus every custom avatar your organization has created.

Request
curl https://app.zentiqai.com/v1/avatars \
  -H "Authorization: Bearer $ZENTIQ_API_KEY"
Response
{
  "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": "…" }
  ]
}
POST/avatars
Create a custom avatar

Bring 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.

Body parameters
namestring

Display name for the avatar. Defaults to “Custom avatar”.

imageBase64string

The portrait as a data:image/… base64 URI (max ~6 MB). Provide this or imageUrl.

imageUrlstring

An https URL to fetch the portrait from instead of inlining it.

Request
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" }'
Response
{
  "data": { "id": "av_…", "name": "Our CEO", "kind": "custom", "previewUrl": "https://app.zentiqai.com/v1/avatars/av_…/preview", "createdAt": "…" }
}
GET/avatars/:id
Retrieve an avatar
Request
curl https://app.zentiqai.com/v1/avatars/av_123 \
  -H "Authorization: Bearer $ZENTIQ_API_KEY"
Response
{ "data": { "id": "av_123", "name": "Our CEO", "kind": "custom", "previewUrl": "https://app.zentiqai.com/v1/avatars/av_123/preview", "createdAt": "…" } }
DELETE/avatars/:id
Delete a custom avatar

Built-in stock avatars can't be deleted. Personas already referencing the avatar keep working until re-published.

Request
curl -X DELETE https://app.zentiqai.com/v1/avatars/av_123 \
  -H "Authorization: Bearer $ZENTIQ_API_KEY"
Response
{ "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.

GET/languages
List supported languages

The 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.

Request
curl https://app.zentiqai.com/v1/languages \
  -H "Authorization: Bearer $ZENTIQ_API_KEY"
Response
{ "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" } ] } }
GET/voices
List voices
Request
curl https://app.zentiqai.com/v1/voices \
  -H "Authorization: Bearer $ZENTIQ_API_KEY"
Response
{
  "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": "…" }
  ]
}
POST/voices
Clone a voice

Send ~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.

Body parameters
namestring

Display 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".

Request
curl -X POST https://app.zentiqai.com/v1/voices \
  -H "Authorization: Bearer $ZENTIQ_API_KEY" \
  -F "name=Founder voice" \
  -F "sample=@founder.wav"
Response
{
  "data": { "id": "vo_…", "name": "Founder voice", "language": "en", "languages": ["en","fr","de","es","it","pt","ja","ko","ru","zh"], "kind": "custom", "createdAt": "…" }
}
GET/voices/:id
Retrieve a voice
Request
curl https://app.zentiqai.com/v1/voices/vo_123 \
  -H "Authorization: Bearer $ZENTIQ_API_KEY"
Response
{ "data": { "id": "vo_123", "name": "Founder voice", "language": "en", "languages": ["en","fr","de","es","it","pt","ja","ko","ru","zh"], "kind": "custom", "createdAt": "…" } }
DELETE/voices/:id
Delete a cloned voice
Request
curl -X DELETE https://app.zentiqai.com/v1/voices/vo_123 \
  -H "Authorization: Bearer $ZENTIQ_API_KEY"
Response
{ "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.

GET/personas
List personas
Request
curl https://app.zentiqai.com/v1/personas \
  -H "Authorization: Bearer $ZENTIQ_API_KEY"
Response
{
  "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": "…"
    }
  ]
}
POST/personas
Create a persona
Body parameters
namestringrequired

The persona's name — the avatar introduces itself with it.

systemPromptstringrequired

Who the persona is and how it behaves. This drives every reply.

greetingstring

The opening line spoken when a session starts.

avatarIdstring

A stock or custom avatar id from GET /avatars. Defaults to the built-in face.

voiceIdstring

A voice id from GET /voices.

settingsobject

Behaviour 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.

Request
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" }
  }'
Response
{ "data": { "id": "pa_…", "name": "Ava", "avatarId": "av_123", "voiceId": "vo_456", "settings": { "engine": "premier", "language": "en", "…": "…" }, "createdAt": "…" } }
GET/personas/:id
Retrieve a persona
Request
curl https://app.zentiqai.com/v1/personas/pa_123 \
  -H "Authorization: Bearer $ZENTIQ_API_KEY"
Response
{ "data": { "id": "pa_123", "name": "Ava", "systemPrompt": "…", "avatar": { … }, "voice": { … } } }
PATCH/personas/:id
Update a persona

Send 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.

Request
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" } }'
Response
{ "data": { "id": "pa_123", "settings": { "engine": "global", "globalVoice": "Aoede", "…": "…" }, "updatedAt": "…" } }
DELETE/personas/:id
Delete a persona

Soft delete — session history for the persona is preserved.

Request
curl -X DELETE https://app.zentiqai.com/v1/personas/pa_123 \
  -H "Authorization: Bearer $ZENTIQ_API_KEY"
Response
{ "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.

POST/sessions/tokens
Start a session

Provisions 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.

Body parameters
personaIdstringrequired

The persona to bring live.

identitystring

Your user's identity in the room. Defaults to a generated id.

languagestring

Override the persona's conversation language for this session, e.g. "German".

overridesobject

Personalize 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.

Request
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" }
    }
  }'
Response
{
  "sessionId": "sess_…",
  "sessionToken": "eyJhbGciOi…",
  "url": "wss://…",
  "room": "zentiq-…",
  "persona": { "id": "pa_123", "name": "Ava" }
}
GET/sessions
List sessions
Query parameters
limitinteger

1–200, default 50. Newest first.

statusstring

Filter: ACTIVE | COMPLETED | ABANDONED.

Request
curl "https://app.zentiqai.com/v1/sessions?status=COMPLETED&limit=20" \
  -H "Authorization: Bearer $ZENTIQ_API_KEY"
Response
{
  "data": [
    { "id": "sess_…", "personaId": "pa_…", "status": "COMPLETED", "source": "api", "minutesBilled": 4.27, "startedAt": "…", "endedAt": "…", "duration": 256 }
  ]
}
GET/sessions/:id
Retrieve a session
Request
curl https://app.zentiqai.com/v1/sessions/sess_123 \
  -H "Authorization: Bearer $ZENTIQ_API_KEY"
Response
{ "data": { "id": "sess_123", "status": "ACTIVE", "startedAt": "…", "minutesBilled": null } }
POST/sessions/:id/end
End a session

Releases 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.

Request
curl -X POST https://app.zentiqai.com/v1/sessions/sess_123/end \
  -H "Authorization: Bearer $ZENTIQ_API_KEY"
Response
{ "data": { "id": "sess_123", "status": "COMPLETED", "duration": 256, "minutesBilled": 4.27, "endedAt": "…" } }
POST/sessions/end-idle
Reclaim idle sessions

Force-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.

Request
curl -X POST https://app.zentiqai.com/v1/sessions/end-idle \
  -H "Authorization: Bearer $ZENTIQ_API_KEY"
Response
{ "data": { "reaped": 3 } }
GET/sessions/:id/transcript
Get the transcript

The 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.

Request
curl https://app.zentiqai.com/v1/sessions/sess_123/transcript \
  -H "Authorization: Bearer $ZENTIQ_API_KEY"
Response
{
  "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 }
    ]
  }
}
PATCH/sessions/:id
Steer a live session

Adjust 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.

Body parameters
systemPromptstring

New instructions the avatar follows on subsequent turns.

languagestring

Switch the conversation language, e.g. "German".

variablesobject

Arbitrary values your prompt can reference.

Request
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." }'
Response
{ "data": { "id": "sess_123", "status": "ACTIVE" } }
POST/sessions/:id/say
Make the avatar speak

Have 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.

Body parameters
textstringrequired

What the avatar should say (max 2000 characters).

interruptboolean

Cut off any in-progress speech first. Default true.

languagestring

Speak this line in a specific language.

Request
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." }'
Response
{ "data": { "ok": true } }

Usage

Metered live minutes with plan context — the same numbers your dashboard billing page shows.

GET/usage
Usage for the current period
Query parameters
fromISO date

Custom range start. Defaults to the first of the current month (UTC).

toISO date

Custom range end (exclusive). Defaults to the first of next month.

Request
curl https://app.zentiqai.com/v1/usage \
  -H "Authorization: Bearer $ZENTIQ_API_KEY"
Response
{
  "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.

POST/webhooks
Create a webhook

Subscribe an https endpoint. `events` is a subset of the known types or ["*"] for all. Returns the signing `secret` ONCE — store it.

Body parameters
urlstringrequired

Your https endpoint.

eventsstring[]

session.started · session.ended · transcript.final · voice.clone.completed · usage.threshold — or ["*"].

Request
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"] }'
Response
{ "data": { "id": "wh_…", "url": "https://example.com/zentiq", "events": ["session.ended","transcript.final"], "active": true, "secret": "whsec_…", "createdAt": "…" } }
GET/webhooks
List webhooks
Request
curl https://app.zentiqai.com/v1/webhooks \
  -H "Authorization: Bearer $ZENTIQ_API_KEY"
Response
{ "data": [ { "id": "wh_…", "url": "…", "events": ["*"], "active": true, "createdAt": "…" } ] }
DELETE/webhooks/:id
Delete a webhook
Request
curl -X DELETE https://app.zentiqai.com/v1/webhooks/wh_123 \
  -H "Authorization: Bearer $ZENTIQ_API_KEY"
Response
{ "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.

POST/keys
Create a key

Optionally scope it. Rotation = create a new key, then DELETE the old one. Returns the full `key` ONCE.

Body parameters
namestring

Label for the key.

scopesstring[]

e.g. ["sessions:write", "personas:read"]. Omit or [] for full access.

Request
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"] }'
Response
{ "data": { "id": "key_…", "name": "server", "prefix": "zq_live_…", "scopes": ["sessions:write","personas:read"], "key": "zq_live_…", "createdAt": "…" } }
GET/keys
List keys
Request
curl https://app.zentiqai.com/v1/keys \
  -H "Authorization: Bearer $ZENTIQ_API_KEY"
Response
{ "data": [ { "id": "key_…", "name": "…", "prefix": "zq_live_…", "scopes": [], "lastUsedAt": "…", "createdAt": "…" } ] }
DELETE/keys/:id
Revoke a key
Request
curl -X DELETE https://app.zentiqai.com/v1/keys/key_123 \
  -H "Authorization: Bearer $ZENTIQ_API_KEY"
Response
{ "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.

POST/knowledge/bases
Create a knowledge base
Body parameters
namestringrequired

e.g. "Product Docs".

Request
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" }'
Response
{ "data": { "id": "kb_…", "name": "Product Docs", "documentCount": 0, "createdAt": "…", "updatedAt": "…" } }
GET/knowledge/bases
List knowledge bases
Query parameters
limitnumber

1–200 (default 50).

cursorstring

nextCursor from a previous page.

Request
curl https://app.zentiqai.com/v1/knowledge/bases \
  -H "Authorization: Bearer $ZENTIQ_API_KEY"
Response
{ "data": [ { "id": "kb_…", "name": "Product Docs", "documentCount": 12, "createdAt": "…" } ], "nextCursor": null }
POST/knowledge/bases/:id/documents
Add a document (file / url / text)

Three 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.

Request
curl -X POST https://app.zentiqai.com/v1/knowledge/bases/kb_123/documents \
  -H "Authorization: Bearer $ZENTIQ_API_KEY" \
  -F "file=@handbook.pdf"
Response
{ "data": { "id": "doc_…", "knowledgeBaseId": "kb_123", "name": "handbook.pdf", "type": "pdf", "status": "processing", "chunkCount": null, "createdAt": "…" } }
GET/knowledge/documents/:id
Poll a document's status
Request
curl https://app.zentiqai.com/v1/knowledge/documents/doc_123 \
  -H "Authorization: Bearer $ZENTIQ_API_KEY"
Response
{ "data": { "id": "doc_123", "status": "ready", "chunkCount": 42, "type": "pdf", "bytes": 248210 } }
GET/knowledge/documents/:id/download
Signed URL for the original file

Short-TTL (10 min) signed URL — never a raw storage path.

Request
curl https://app.zentiqai.com/v1/knowledge/documents/doc_123/download \
  -H "Authorization: Bearer $ZENTIQ_API_KEY"
Response
{ "data": { "url": "https://…", "expiresAt": "…" } }
DELETE/knowledge/documents/:id
Delete a document
Request
curl -X DELETE https://app.zentiqai.com/v1/knowledge/documents/doc_123 \
  -H "Authorization: Bearer $ZENTIQ_API_KEY"
Response
{ "data": { "deleted": true, "id": "doc_123" } }
POST/knowledge/bases/:id/search
Semantic search (no session needed)
Body parameters
querystringrequired

≤ 4 KB.

topKnumber

1–50 (default 5).

minScorenumber

0–1 similarity floor (default 0).

Request
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 }'
Response
{ "data": { "results": [ { "documentId": "doc_…", "text": "…retrieved chunk…", "score": 0.83, "metadata": { "page": 4 } } ] } }
PATCH/personas/:id
Attach bases to a persona

Set `knowledge.baseIds` on a persona (see Personas). Every session with that persona is then grounded automatically. `null` clears it.

Body parameters
knowledgeobject

{ baseIds: ["kb_…"], topK?, minScore?, maxChars? }

Request
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 } }'
Response
{ "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