Subjects
A portal's subjects and their content.
/v1/subjects/{id}Get a subject
One subject: its template and kind, its live version and pending draft, its languages and the portals it is on.
Path parameters
idstringrequiredSubject id (
sub_…); the id without its prefix is accepted too.
Returns
200 The subject. data is Subject.
Fields of data
idstringrequiredSubject id (
sub_…).namestringrequiredtemplatestringrequiredThe template it was built from (
customfor one written for the organisation).kind"tutor" | "roleplay" | "assessor" | "service"requiredgroup"learn" | "train" | "serve" | "assess"requiredscreeningbooleanrequiredA candidate screening role (its invites and candidates are under /roles).
status"draft" | "published" | "archived"requireddraftuntil it (or its portal) is published.liveVersionintegerrequiredThe version sessions run; 0 when there is none.
draftVersioninteger | nullrequiredA pending draft to publish; null when there is none.
languagesstring[]requiredLanguage codes (
GET /languages).visitorLanguage"follow" | "listed" | "english" | nullrequiredA website subject: whether it follows the visitor's language, keeps to
languages, or speaks English. Null for any other.unitIdstring | nullrequiredportalIdsstring[]requiredcreatedAtstring (date-time)requiredISO 8601 UTC timestamp.
updatedAtstring (date-time)requiredISO 8601 UTC timestamp.
Errors
invalid_requestunauthorizedforbiddenupgrade_requirednot_foundrate_limitedserver_error
Request
curl "https://app.zentiqai.com/v1/subjects/sub_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.getSubject("sub_123");import os
from zentiq import Zentiq
zentiq = Zentiq(api_key=os.environ["ZENTIQ_API_KEY"])
result = zentiq.get_subject("sub_123")Example answer
{
"data": {
"id": "sub_cmfq2b7x10000abcd1234",
"name": "Zoe",
"template": "…",
"kind": "tutor",
"group": "learn",
"screening": true
},
"error": null,
"requestId": "req_4f1c2e9a7b3d5e6f7a8b9c0d"
}/v1/subjects/{id}Change a subject
Its name, its unit (unitId; null: the organisation level), and for a website subject the language rule for its visitors (visitorLanguage: follow the visitor's language, keep to its languages (listed), or english; null goes back to the organisation's own setting). Only the fields sent change, and what sessions run changes at once. visitorLanguage on any other subject is 409 conflict (details.reason: "not_a_website_subject"), and so is one the organisation has locked ("locked"). A website widget's subject changes with its avatar (PUT /personas/{id}/widget: 409 conflict, "widget").
Path parameters
idstringrequiredSubject id (
sub_…); the id without its prefix is accepted too.
Body
namestring1-80 characters
unitIdstring | nullThe unit it belongs to; null: the organisation level.
visitorLanguage"follow" | "listed" | "english" | nullA website subject only: follow the visitor's language, keep to its
languages(listed), or English only; null goes back to the organisation's own setting.
Returns
200 The subject as it now stands. data is Subject.
Fields of data
idstringrequiredSubject id (
sub_…).namestringrequiredtemplatestringrequiredThe template it was built from (
customfor one written for the organisation).kind"tutor" | "roleplay" | "assessor" | "service"requiredgroup"learn" | "train" | "serve" | "assess"requiredscreeningbooleanrequiredA candidate screening role (its invites and candidates are under /roles).
status"draft" | "published" | "archived"requireddraftuntil it (or its portal) is published.liveVersionintegerrequiredThe version sessions run; 0 when there is none.
draftVersioninteger | nullrequiredA pending draft to publish; null when there is none.
languagesstring[]requiredLanguage codes (
GET /languages).visitorLanguage"follow" | "listed" | "english" | nullrequiredA website subject: whether it follows the visitor's language, keeps to
languages, or speaks English. Null for any other.unitIdstring | nullrequiredportalIdsstring[]requiredcreatedAtstring (date-time)requiredISO 8601 UTC timestamp.
updatedAtstring (date-time)requiredISO 8601 UTC timestamp.
Errors
invalid_requestunauthorizedforbiddenupgrade_requirednot_foundconflictrate_limitedserver_error
Events
Request
curl -X PATCH "https://app.zentiqai.com/v1/subjects/sub_123" \
-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.updateSubject("sub_123", {});import os
from zentiq import Zentiq
zentiq = Zentiq(api_key=os.environ["ZENTIQ_API_KEY"])
result = zentiq.update_subject("sub_123", {})Example answer
{
"data": {
"id": "sub_cmfq2b7x10000abcd1234",
"name": "Zoe",
"template": "…",
"kind": "tutor",
"group": "learn",
"screening": true
},
"error": null,
"requestId": "req_4f1c2e9a7b3d5e6f7a8b9c0d"
}/v1/subjects/{id}/contentRead a subject's content
The content document of one version: facts, FAQs, opening hours, scenarios, questions, the rubric, a screening role... one section per thing a business edits, in camelCase. By default the pending draft, or the live version when there is none (what your next change builds on). The ETag is "v<version>": send it back as If-Match (or baseVersion) with your change.
Path parameters
idstringrequiredSubject id (
sub_…); the id without its prefix is accepted too.
Query parameters
version"live" | "draft" | stringdraft(the default: the pending draft, or the live version when there is none: what your next change builds on),live, or a version number.
Returns
200 The content. data is SubjectContentVersion.
Fields of data
versionintegerrequiredThe version shown; its
ETagis"v<version>".livebooleanrequiredIt is what sessions run.
basedOninteger | nullrequiredThe version it was saved over; null for the first.
A subject's content, one section per thing a business edits. Sections that don't apply to the subject's kind are left out; a section that applies but isn't set is null (objects) or [] (lists).
Errors
invalid_requestunauthorizedforbiddenupgrade_requirednot_foundrate_limitedserver_error
Request
curl "https://app.zentiqai.com/v1/subjects/sub_123/content" \
-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.getSubjectContent("sub_123");import os
from zentiq import Zentiq
zentiq = Zentiq(api_key=os.environ["ZENTIQ_API_KEY"])
result = zentiq.get_subject_content("sub_123")Example answer
{
"data": {
"version": 1,
"live": true,
"basedOn": 1,
"content": {
"name": "Zoe",
"audience": "…",
"languages": [
"en"
],
"gradeBand": [],
"facts": [
{
"key": "…",
"label": "…",
"value": "…",
"help": "…",
"keep": true,
"perPerson": true
}
],
"topics": [
{
"id": "…",
"title": "…",
"description": "…",
"grade": [],
"skills": [
"…"
],
"prerequisites": [
"…"
]
}
]
}
},
"error": null,
"requestId": "req_4f1c2e9a7b3d5e6f7a8b9c0d"
}/v1/subjects/{id}/contentChange a subject's content (as a draft)
Saves the sections in set as a new draft version; what sessions run changes only when it is published (POST /subjects/{id}/publish). A list section replaces the whole list (an item without an id gets one); an object section changes only the fields sent (null clears one); facts also takes { key: value }. facts.assistant_name is always the name of the avatar that speaks, set on each portal with PUT /portals/{id}/persona: a changed value is 400 invalid_request (param: body.set.facts.assistant_name). A document read with GET can be sent back as it is. The content's own rules are checked (400 invalid_request, details.errors: [{ path, message }] in the document's paths), and the plan's features (403 upgrade_required). With If-Match or baseVersion, a newer version is 409 version_conflict (details.currentVersion); without, the change applies to the newest. The answer says whether the draft could go live (goLive). Sending the newest content again stores nothing.
Path parameters
idstringrequiredSubject id (
sub_…); the id without its prefix is accepted too.
Body
The sections to change. A list replaces the whole list; an object section changes only the fields sent (null clears an optional one);
factsalso takes{ key: value }. A document read with GET can be sent back as it is.notestringKept with the version.
at most 200 characters
baseVersionintegerThe version your change is based on (or send
If-Match: "v<n>"); a newer one is409 version_conflict.at least 1
Returns
200 The draft. data is SubjectContentDraft.
Fields of data
versionintegerrequiredThe version shown; its
ETagis"v<version>".livebooleanrequiredIt is what sessions run.
basedOninteger | nullrequiredThe version it was saved over; null for the first.
A subject's content, one section per thing a business edits. Sections that don't apply to the subject's kind are left out; a section that applies but isn't set is null (objects) or [] (lists).
goLiveobjectrequiredreadybooleanrequiredWorth fixing; never a refusal.
Errors
invalid_requestunauthorizedforbiddenupgrade_requirednot_foundversion_conflictrate_limitedserver_error
Events
Request
curl -X PATCH "https://app.zentiqai.com/v1/subjects/sub_123/content" \
-H "Authorization: Bearer $ZENTIQ_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"set": {}
}'import { Zentiq } from "@zentiq/node";
const zentiq = new Zentiq({ apiKey: process.env.ZENTIQ_API_KEY });
const result = await zentiq.updateSubjectContent("sub_123", {
set: {},
});import os
from zentiq import Zentiq
zentiq = Zentiq(api_key=os.environ["ZENTIQ_API_KEY"])
result = zentiq.update_subject_content("sub_123", {
"set": {},
})Example answer
{
"data": {
"version": 1,
"live": true,
"basedOn": 1,
"content": {
"name": "Zoe",
"audience": "…",
"languages": [
"en"
],
"gradeBand": [],
"facts": [
{
"key": "…",
"label": "…",
"value": "…",
"help": "…",
"keep": true,
"perPerson": true
}
],
"topics": [
{
"id": "…",
"title": "…",
"description": "…",
"grade": [],
"skills": [
"…"
],
"prerequisites": [
"…"
]
}
]
},
"goLive": {
"ready": true,
"problems": [
{
"field": "hours",
"message": "…",
"short": "…"
}
],
"warnings": [
{
"field": "hours",
"message": "…",
"short": "…"
}
]
}
},
"error": null,
"requestId": "req_4f1c2e9a7b3d5e6f7a8b9c0d"
}/v1/subjects/{id}/go-liveCheck a subject can go live
Runs the go-live check on a version (by default the pending draft, or the live version): what to finish first, in the content document's fields. Changes nothing.
Path parameters
idstringrequiredSubject id (
sub_…); the id without its prefix is accepted too.
Query parameters
version"live" | "draft" | stringdraft(the default: the pending draft, or the live version when there is none: what your next change builds on),live, or a version number.
Returns
200 Whether it can go live. data is SubjectGoLive.
Fields of data
versionintegerrequiredThe version checked.
readybooleanrequiredWorth fixing; never a refusal.
Errors
invalid_requestunauthorizedforbiddenupgrade_requirednot_foundrate_limitedserver_error
Request
curl "https://app.zentiqai.com/v1/subjects/sub_123/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.getSubjectGoLive("sub_123");import os
from zentiq import Zentiq
zentiq = Zentiq(api_key=os.environ["ZENTIQ_API_KEY"])
result = zentiq.get_subject_go_live("sub_123")Example answer
{
"data": {
"version": 1,
"ready": true,
"problems": [
{
"field": "hours",
"message": "…",
"short": "…"
}
],
"warnings": [
{
"field": "hours",
"message": "…",
"short": "…"
}
]
},
"error": null,
"requestId": "req_4f1c2e9a7b3d5e6f7a8b9c0d"
}/v1/subjects/{id}/versionsList a subject's versions
Every stored version of the subject's content, newest first: which is live, which is the pending draft, and who saved each.
Path parameters
idstringrequiredSubject id (
sub_…); 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 versions. A page: data is a list of SubjectVersion, and nextCursor fetches the next page.
Fields of data
versionintegerrequiredlivebooleanrequireddraftbooleanrequiredThe pending draft.
notestring | nullrequiredcreatedAtstring (date-time)requiredISO 8601 UTC timestamp.
createdByobjectrequiredtype"user" | "api_key" | "system"requiredidstring | nullrequiredusr_…orkey_…; null for the system.
Errors
invalid_requestunauthorizedforbiddenupgrade_requirednot_foundrate_limitedserver_error
Request
curl "https://app.zentiqai.com/v1/subjects/sub_123/versions?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.listSubjectVersions("sub_123", {
limit: 20,
});import os
from zentiq import Zentiq
zentiq = Zentiq(api_key=os.environ["ZENTIQ_API_KEY"])
page = zentiq.list_subject_versions("sub_123", limit=20)Example answer
{
"data": [
{
"version": 1,
"live": true,
"draft": true,
"note": "…",
"createdAt": "2026-09-25T12:00:00.000Z",
"createdBy": {
"type": "user",
"id": "…"
}
}
],
"error": null,
"requestId": "req_4f1c2e9a7b3d5e6f7a8b9c0d",
"nextCursor": null
}/v1/subjects/{id}/publishPublish a subject's content
Makes a version (by default the pending draft) what sessions run, and publishes the subject. It must pass the go-live check (422 go_live_refused, details.problems), the plan's features (403 upgrade_required) and, for a screening role that keeps candidates, the round lock (409 conflict, details.reason: "round_locked": send newRound: true to start a new round). An older version is brought back as the newest. Already live and published: returned unchanged.
Path parameters
idstringrequiredSubject id (
sub_…); the id without its prefix is accepted too.
Body (optional)
versionintegerThe version to publish (default: the newest, the pending draft). An older one is brought back as the newest.
at least 1
newRoundbooleanA screening role that keeps candidates: changes to how candidates compare start a new round (otherwise
409 conflict, reasonround_locked).
Returns
200 The subject as it now stands. data is Subject.
Fields of data
idstringrequiredSubject id (
sub_…).namestringrequiredtemplatestringrequiredThe template it was built from (
customfor one written for the organisation).kind"tutor" | "roleplay" | "assessor" | "service"requiredgroup"learn" | "train" | "serve" | "assess"requiredscreeningbooleanrequiredA candidate screening role (its invites and candidates are under /roles).
status"draft" | "published" | "archived"requireddraftuntil it (or its portal) is published.liveVersionintegerrequiredThe version sessions run; 0 when there is none.
draftVersioninteger | nullrequiredA pending draft to publish; null when there is none.
languagesstring[]requiredLanguage codes (
GET /languages).visitorLanguage"follow" | "listed" | "english" | nullrequiredA website subject: whether it follows the visitor's language, keeps to
languages, or speaks English. Null for any other.unitIdstring | nullrequiredportalIdsstring[]requiredcreatedAtstring (date-time)requiredISO 8601 UTC timestamp.
updatedAtstring (date-time)requiredISO 8601 UTC timestamp.
Errors
invalid_requestunauthorizedforbiddenupgrade_requirednot_foundconflictidempotency_conflictidempotency_in_progressgo_live_refusedrate_limitedserver_error
Events
Request
curl -X POST "https://app.zentiqai.com/v1/subjects/sub_123/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.publishSubject("sub_123");import os
from zentiq import Zentiq
zentiq = Zentiq(api_key=os.environ["ZENTIQ_API_KEY"])
result = zentiq.publish_subject("sub_123")Example answer
{
"data": {
"id": "sub_cmfq2b7x10000abcd1234",
"name": "Zoe",
"template": "…",
"kind": "tutor",
"group": "learn",
"screening": true
},
"error": null,
"requestId": "req_4f1c2e9a7b3d5e6f7a8b9c0d"
}/v1/subjects/{id}/translate-questionsTranslate a subject's questions (as a draft)
Translates what is said of each question (the question, its options, follow-ups and complication) into language and saves it as a new draft, for you to check and publish (POST /subjects/{id}/publish); the guides for marking stay as written. A screening interview, an exam or an assessment asks each question word for word, so everyone is asked the same: translate once, here, rather than in each session. All or nothing: when a question can't be translated well enough (its numbers, its options and follow-ups kept), nothing is saved (503, try again). An unknown language, English, or a subject without questions is 400 invalid_request.
Path parameters
idstringrequiredSubject id (
sub_…); the id without its prefix is accepted too.
Body
languagestringrequiredA name or code from
GET /languages("German", "es"): the portal's language.1-80 characters
Returns
200 The draft with the translated questions. data is SubjectQuestionsTranslated.
Fields of data
versionintegerrequiredThe draft that holds the translated questions.
languagestringrequiredThe language's name.
translatedintegerrequiredHow many questions were translated.
Errors
invalid_requestunauthorizedforbiddenupgrade_requirednot_foundversion_conflictrate_limitedserver_error
Events
Request
curl -X POST "https://app.zentiqai.com/v1/subjects/sub_123/translate-questions" \
-H "Authorization: Bearer $ZENTIQ_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"language": "en"
}'import { Zentiq } from "@zentiq/node";
const zentiq = new Zentiq({ apiKey: process.env.ZENTIQ_API_KEY });
const result = await zentiq.translateSubjectQuestions("sub_123", {
language: "en",
});import os
from zentiq import Zentiq
zentiq = Zentiq(api_key=os.environ["ZENTIQ_API_KEY"])
result = zentiq.translate_subject_questions("sub_123", {
"language": "en",
})Example answer
{
"data": {
"version": 1,
"language": "en",
"translated": 1
},
"error": null,
"requestId": "req_4f1c2e9a7b3d5e6f7a8b9c0d"
}