Portals
Portals built from templates, and their doors.
/v1/portalsList portals
The organisation's portals, oldest first, a page at a time. Published ones unless status says otherwise; archived ones are never listed, and neither are the hidden portals behind personas' website widgets.
Query parameters
limitintegerPage size, 1-200 (default 50).
cursorstringThe
nextCursorof the previous page.status"published" | "draft" | "all"published(the default),draft, orall(both). Archived portals are never listed.unitIdstringOnly the portals of this team or class (
unit_…).
Returns
200 A page of portals. A page: data is a list of Portal, and nextCursor fetches the next page.
Fields of data
idstringrequiredPortal id (
portal_…).namestringrequiredslugstringrequiredstatus"draft" | "published" | "archived"requireddraftuntil published; people only reach a published portal.group"learn" | "train" | "serve" | "assess" | "mixed" | nullrequiredIts subjects' group,
mixedwhen they differ; null without subjects.welcomestring | nullrequiredaddressstringrequiredWhere people open it: the sign-in page, or the verified custom domain.
addressesobjectrequiredmembersstringrequiredThe sign-in page (
/d/<org>/<portal>).visitorsstring | nullrequiredThe visitor page (
/v/<org>/<portal>) when a subject opens to visitors; null otherwise.embedstringrequiredWhat an iframe mounts, with
?t=<embed token>.
loginMethods("email" | "login_card" | "class_code" | "visitor")[]requiredunitIdstring | nullrequiredThe team or class it is for; null = the whole organisation.
personaIdstring | nullrequiredWho speaks on it.
brandobjectrequiredprimarystring | nullrequired#rrggbblogoUrlstring | nullrequiredmode"light" | "dark" | "system" | nullrequired
domainobject | nullrequiredThe organisation's own address for it, when one is set.
namestringrequiredverifiedbooleanrequiredverifiedAtstring (date-time) | nullrequired
createdAtstring (date-time)requiredISO 8601 UTC timestamp.
updatedAtstring (date-time)requiredISO 8601 UTC timestamp.
publishRefusedstringOnly when a publish was asked for and the go-live check refused it (
POST /portalswithpublish: true;POST /portals/{id}/publishfor keys on 2026-07-08): what to finish first. The portal stays a draft.
Errors
invalid_requestunauthorizedforbiddenupgrade_requiredrate_limitedserver_error
Request
curl "https://app.zentiqai.com/v1/portals?limit=20" \
-H "Authorization: Bearer $ZENTIQ_API_KEY"import { Zentiq } from "@zentiq/node";
const zentiq = new Zentiq({ apiKey: process.env.ZENTIQ_API_KEY });
const page = await zentiq.listPortals({
limit: 20,
});import os
from zentiq import Zentiq
zentiq = Zentiq(api_key=os.environ["ZENTIQ_API_KEY"])
page = zentiq.list_portals(limit=20)Example answer
{
"data": [
{
"id": "portal_cmfq2b7x10000abcd1234",
"name": "Zoe",
"slug": "…",
"status": "draft",
"group": "learn",
"welcome": "…"
}
],
"error": null,
"requestId": "req_4f1c2e9a7b3d5e6f7a8b9c0d",
"nextCursor": null
}/v1/portalsBuild a portal from templates
Builds a portal with one subject per template (GET /portals/templates), and its own persona unless personaId is given. It starts as a draft: nobody meets it half set up. With publish: true (needs portals:publish) it is published once it passes the go-live check, together with its subjects; otherwise it stays a draft and publishRefused says what to finish first. Each template's family must be on the plan and one more portal must fit it (403 upgrade_required). A duo persona speaks only once it is live (409 duo_not_live). Website and screening portals open to visitors (their door opens once published) for a key holding portals:publish; without it they stay closed to visitors, and visitor in loginMethods is 403, as on update. Speaking with an existing persona (personaId) needs personas:write as well: that persona speaks elsewhere too (403).
Body
namestringrequired1-80 characters
templatesstring[]Template keys from
GET /portals/templates; one subject each.at most 12 items
templatestringdeprecatedOne template (the 2026-07-08 form of
templates).1-60 characters
personaIdstringA persona of yours to speak on every subject; left out = the portal gets its own.
unitIdstring | nullThe team or class it is for; left out or null = the whole organisation.
welcomestring | nullloginMethods("email" | "login_card" | "class_code" | "visitor")[]publishbooleanPublish it once it passes the go-live check (needs
portals:publish); otherwise it stays a draft andpublishRefusedsays why.
Returns
201 The portal (a draft unless it was published). data is Portal.
Fields of data
idstringrequiredPortal id (
portal_…).namestringrequiredslugstringrequiredstatus"draft" | "published" | "archived"requireddraftuntil published; people only reach a published portal.group"learn" | "train" | "serve" | "assess" | "mixed" | nullrequiredIts subjects' group,
mixedwhen they differ; null without subjects.welcomestring | nullrequiredaddressstringrequiredWhere people open it: the sign-in page, or the verified custom domain.
addressesobjectrequiredmembersstringrequiredThe sign-in page (
/d/<org>/<portal>).visitorsstring | nullrequiredThe visitor page (
/v/<org>/<portal>) when a subject opens to visitors; null otherwise.embedstringrequiredWhat an iframe mounts, with
?t=<embed token>.
loginMethods("email" | "login_card" | "class_code" | "visitor")[]requiredunitIdstring | nullrequiredThe team or class it is for; null = the whole organisation.
personaIdstring | nullrequiredWho speaks on it.
brandobjectrequiredprimarystring | nullrequired#rrggbblogoUrlstring | nullrequiredmode"light" | "dark" | "system" | nullrequired
domainobject | nullrequiredThe organisation's own address for it, when one is set.
namestringrequiredverifiedbooleanrequiredverifiedAtstring (date-time) | nullrequired
createdAtstring (date-time)requiredISO 8601 UTC timestamp.
updatedAtstring (date-time)requiredISO 8601 UTC timestamp.
publishRefusedstringOnly when a publish was asked for and the go-live check refused it (
POST /portalswithpublish: true;POST /portals/{id}/publishfor keys on 2026-07-08): what to finish first. The portal stays a draft.
Errors
invalid_requestunauthorizedforbiddenupgrade_requirednot_foundconflictduo_not_liveidempotency_conflictidempotency_in_progressrate_limitedserver_error
Events
portal.createdworkspace.createdportal.updatedworkspace.published
Request
curl -X POST "https://app.zentiqai.com/v1/portals" \
-H "Authorization: Bearer $ZENTIQ_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"name": "Zoe"
}'import { Zentiq } from "@zentiq/node";
const zentiq = new Zentiq({ apiKey: process.env.ZENTIQ_API_KEY });
const result = await zentiq.createPortal({
name: "Zoe",
});import os
from zentiq import Zentiq
zentiq = Zentiq(api_key=os.environ["ZENTIQ_API_KEY"])
result = zentiq.create_portal({
"name": "Zoe",
})Example answer
{
"data": {
"id": "portal_cmfq2b7x10000abcd1234",
"name": "Zoe",
"slug": "…",
"status": "draft",
"group": "learn",
"welcome": "…"
},
"error": null,
"requestId": "req_4f1c2e9a7b3d5e6f7a8b9c0d"
}/v1/portals/templatesPortal templates (the gallery)
The templates POST /portals builds subjects from, with what each needs from the plan and whether the organisation's plan includes it. Templates written for two people (duos) are listed where the duo builder is open for the organisation.
Returns
200 The templates. data is a list of PortalTemplate.
Fields of data
keystringrequiredWhat
POST /portalstakes intemplates.group"Learn" | "Train" | "Serve" | "Assess"requiredlabelstringrequireddescriptionstringrequiredavailablebooleanrequiredIt can be built today.
kind"tutor" | "roleplay" | "assessor" | "service"requiredWhat the avatar does:
tutorteaches,roleplayplays someone to practise with,assessorasks and scores,servicetalks to website visitors.duobooleanrequiredWritten for two people in one conversation (needs a duo).
requiresstring[]requiredThe plan features it needs (its portal family, candidate screening, ready-made duos).
includedInPlanbooleanrequiredThe organisation's plan has every feature it needs.
Errors
invalid_requestunauthorizedforbiddenupgrade_requiredrate_limitedserver_error
Request
curl "https://app.zentiqai.com/v1/portals/templates" \
-H "Authorization: Bearer $ZENTIQ_API_KEY"import { Zentiq } from "@zentiq/node";
const zentiq = new Zentiq({ apiKey: process.env.ZENTIQ_API_KEY });
const result = await zentiq.listPortalTemplates();import os
from zentiq import Zentiq
zentiq = Zentiq(api_key=os.environ["ZENTIQ_API_KEY"])
result = zentiq.list_portal_templates()Example answer
{
"data": [
{
"key": "sales-trainer",
"group": "Learn",
"label": "…",
"description": "…",
"available": true,
"kind": "tutor"
}
],
"error": null,
"requestId": "req_4f1c2e9a7b3d5e6f7a8b9c0d"
}/v1/portals/{id}Get a portal
One portal, draft or published. An archived one is 410 gone.
Path parameters
idstringrequiredPortal id (
portal_…); the id without its prefix is accepted too.
Returns
200 The portal. data is Portal.
Fields of data
idstringrequiredPortal id (
portal_…).namestringrequiredslugstringrequiredstatus"draft" | "published" | "archived"requireddraftuntil published; people only reach a published portal.group"learn" | "train" | "serve" | "assess" | "mixed" | nullrequiredIts subjects' group,
mixedwhen they differ; null without subjects.welcomestring | nullrequiredaddressstringrequiredWhere people open it: the sign-in page, or the verified custom domain.
addressesobjectrequiredmembersstringrequiredThe sign-in page (
/d/<org>/<portal>).visitorsstring | nullrequiredThe visitor page (
/v/<org>/<portal>) when a subject opens to visitors; null otherwise.embedstringrequiredWhat an iframe mounts, with
?t=<embed token>.
loginMethods("email" | "login_card" | "class_code" | "visitor")[]requiredunitIdstring | nullrequiredThe team or class it is for; null = the whole organisation.
personaIdstring | nullrequiredWho speaks on it.
brandobjectrequiredprimarystring | nullrequired#rrggbblogoUrlstring | nullrequiredmode"light" | "dark" | "system" | nullrequired
domainobject | nullrequiredThe organisation's own address for it, when one is set.
namestringrequiredverifiedbooleanrequiredverifiedAtstring (date-time) | nullrequired
createdAtstring (date-time)requiredISO 8601 UTC timestamp.
updatedAtstring (date-time)requiredISO 8601 UTC timestamp.
publishRefusedstringOnly when a publish was asked for and the go-live check refused it (
POST /portalswithpublish: true;POST /portals/{id}/publishfor keys on 2026-07-08): what to finish first. The portal stays a draft.
Errors
invalid_requestunauthorizedforbiddenupgrade_requirednot_foundgonerate_limitedserver_error
Request
curl "https://app.zentiqai.com/v1/portals/ID" \
-H "Authorization: Bearer $ZENTIQ_API_KEY"import { Zentiq } from "@zentiq/node";
const zentiq = new Zentiq({ apiKey: process.env.ZENTIQ_API_KEY });
const result = await zentiq.getPortal("ID");import os
from zentiq import Zentiq
zentiq = Zentiq(api_key=os.environ["ZENTIQ_API_KEY"])
result = zentiq.get_portal("ID")Example answer
{
"data": {
"id": "portal_cmfq2b7x10000abcd1234",
"name": "Zoe",
"slug": "…",
"status": "draft",
"group": "learn",
"welcome": "…"
},
"error": null,
"requestId": "req_4f1c2e9a7b3d5e6f7a8b9c0d"
}/v1/portals/{id}Update a portal
Changes its name, the sign-in page's welcome line, how people sign in, its team, who speaks on it or its brand: the changes the console's Settings make, with its rules. Adding visitor to loginMethods opens it to visitors (needs portals:publish; on a published portal, the go-live check runs first: 422 go_live_refused). Publishing is POST /portals/{id}/publish. Pointing it at a persona that isn't its own (personaId: a standalone persona, a website widget's or another portal's) needs personas:write as well (403). An archived portal is 410 gone; a website widget's portal is 409 conflict (change it with PUT /personas/{id}/widget).
Path parameters
idstringrequiredPortal id (
portal_…); the id without its prefix is accepted too.
Body
namestring1-80 characters
welcomestring | nullloginMethods("email" | "login_card" | "class_code" | "visitor")[]Adding
visitoropens it to visitors: needsportals:publish, and runs the go-live check on a published portal.unitIdstring | nullpersonaIdstringOne of your personas to speak on it instead of its own.
brandobjectprimarystring | null#rrggbbor a colour name (green); null goes back to the organisation's colour.mode"light" | "dark" | "system" | nullhideZentiqBadgeboolean | nullHonoured when the plan includes removing the badge.
Returns
200 The portal as it now stands. data is Portal.
Fields of data
idstringrequiredPortal id (
portal_…).namestringrequiredslugstringrequiredstatus"draft" | "published" | "archived"requireddraftuntil published; people only reach a published portal.group"learn" | "train" | "serve" | "assess" | "mixed" | nullrequiredIts subjects' group,
mixedwhen they differ; null without subjects.welcomestring | nullrequiredaddressstringrequiredWhere people open it: the sign-in page, or the verified custom domain.
addressesobjectrequiredmembersstringrequiredThe sign-in page (
/d/<org>/<portal>).visitorsstring | nullrequiredThe visitor page (
/v/<org>/<portal>) when a subject opens to visitors; null otherwise.embedstringrequiredWhat an iframe mounts, with
?t=<embed token>.
loginMethods("email" | "login_card" | "class_code" | "visitor")[]requiredunitIdstring | nullrequiredThe team or class it is for; null = the whole organisation.
personaIdstring | nullrequiredWho speaks on it.
brandobjectrequiredprimarystring | nullrequired#rrggbblogoUrlstring | nullrequiredmode"light" | "dark" | "system" | nullrequired
domainobject | nullrequiredThe organisation's own address for it, when one is set.
namestringrequiredverifiedbooleanrequiredverifiedAtstring (date-time) | nullrequired
createdAtstring (date-time)requiredISO 8601 UTC timestamp.
updatedAtstring (date-time)requiredISO 8601 UTC timestamp.
publishRefusedstringOnly when a publish was asked for and the go-live check refused it (
POST /portalswithpublish: true;POST /portals/{id}/publishfor keys on 2026-07-08): what to finish first. The portal stays a draft.
Errors
invalid_requestunauthorizedforbiddenupgrade_requirednot_foundconflictduo_not_livegonego_live_refusedrate_limitedserver_error
Events
Request
curl -X PATCH "https://app.zentiqai.com/v1/portals/ID" \
-H "Authorization: Bearer $ZENTIQ_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'import { Zentiq } from "@zentiq/node";
const zentiq = new Zentiq({ apiKey: process.env.ZENTIQ_API_KEY });
const result = await zentiq.updatePortal("ID", {});import os
from zentiq import Zentiq
zentiq = Zentiq(api_key=os.environ["ZENTIQ_API_KEY"])
result = zentiq.update_portal("ID", {})Example answer
{
"data": {
"id": "portal_cmfq2b7x10000abcd1234",
"name": "Zoe",
"slug": "…",
"status": "draft",
"group": "learn",
"welcome": "…"
},
"error": null,
"requestId": "req_4f1c2e9a7b3d5e6f7a8b9c0d"
}/v1/portals/{id}/personaWho speaks on a portal
An archived portal is 410 gone.
Path parameters
idstringrequiredPortal id (
portal_…); the id without its prefix is accepted too.
Returns
200 Who speaks. data is PortalPersona.
Fields of data
idstringrequiredPersona id (
pa_…).pattern `^pa_.*`
namestringrequiredfacestring | nullrequiredThe face's name.
voicestring | nullrequiredThe voice's name.
avatarIdstring | nullrequiredvoiceIdstring | nullrequiredaccentstring | nullrequiredgreetingstring | nullrequiredcharacterstringrequiredlanguagestringrequiredThe language it speaks (a name from
GET /languages): English unless set.readybooleanrequiredIt has a face and a voice: people can talk to it.
Errors
invalid_requestunauthorizedforbiddenupgrade_requirednot_foundgonerate_limitedserver_error
Request
curl "https://app.zentiqai.com/v1/portals/ID/persona" \
-H "Authorization: Bearer $ZENTIQ_API_KEY"import { Zentiq } from "@zentiq/node";
const zentiq = new Zentiq({ apiKey: process.env.ZENTIQ_API_KEY });
const result = await zentiq.getPortalPersona("ID");import os
from zentiq import Zentiq
zentiq = Zentiq(api_key=os.environ["ZENTIQ_API_KEY"])
result = zentiq.get_portal_persona("ID")Example answer
{
"data": {
"id": "pa_cmfq2b7x10000abcd1234",
"name": "Zoe",
"face": "…",
"voice": "…",
"avatarId": "av_cmfq2b7x10000abcd1234",
"voiceId": "vo_cmfq2b7x10000abcd1234"
},
"error": null,
"requestId": "req_4f1c2e9a7b3d5e6f7a8b9c0d"
}/v1/portals/{id}/personaChange who speaks on a portal
The console's Persona tab over the API. The face and voice by id (avatarId from GET /avatars, voiceId from GET /voices); face and voice by name still work and are deprecated. accent in plain words (British, Australian). character and greeting are set word for word within the console's limits (character 4,000, greeting 300, name 80 characters; longer is 413). A duo keeps its two people (409 conflict). With portals:write this changes the portal's own persona only; one it borrows (a standalone persona, a website widget's or another portal's) changes wherever else it speaks, so it needs personas:write as well (403). An archived portal is 410 gone.
Path parameters
idstringrequiredPortal id (
portal_…); the id without its prefix is accepted too.
Body
avatarIdstringA face (
av_…) fromGET /avatars.voiceIdstringA voice (
vo_…) fromGET /voices.facestringdeprecatedA library face by first name ("Marcus"). Prefer
avatarId.at most 40 characters
voicestringdeprecatedA studio voice by name ("James"). Prefer
voiceId.at most 40 characters
accentstringIn plain words ("British").
at most 60 characters
characterstringWho they are and how they behave, word for word (up to 4,000 characters; longer is 413).
greetingstringTheir first line (up to 300 characters).
namestringUp to 80 characters.
languagestringThe language it speaks: a name or code from
GET /languages("German", "es"); one it doesn't know is400. A duo speaks one of its ten.at most 80 characters
Returns
200 Who speaks now. data is PortalPersona.
Fields of data
idstringrequiredPersona id (
pa_…).pattern `^pa_.*`
namestringrequiredfacestring | nullrequiredThe face's name.
voicestring | nullrequiredThe voice's name.
avatarIdstring | nullrequiredvoiceIdstring | nullrequiredaccentstring | nullrequiredgreetingstring | nullrequiredcharacterstringrequiredlanguagestringrequiredThe language it speaks (a name from
GET /languages): English unless set.readybooleanrequiredIt has a face and a voice: people can talk to it.
Errors
invalid_requestunauthorizedforbiddenupgrade_requirednot_foundconflictgonetoo_largerate_limitedserver_error
Events
Request
curl -X PUT "https://app.zentiqai.com/v1/portals/ID/persona" \
-H "Authorization: Bearer $ZENTIQ_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'import { Zentiq } from "@zentiq/node";
const zentiq = new Zentiq({ apiKey: process.env.ZENTIQ_API_KEY });
const result = await zentiq.updatePortalPersona("ID", {});import os
from zentiq import Zentiq
zentiq = Zentiq(api_key=os.environ["ZENTIQ_API_KEY"])
result = zentiq.update_portal_persona("ID", {})Example answer
{
"data": {
"id": "pa_cmfq2b7x10000abcd1234",
"name": "Zoe",
"face": "…",
"voice": "…",
"avatarId": "av_cmfq2b7x10000abcd1234",
"voiceId": "vo_cmfq2b7x10000abcd1234"
},
"error": null,
"requestId": "req_4f1c2e9a7b3d5e6f7a8b9c0d"
}/v1/portals/{id}/publishPublish a portal
Opens a draft (or brings back an archived) portal at its address, once it passes the go-live check (every subject set up with the organisation's own details, never a template's example content); its draft subjects are published with it. A refusal is 422 go_live_refused with details.problems for keys on 2026-10-01; keys on 2026-07-08 get the portal back unchanged (200) with publishRefused. Bringing back an archived portal must fit the plan's portal limit (403 upgrade_required). Already published: returned unchanged.
Path parameters
idstringrequiredPortal id (
portal_…); the id without its prefix is accepted too.
Returns
200 The portal. data is Portal.
Fields of data
idstringrequiredPortal id (
portal_…).namestringrequiredslugstringrequiredstatus"draft" | "published" | "archived"requireddraftuntil published; people only reach a published portal.group"learn" | "train" | "serve" | "assess" | "mixed" | nullrequiredIts subjects' group,
mixedwhen they differ; null without subjects.welcomestring | nullrequiredaddressstringrequiredWhere people open it: the sign-in page, or the verified custom domain.
addressesobjectrequiredmembersstringrequiredThe sign-in page (
/d/<org>/<portal>).visitorsstring | nullrequiredThe visitor page (
/v/<org>/<portal>) when a subject opens to visitors; null otherwise.embedstringrequiredWhat an iframe mounts, with
?t=<embed token>.
loginMethods("email" | "login_card" | "class_code" | "visitor")[]requiredunitIdstring | nullrequiredThe team or class it is for; null = the whole organisation.
personaIdstring | nullrequiredWho speaks on it.
brandobjectrequiredprimarystring | nullrequired#rrggbblogoUrlstring | nullrequiredmode"light" | "dark" | "system" | nullrequired
domainobject | nullrequiredThe organisation's own address for it, when one is set.
namestringrequiredverifiedbooleanrequiredverifiedAtstring (date-time) | nullrequired
createdAtstring (date-time)requiredISO 8601 UTC timestamp.
updatedAtstring (date-time)requiredISO 8601 UTC timestamp.
publishRefusedstringOnly when a publish was asked for and the go-live check refused it (
POST /portalswithpublish: true;POST /portals/{id}/publishfor keys on 2026-07-08): what to finish first. The portal stays a draft.
Errors
invalid_requestunauthorizedforbiddenupgrade_requirednot_foundconflictidempotency_conflictidempotency_in_progressgo_live_refusedrate_limitedserver_error
Events
portal.updatedworkspace.published
Request
curl -X POST "https://app.zentiqai.com/v1/portals/ID/publish" \
-H "Authorization: Bearer $ZENTIQ_API_KEY" \
-H "Idempotency-Key: $(uuidgen)"import { Zentiq } from "@zentiq/node";
const zentiq = new Zentiq({ apiKey: process.env.ZENTIQ_API_KEY });
const result = await zentiq.publishPortal("ID");import os
from zentiq import Zentiq
zentiq = Zentiq(api_key=os.environ["ZENTIQ_API_KEY"])
result = zentiq.publish_portal("ID")Example answer
{
"data": {
"id": "portal_cmfq2b7x10000abcd1234",
"name": "Zoe",
"slug": "…",
"status": "draft",
"group": "learn",
"welcome": "…"
},
"error": null,
"requestId": "req_4f1c2e9a7b3d5e6f7a8b9c0d"
}/v1/portals/{id}/embed-tokensSign one of your users in (an embed token)
Called by your backend after your own login, never from a browser. Returns a five-minute, single-use token for a published portal: embedUrl goes in an iframe (zentiq-embed.js or @zentiq/react's <ZentiqPortal>), url opens the portal as that user. The person is the one POST /people knows by the same externalId. Never someone who runs your Zentiq account. A leader beyond the plan's staff seats is 403 upgrade_required; a learner under the consent age waits for a guardian's consent unless consent: { by: "organisation" } attests it (with people:write as well). A retry makes a new token (nothing is replayed).
Path parameters
idstringrequiredPortal id (
portal_…); the id without its prefix is accepted too.
Body
externalUserIdstringrequiredYour own id for this person: the same person as
POST /peoplewith thisexternalId.1-120 characters
namestringat most 80 characters
emailstringat most 200 characters
role"learner" | "leader" | "observer"Default
learner. A leader takes a staff seat.unitIdstringTheir team or class (
unit_…).unitNamestringOr a group by name, made under the portal's team on first sight.
1-80 characters
gradeBandstringat most 8 characters
consentobject{ by: "organisation" }: your organisation attests the consent a young learner needs (recorded as yours; needspeople:writeas well). Without it, a learner under the consent age waits for a guardian's consent.by"organisation"required
Returns
201 The token. data is EmbedToken.
Fields of data
tokenstringrequiredFive minutes, single use.
expiresAtstring (date-time)requiredISO 8601 UTC timestamp.
urlstringrequiredOpens the portal as that person (a full page).
embedUrlstringrequiredWhat an iframe mounts (
zentiq-embed.js,@zentiq/react's<ZentiqPortal>).
Errors
invalid_requestunauthorizedforbiddenupgrade_requirednot_foundconflictidempotency_conflictidempotency_in_progressrate_limitedserver_error
Request
curl -X POST "https://app.zentiqai.com/v1/portals/ID/embed-tokens" \
-H "Authorization: Bearer $ZENTIQ_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"externalUserId": "…"
}'import { Zentiq } from "@zentiq/node";
const zentiq = new Zentiq({ apiKey: process.env.ZENTIQ_API_KEY });
const result = await zentiq.createEmbedToken("ID", {
externalUserId: "…",
});import os
from zentiq import Zentiq
zentiq = Zentiq(api_key=os.environ["ZENTIQ_API_KEY"])
result = zentiq.create_embed_token("ID", {
"externalUserId": "…",
})Example answer
{
"data": {
"token": "…",
"expiresAt": "2026-09-25T12:00:00.000Z",
"url": "https://www.example.com",
"embedUrl": "https://www.example.com"
},
"error": null,
"requestId": "req_4f1c2e9a7b3d5e6f7a8b9c0d"
}/v1/portals/{id}/go-liveCheck a portal can go live
Runs the go-live check a publish runs, and says what each subject still needs (its content's field, in plain words). Changes nothing.
Path parameters
idstringrequiredPortal id (
portal_…); the id without its prefix is accepted too.
Returns
200 Whether it can go live, and what to finish first. data is PortalGoLive.
Fields of data
readybooleanrequiredPublishing would go through.
Worth fixing; never a refusal.
Errors
invalid_requestunauthorizedforbiddenupgrade_requirednot_foundrate_limitedserver_error
Request
curl "https://app.zentiqai.com/v1/portals/ID/go-live" \
-H "Authorization: Bearer $ZENTIQ_API_KEY"import { Zentiq } from "@zentiq/node";
const zentiq = new Zentiq({ apiKey: process.env.ZENTIQ_API_KEY });
const result = await zentiq.getPortalGoLive("ID");import os
from zentiq import Zentiq
zentiq = Zentiq(api_key=os.environ["ZENTIQ_API_KEY"])
result = zentiq.get_portal_go_live("ID")Example answer
{
"data": {
"ready": true,
"problems": [
{
"field": "hours",
"message": "…",
"short": "…",
"subjectId": "sub_cmfq2b7x10000abcd1234",
"subjectName": "…"
}
],
"warnings": [
{
"field": "hours",
"message": "…",
"short": "…",
"subjectId": "sub_cmfq2b7x10000abcd1234",
"subjectName": "…"
}
]
},
"error": null,
"requestId": "req_4f1c2e9a7b3d5e6f7a8b9c0d"
}/v1/portals/{id}/customer-tokensMint an identity token for a signed-in customer (Actions)
Called by your backend, where your customer is signed in, never from a browser. Returns a short-lived token naming that customer, for this portal only: give it to the website widget (data-customer-token, or ZentiqWidget.identify(token)) or to a conversation's start (customerToken), and the avatar's actions for signed-in customers run with that customer's id, never an id the visitor said. 10 minutes unless you ask; never more than 15 (a longer ttlSeconds is cut to 15: expiresAt says when). Mint one per page view. A token works for one conversation only: the first conversation that uses it keeps it, and any other refuses it even while it lives. A portal of another organisation is 404; an archived one 410. Each mint is recorded for your organisation with the portal, the key and how long the token lives, never who it names. A key that lives on your website's servers needs only this scope. Verified customer: contact.email is the address on record where Zentiq sends a one-time code before a sensitive action (when your organisation switches it on); it is signed into the token and never shown to the avatar or kept in a transcript or log. For a visitor who isn't signed in, an action's answer can name it instead, in the reserved field zentiq_verify: { email }.
Path parameters
idstringrequiredPortal id (
portal_…); the id without its prefix is accepted too.
Body
customerIdstringrequiredYour own id for the signed-in customer: sent to your system as customer.id on every call.
at most 128 characters
namestring | nullShown to them as "Signed in as …".
at most 80 characters
emailstring | nullauthTimenumber | nullWhen they signed in, seconds since 1970 (within the last 30 days).
ttlSecondsnumber | nullHow long the token lasts, in seconds. At most 900: a longer one is cut to 900 (expiresAt says when).
at least 30 · default 600
contactobject | nullVerified customer: the contact on record, where Zentiq sends a one-time code before a sensitive action (when the organisation switches it on). Signed into the token; never shown to the avatar, the conversation or a log, only as a masked read-back ("s***@example.com"). Use the address you would send a password reset to.
emailstring | nullThe customer's email address on record: where a one-time code is sent.
Returns
201 The token. data is CustomerToken.
Fields of data
tokenstringrequiredzct1.…: for this portal and one conversation only. Give it to the website widget or a conversation's start; never put it in a URL.
expiresAtstring (date-time)requiredISO 8601 UTC timestamp.
Errors
invalid_requestunauthorizedforbiddenupgrade_requirednot_foundgonerate_limitedserver_error
Request
curl -X POST "https://app.zentiqai.com/v1/portals/ID/customer-tokens" \
-H "Authorization: Bearer $ZENTIQ_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"customerId": "…"
}'import { Zentiq } from "@zentiq/node";
const zentiq = new Zentiq({ apiKey: process.env.ZENTIQ_API_KEY });
const result = await zentiq.createCustomerToken("ID", {
customerId: "…",
});import os
from zentiq import Zentiq
zentiq = Zentiq(api_key=os.environ["ZENTIQ_API_KEY"])
result = zentiq.create_customer_token("ID", {
"customerId": "…",
})Example answer
{
"data": {
"token": "…",
"expiresAt": "2026-09-25T12:00:00.000Z"
},
"error": null,
"requestId": "req_4f1c2e9a7b3d5e6f7a8b9c0d"
}/v1/portals/{id}/escalationsHand a chat to the video avatar
Called by your chat tool's server (Intercom, Zendesk or your own), never from a browser. Returns a link to show the customer: it opens the portal's website avatar, which starts knowing the chat so far (as information, never as instructions) and opens with a one-line recap. The link works for one conversation, for 30 minutes. When that conversation ends, escalation.completed goes to your webhooks with the outcome (the summary only with includeConversation), and the visitor is offered returnUrl. Needs the switch serve.chat_escalation on and the portal's public page open (409 conflict, details.reason: switched_off, door_closed or no_website_subject). Chat handover is on Starter and up: on a plan without it, 403 upgrade_required with details.reason plan_required and details.plan. A portal of another organisation is 404; an archived one 410. A retry makes a new link (nothing is replayed).
Path parameters
idstringrequiredPortal id (
portal_…); the id without its prefix is accepted too.
Body
The chat so far, oldest first: at most 50 turns and 20,000 characters in all (send the newest). The avatar reads it as information, never as instructions.
at most 50 items
customerobject | nullThe customer, when your chat knows them.
externalIdstring | nullYour own id for the customer: it comes back on escalation.completed as customerRef.
at most 128 characters
firstNamestring | nullThe avatar greets them by it.
at most 80 characters
emailstring | nullKept with the escalation only; never read out, never sent back.
reasonstring | nullA short phrase finishing "your chat about …" ("the refund on order 1042"): the avatar opens with it.
at most 120 characters
returnUrlstring | nullAn https address the visitor is offered at the end (back to your chat).
at most 2,000 characters
Returns
201 The link. data is Escalation.
Fields of data
idstringrequiredEscalation id (
esc_…).pattern `^esc_.*`
urlstringrequiredThe link to show the customer (or open in the widget). It works once, for 30 minutes; the token is in its fragment, never sent to a server.
tokenstringrequiredzesc_…: the link's own token (already in url). For one conversation only; never put it in a query string or a log.
expiresAtstring (date-time)requiredISO 8601 UTC timestamp.
Errors
invalid_requestunauthorizedforbiddenupgrade_requirednot_foundconflictgonerate_limitedserver_error
Request
curl -X POST "https://app.zentiqai.com/v1/portals/ID/escalations" \
-H "Authorization: Bearer $ZENTIQ_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"transcript": [
{
"role": "visitor",
"text": "…"
}
]
}'import { Zentiq } from "@zentiq/node";
const zentiq = new Zentiq({ apiKey: process.env.ZENTIQ_API_KEY });
const result = await zentiq.createEscalation("ID", {
transcript: [{
role: "visitor",
text: "…",
}],
});import os
from zentiq import Zentiq
zentiq = Zentiq(api_key=os.environ["ZENTIQ_API_KEY"])
result = zentiq.create_escalation("ID", {
"transcript": [{
"role": "visitor",
"text": "…",
}],
})Example answer
{
"data": {
"id": "esc_cmfq2b7x10000abcd1234",
"url": "https://www.example.com",
"token": "…",
"expiresAt": "2026-09-25T12:00:00.000Z"
},
"error": null,
"requestId": "req_4f1c2e9a7b3d5e6f7a8b9c0d"
}/v1/portals/{id}/case-linksList a portal's case links
The portal's case links, newest first, with their state and opens. Never the case's details or the links' addresses. A link's row is deleted 30 days after it stops working.
Path parameters
idstringrequiredPortal id (
portal_…); the id without its prefix is accepted too.
Query parameters
limitintegerPage size, 1-200 (default 50).
cursorstringThe
nextCursorof the previous page.
Returns
200 A page of links. A page: data is a list of CaseLink, and nextCursor fetches the next page.
Fields of data
idstringrequiredCase link id (
clk_…).pattern `^clk_.*`
portalIdstringrequiredPortal id (
portal_…).referencestringrequiredkind"case" | "order" | "claim" | "appointment" | "ticket" | "booking" | "application" | "renewal"requiredcustomerIdstringrequiredYour own id for the customer.
status"active" | "expired" | "revoked" | "used_up"requiredactive: it opens; expired: past expiresAt; revoked: switched off; used_up: every conversation it may start has started.
opensintegerrequiredConversations started from it so far.
maxOpensintegerrequiredexpiresAtstring (date-time)requiredISO 8601 UTC timestamp.
revokedAtstring (date-time) | nullrequiredlastOpenedAtstring (date-time) | nullrequiredemailedAtstring (date-time) | nullrequiredWhen your team emailed it from the console; null when it was sent some other way.
source"api" | "console"requiredMade with an API key, or by your team in the console.
createdAtstring (date-time)requiredISO 8601 UTC timestamp.
Errors
invalid_requestunauthorizedforbiddenupgrade_requirednot_foundrate_limitedserver_error
Request
curl "https://app.zentiqai.com/v1/portals/ID/case-links?limit=20" \
-H "Authorization: Bearer $ZENTIQ_API_KEY"import { Zentiq } from "@zentiq/node";
const zentiq = new Zentiq({ apiKey: process.env.ZENTIQ_API_KEY });
const page = await zentiq.listCaseLinks("ID", {
limit: 20,
});import os
from zentiq import Zentiq
zentiq = Zentiq(api_key=os.environ["ZENTIQ_API_KEY"])
page = zentiq.list_case_links("ID", limit=20)Example answer
{
"data": [
{
"id": "clk_cmfq2b7x10000abcd1234",
"portalId": "portal_cmfq2b7x10000abcd1234",
"reference": "…",
"kind": "case",
"customerId": "…",
"status": "active"
}
],
"error": null,
"requestId": "req_4f1c2e9a7b3d5e6f7a8b9c0d",
"nextCursor": null
}/v1/portals/{id}/case-linksSend a customer a link to their case
A personal link to the portal's website door for one customer and one case (an order, a claim, an appointment, a support ticket), made by your CRM or helpdesk. Send url by email, text message or your app: when they open it, the avatar greets them about their case and already knows what it is about. The link expires (7 days unless you say, 30 at most), opens at most maxOpens conversations (20 unless you say) and can be revoked. The case is your record: the avatar reads it as data (never as instructions), only ever says the email masked, and stops talking about it if the visitor may be under 18. The conversation's actions run for this customer (customer.id) and carry the case (case: { reference, kind }), so your system finds it without asking. A link is recognition, not a full identity check (anyone holding it is taken for your customer): for anything sensitive, switch on Verified customer, which sends a one-time code to the email you gave. url is only in this answer: Zentiq keeps a hash, never the link. The case's details are kept encrypted and wiped when the link stops working. 409 conflict with details.reason case_links_off while the organisation's serve.case_links switch is off, and door_closed while the portal's website door isn't open to visitors (published, open to visitors, with a website subject live). Case links are on Starter and up: on a plan without them, 403 upgrade_required with details.reason plan_required and details.plan. A portal of another organisation is 404; an archived one 410.
Path parameters
idstringrequiredPortal id (
portal_…); the id without its prefix is accepted too.
Body
customerIdstringrequiredYour own id for the customer: every action call from the conversation carries it as
customer.id.at most 128 characters
referencestringrequiredThe case's own reference in your system (an order, claim, booking or ticket number): the avatar names it, and every action call carries it as
case.reference.at most 64 characters
kind"case" | "order" | "claim" | "appointment" | "ticket" | "booking" | "application" | "renewal" | nullWhat the case is, for the avatar's words ("your order 10423", "your support ticket 10423").
default "case"
summarystringrequiredWhat the case is about and where it stands, in a sentence or two the avatar may use with your customer. The avatar reads it as your record (data, never instructions).
at most 1,000 characters
factsmap of string | number | boolean | nullUp to 20 short facts, like { "status": "delayed", "items": 2 } (names of letters and digits, values of at most 200 characters). Never a password, code or card detail: a fact named like one is refused.
firstNamestring | nullYour customer's first name: the avatar greets them with it.
at most 40 characters
emailstring | nullThe email address you have on file for them. The avatar only ever sees it masked ("s***@example.com"). With Verified customer switched on, it is where the one-time code goes before a sensitive action.
openingstring | nullThe avatar's first line about the case, in your words. Without it the avatar says what the case is and asks how it can help.
at most 300 characters
expiresInDaysnumber | nullHow long the link works, in whole days.
1-30 · default 7
maxOpensnumber | nullHow many conversations the link may start (so a forwarded link can't be used again and again). Opening the page alone uses none.
1-100 · default 20
Returns
201 The link, with the address to send (only in this answer). data is CaseLinkCreated.
Fields of data
idstringrequiredCase link id (
clk_…).pattern `^clk_.*`
portalIdstringrequiredPortal id (
portal_…).referencestringrequiredkind"case" | "order" | "claim" | "appointment" | "ticket" | "booking" | "application" | "renewal"requiredcustomerIdstringrequiredYour own id for the customer.
status"active" | "expired" | "revoked" | "used_up"requiredactive: it opens; expired: past expiresAt; revoked: switched off; used_up: every conversation it may start has started.
opensintegerrequiredConversations started from it so far.
maxOpensintegerrequiredexpiresAtstring (date-time)requiredISO 8601 UTC timestamp.
revokedAtstring (date-time) | nullrequiredlastOpenedAtstring (date-time) | nullrequiredemailedAtstring (date-time) | nullrequiredWhen your team emailed it from the console; null when it was sent some other way.
source"api" | "console"requiredMade with an API key, or by your team in the console.
createdAtstring (date-time)requiredISO 8601 UTC timestamp.
urlstringrequiredThe link to send your customer (by email, text message or your app). Shown only now, never again: Zentiq keeps no copy it could show. Never put it in a public place.
Errors
invalid_requestunauthorizedforbiddenupgrade_requirednot_foundconflictidempotency_conflictidempotency_in_progressgonerate_limitedserver_error
Request
curl -X POST "https://app.zentiqai.com/v1/portals/ID/case-links" \
-H "Authorization: Bearer $ZENTIQ_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"customerId": "…",
"reference": "10423",
"summary": "The parcel is delayed at the depot; the new delivery date is Tuesday."
}'import { Zentiq } from "@zentiq/node";
const zentiq = new Zentiq({ apiKey: process.env.ZENTIQ_API_KEY });
const result = await zentiq.createCaseLink("ID", {
customerId: "…",
reference: "10423",
summary: "The parcel is delayed at the depot; the new delivery date is Tuesday.",
});import os
from zentiq import Zentiq
zentiq = Zentiq(api_key=os.environ["ZENTIQ_API_KEY"])
result = zentiq.create_case_link("ID", {
"customerId": "…",
"reference": "10423",
"summary": "The parcel is delayed at the depot; the new delivery date is Tuesday.",
})Example answer
{
"data": {
"id": "clk_cmfq2b7x10000abcd1234",
"portalId": "portal_cmfq2b7x10000abcd1234",
"reference": "…",
"kind": "case",
"customerId": "…",
"status": "active"
},
"error": null,
"requestId": "req_4f1c2e9a7b3d5e6f7a8b9c0d"
}/v1/case-links/{id}Get a case link
One link's state: whether it still opens, how many conversations it started, when it expires. Never the case's details or the link's address.
Path parameters
idstringrequiredCase link id (
clk_…); the id without its prefix is accepted too.
Returns
200 The link. data is CaseLink.
Fields of data
idstringrequiredCase link id (
clk_…).pattern `^clk_.*`
portalIdstringrequiredPortal id (
portal_…).referencestringrequiredkind"case" | "order" | "claim" | "appointment" | "ticket" | "booking" | "application" | "renewal"requiredcustomerIdstringrequiredYour own id for the customer.
status"active" | "expired" | "revoked" | "used_up"requiredactive: it opens; expired: past expiresAt; revoked: switched off; used_up: every conversation it may start has started.
opensintegerrequiredConversations started from it so far.
maxOpensintegerrequiredexpiresAtstring (date-time)requiredISO 8601 UTC timestamp.
revokedAtstring (date-time) | nullrequiredlastOpenedAtstring (date-time) | nullrequiredemailedAtstring (date-time) | nullrequiredWhen your team emailed it from the console; null when it was sent some other way.
source"api" | "console"requiredMade with an API key, or by your team in the console.
createdAtstring (date-time)requiredISO 8601 UTC timestamp.
Errors
invalid_requestunauthorizedforbiddenupgrade_requirednot_foundrate_limitedserver_error
Request
curl "https://app.zentiqai.com/v1/case-links/clk_123" \
-H "Authorization: Bearer $ZENTIQ_API_KEY"import { Zentiq } from "@zentiq/node";
const zentiq = new Zentiq({ apiKey: process.env.ZENTIQ_API_KEY });
const result = await zentiq.getCaseLink("clk_123");import os
from zentiq import Zentiq
zentiq = Zentiq(api_key=os.environ["ZENTIQ_API_KEY"])
result = zentiq.get_case_link("clk_123")Example answer
{
"data": {
"id": "clk_cmfq2b7x10000abcd1234",
"portalId": "portal_cmfq2b7x10000abcd1234",
"reference": "…",
"kind": "case",
"customerId": "…",
"status": "active"
},
"error": null,
"requestId": "req_4f1c2e9a7b3d5e6f7a8b9c0d"
}/v1/case-links/{id}Revoke a case link
The link stops opening at once and its case details are wiped; a conversation it already started goes on. Revoking a link that is already revoked changes nothing. Works while the switch is off.
Path parameters
idstringrequiredCase link id (
clk_…); the id without its prefix is accepted too.
Returns
200 The link, revoked. data is CaseLink.
Fields of data
idstringrequiredCase link id (
clk_…).pattern `^clk_.*`
portalIdstringrequiredPortal id (
portal_…).referencestringrequiredkind"case" | "order" | "claim" | "appointment" | "ticket" | "booking" | "application" | "renewal"requiredcustomerIdstringrequiredYour own id for the customer.
status"active" | "expired" | "revoked" | "used_up"requiredactive: it opens; expired: past expiresAt; revoked: switched off; used_up: every conversation it may start has started.
opensintegerrequiredConversations started from it so far.
maxOpensintegerrequiredexpiresAtstring (date-time)requiredISO 8601 UTC timestamp.
revokedAtstring (date-time) | nullrequiredlastOpenedAtstring (date-time) | nullrequiredemailedAtstring (date-time) | nullrequiredWhen your team emailed it from the console; null when it was sent some other way.
source"api" | "console"requiredMade with an API key, or by your team in the console.
createdAtstring (date-time)requiredISO 8601 UTC timestamp.
Errors
invalid_requestunauthorizedforbiddenupgrade_requirednot_foundrate_limitedserver_error
Request
curl -X DELETE "https://app.zentiqai.com/v1/case-links/clk_123" \
-H "Authorization: Bearer $ZENTIQ_API_KEY"import { Zentiq } from "@zentiq/node";
const zentiq = new Zentiq({ apiKey: process.env.ZENTIQ_API_KEY });
const result = await zentiq.revokeCaseLink("clk_123");import os
from zentiq import Zentiq
zentiq = Zentiq(api_key=os.environ["ZENTIQ_API_KEY"])
result = zentiq.revoke_case_link("clk_123")Example answer
{
"data": {
"id": "clk_cmfq2b7x10000abcd1234",
"portalId": "portal_cmfq2b7x10000abcd1234",
"reference": "…",
"kind": "case",
"customerId": "…",
"status": "active"
},
"error": null,
"requestId": "req_4f1c2e9a7b3d5e6f7a8b9c0d"
}/v1/portals/{id}/doorsEvery way in to a portal
The addresses to hand out: the members' sign-in page, the visitors' page (whether it is open now, and what would open it), the embed for your signed-in users, and the custom domain.
Path parameters
idstringrequiredPortal id (
portal_…); the id without its prefix is accepted too.
Returns
200 The doors. data is PortalDoors.
Fields of data
membersobjectrequiredurlstringrequiredThe sign-in page.
loginMethods("email" | "login_card" | "class_code" | "visitor")[]requiredunitIdstring | nullrequired
The visitors' door: people come in without signing in.
embedobjectrequiredurlstringrequiredWhat an iframe mounts, with
?t=and a token fromPOST /portals/{id}/embed-tokens.scriptstringrequiredThe snippet for a signed-in embed (your backend gets the token).
customDomainobject | nullrequirednamestringrequiredverifiedbooleanrequiredverifiedAtstring (date-time) | nullrequired
Errors
invalid_requestunauthorizedforbiddenupgrade_requirednot_foundrate_limitedserver_error
Request
curl "https://app.zentiqai.com/v1/portals/ID/doors" \
-H "Authorization: Bearer $ZENTIQ_API_KEY"import { Zentiq } from "@zentiq/node";
const zentiq = new Zentiq({ apiKey: process.env.ZENTIQ_API_KEY });
const result = await zentiq.listPortalDoors("ID");import os
from zentiq import Zentiq
zentiq = Zentiq(api_key=os.environ["ZENTIQ_API_KEY"])
result = zentiq.list_portal_doors("ID")Example answer
{
"data": {
"members": {
"url": "https://www.example.com",
"loginMethods": [
"email"
],
"unitId": "unit_cmfq2b7x10000abcd1234"
},
"visitors": {
"url": "https://www.example.com",
"open": true,
"readiness": {
"open": true,
"text": "…",
"fix": "publish"
},
"allowedOrigins": [
"…"
],
"script": "…"
},
"embed": {
"url": "https://www.example.com",
"script": "…"
},
"customDomain": {
"name": "Zoe",
"verified": true,
"verifiedAt": "2026-09-25T12:00:00.000Z"
}
},
"error": null,
"requestId": "req_4f1c2e9a7b3d5e6f7a8b9c0d"
}/v1/portals/{id}/doors/visitorsOpen or close the visitors' door
Sets the websites that may show the visitor page and start conversations from it (allowedOrigins, at most 20; [] allows any), and opens or closes the door (open). Opening needs portals:publish; on a published portal it runs the go-live check on what the door opens (422 go_live_refused), and it lets outside people talk to the organisation's avatars (its public website switch). Only a portal with a website assistant or a screening role has a visitors' door (409 conflict).
Path parameters
idstringrequiredPortal id (
portal_…); the id without its prefix is accepted too.
Body
allowedOriginsstring[]Website addresses (
https://www.example.com); [] allows any website again.at most 20 items
openbooleantrueopens the portal to visitors (needsportals:publish; runs the go-live check on a published portal, and lets outside people in);falsecloses it.
Returns
200 The visitors' door as it now stands. data is VisitorsDoor.
Fields of data
urlstring | nullrequiredThe visitor page to hand out (or the custom domain that leads there); null when no subject opens to visitors.
openbooleanrequiredVisitors can come in now.
readinessDoorReadiness | nullrequiredNull when no subject opens to visitors (a website assistant or a screening role).
allowedOriginsstring[]requiredThe only websites that may show the visitor page in a frame and start conversations from it; [] = any.
scriptstring | nullrequiredThe snippet that puts the visitor page on your website.
Errors
invalid_requestunauthorizedforbiddenupgrade_requirednot_foundconflictgo_live_refusedrate_limitedserver_error
Events
Request
curl -X PUT "https://app.zentiqai.com/v1/portals/ID/doors/visitors" \
-H "Authorization: Bearer $ZENTIQ_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'import { Zentiq } from "@zentiq/node";
const zentiq = new Zentiq({ apiKey: process.env.ZENTIQ_API_KEY });
const result = await zentiq.updateVisitorDoor("ID", {});import os
from zentiq import Zentiq
zentiq = Zentiq(api_key=os.environ["ZENTIQ_API_KEY"])
result = zentiq.update_visitor_door("ID", {})Example answer
{
"data": {
"url": "https://www.example.com",
"open": true,
"readiness": {
"open": true,
"text": "…",
"fix": "publish"
},
"allowedOrigins": [
"…"
],
"script": "…"
},
"error": null,
"requestId": "req_4f1c2e9a7b3d5e6f7a8b9c0d"
}