Connect interviews to your hiring system
By the end of this guide your applicant tracking system (ATS) holds every first-round interview Zentiq runs for a role: each question with the candidate's answer, the written summary, the transcript and, where the role shares them, the marks. A person on your hiring team reads it there, decides, and the decision comes back to Zentiq.
You need: a screening role (see Screening from your ATS), the Node SDK, and two keys:
- one made with the Hiring integration preset, to invite candidates, receive webhooks and record decisions;
- one an owner or admin makes under More → API Keys with Interview content (hiring) ticked. It holds
candidates:interviews, which is never part of full access, which a key can never pass on, and which an app or assistant connected to Zentiq can never have. Keep it on the server that talks to your ATS.
1. The role#
const [role] = (await zentiq.listRoles({ status: "published" })).data; // a screening role (sub_…), set up as in "Screening from your ATS"2. Invite candidates, and hear when they are done#
Give each candidate your ATS's own id (externalId): it comes back on every candidate event and on the interview.
await zentiq.createInvites(role.id, {
candidates: [{ name: "Pat Jones", email: "pat@example.com", externalId: "ats-123" }], // externalId: your ATS's id
timing: { mode: "deadline", deadlineAt: new Date(Date.now() + 7 * 86_400_000).toISOString() },
send: true,
});
await zentiq.createWebhook({ url: "https://ats.example.com/zentiq/webhooks", events: ["candidate.screened", "candidate.erased"] });Webhooks carry ids, statuses and times only, never words or scores. candidate.screened says an interview reached the hiring team: that is the moment to fetch it.
3. Fetch the interview#
// The webhook says an interview reached the hiring team (ids only, never words): fetch the interview itself.
async function onEvent(event: ZentiqWebhookEvent) {
if (event.type === "candidate.screened" && event.data.candidateId) {
const interview = await interviews.getCandidateInterview(event.data.candidateId);
for (const q of interview.questions) console.log(`Q${q.n} ${q.question}\n ${q.answer ?? "(no answer)"}\n ${q.note ?? ""}`);
// score, band, rank, marks and verdicts are null unless the role shares scores (interview.scoresShared).
await ats.saveInterview(interview.externalId, interview);
}
if (event.type === "candidate.erased") {
// The candidate's data was erased in Zentiq: erase your copy too (their interview is now 410 gone).
console.log(`erase ${event.data.externalId ?? event.data.candidateId} in the ATS`);
}
}The interview holds:
requirements: each must-have question with their answer and whether it meets it (a person checks what is unclear);questions: each interview question in order, with what they said after it was read (from the transcript), the platform's note on it and, where the role shares scores, its verdict and marks;summary,strengths,concernsandevidence(their own words, by competency);transcript.turns: the AI interviewer (ai) and the candidate (candidate), turn by turn. An interview that opened with the unscored practice question (practiceQuestion: true) starts after it: that answer is never kept;adjustments.requested(whether they asked for an adjustment; never which one, as it can reveal a disability), theirreviewRequest, thereviews(hidden while the role uses blind review), andstartedAt,endedAt,interviewedAt.
score, band, rank, verdicts, marks and ratings are there only where the role shares scores (candidates.share_scores, off by default); otherwise they are null and scoresShared is false. They help your team sort, never decide.
What stays in Zentiq: reviewers' and deciders' notes, the details of integrity checks and of the report's flags (you get how many, and of which kinds), and any recording (you learn whether there is one).
4. Or sync on a schedule#
// Or pull on a schedule: every interview since the last sync, oldest first, page by page.
for await (const interview of interviews.paginate("listRoleInterviews", { since: await ats.lastSync() }, { params: { id: role.id } })) {
await ats.saveInterview(interview.externalId, interview);
}Oldest first, 20 a page (at most 50); since gives only the interviews that reached the hiring team after a moment. Candidates whose data was erased, and interviews the candidate ended themselves, are never listed.
5. Record the person's decision#
A person decides, never a system: name the member who decided.
// A person on your hiring team decides in the ATS; record their decision (never an automatic one).
await zentiq.decideCandidate("cand_123", { decision: "invite", reason: "strong_answers", decidedBy: "usr_123" });Every read is logged#
Each interview your key reads is recorded on the candidate as opened by that API key, and the hiring team sees it in the candidate's history in Zentiq. It never counts as a person having opened the interview: a decline still needs someone on the team to have opened or reviewed it.
What is never served#
- A candidate under 18:
404. - Anything that isn't a candidate's interview: a member's practice or mock interview, a builder's preview:
404. - A candidate whose data was erased (at their request or at the end of the role's retention), a deleted interview, and one the candidate ended themselves:
410 gone, withdetails.reason(erased,deleted,withdrawn). Whencandidate.erasedarrives, erase your copy too. - Another organisation's candidate or role:
404; a cursor from anywhere else:400. - A key without
candidates:interviews, or one no owner or admin made:403.
Both reads count against the heavy rate limit (see Limits and paging).