Skip to the content
Menu: Screening from your ATS

Screen applicants from your ATS

By the end of this guide every applicant in your applicant tracking system (ATS) gets a link to a first-round interview with an avatar. You hear when each is screened, pull the shortlist, and record the hiring team's decisions.

You need: a plan with candidate screening, a key made with the Hiring integration preset (keys made before 2026-10-01 reach candidates only once an owner adds candidates:read and candidates:write to them), and the Node SDK.

1. Set up the role#

A role is a subject of a screening portal: what the job is, where, and the questions every applicant is asked.

candidate-screening.ts
const careers = await zentiq.createPortal({ name: "Acme Careers", templates: ["candidate-screening"] });
const roleId = careers.subjects[0].id; // the role is a subject: sub_…

await zentiq.updateSubjectContent(roleId, {
  set: {
    screening: {
      role: "Warehouse Operative",
      location: "Columbus, Ohio",
      intro: "This is a first-round interview for a warehouse operative job in Columbus. Every applicant answers the same questions.",
      requirements: [{ question: "Can you work weekend shifts?", kind: "yes_no", meets: { anyOf: ["yes"] }, knockout: false }],
      humanContact: "careers@acme.example",
      market: "US",
    },
  },
});

Two settings shape how the interviewer talks, the same for every applicant to the role, so the ranking stays fair:

  • screening.intensity: relaxed, standard (the default) or rigorous, how much detail it asks for.
  • screening.style: structured (the default: each question numbered, the same thank-you after every answer) or natural (no question numbers; it says back a fact the applicant gave or follows up, then asks the next question). Both ask the same questions in the same order and score them the same way. An interview held in another language keeps the numbers.

2. Publish it#

Screening questions go through a fairness check: a question about age, family or health, for example, is refused (422 go_live_refused, naming it) until you change it.

candidate-screening.ts
try {
  await zentiq.publishSubject(roleId);
  await zentiq.publishPortal(careers.id);
} catch (e) {
  // The fairness check refuses questions about age, family and the like: e.problems says which.
  if (e instanceof GoLiveRefusedError) console.log(e.problems);
  else throw e;
}

3. Hear about candidates#

candidate-screening.ts
await zentiq.createWebhook({
  url: "https://ats.acme.example/zentiq/webhooks",
  events: ["candidate.invited", "candidate.screened", "candidate.decided"],
});

Candidate events carry ids, the role, a status and times, and your ATS's own id for the candidate (externalId). Never a name, an email, answers, notes or a score: you fetch what you need with the API.

4. Invite applicants from your ATS#

candidate-screening.ts
const invites = await zentiq.createInvites(roleId, {
  candidates: [
    { name: "Pat Jones", email: "pat@example.com", externalId: "ats-123" }, // externalId: your ATS's id, sent back on every event
    { name: "Robin Diaz", email: "robin@example.com", externalId: "ats-124" },
  ],
  timing: { mode: "deadline", deadlineAt: new Date(Date.now() + 7 * 86_400_000).toISOString() },
  timeZone: "America/New_York",
  send: true, // Zentiq emails each candidate their link
});
for (const s of invites.skipped) console.log(`not invited: ${s.email}`); // an address already invited, for example

Each invite has its own link (inv_…). An address already invited is listed in skipped, not invited twice.

5. Pull the shortlist#

When candidate.screened arrives, read the role's candidates:

candidate-screening.ts
// After candidate.screened: the ranked list. Scores, bands and ranks appear only where the role shares them.
const shortlist = await zentiq.listCandidates(roleId, { sort: "rank" });
// mustHaves: met, missed or unclear (flagged for a person, never acted on).
for (const c of shortlist.data) console.log(c.id, c.externalId, c.mustHaves, c.score, c.band, c.rank);

A candidate's score, band and rank are there only where the role is set to share them; otherwise they are null and the list is by date. The list never carries answers, summaries or the transcript, and a candidate's interview is never read as a session (GET /v1/sessions/{id}/transcript is 403 candidate_interview). To bring the interview itself into your ATS, with a key an owner or admin gives interview content, see Interviews in your hiring system. Notes and recordings never leave Zentiq.

6. Record the team's decision#

A person decides, never a system. Name the member who decided (decidedBy); they must have opened the interview (or reviewed it) before declining, and each decision is one candidate's.

candidate-screening.ts
// A person decides: name them. Declining needs them to have opened the interview (or reviewed it) first.
try {
  await zentiq.decideCandidate(shortlist.data[0].id, { decision: "decline", reason: "missing_must_have", decidedBy: "usr_123" });
} catch (e) {
  if (e instanceof ConflictError) console.log(`not yet: ${e.reason}`); // for example, they haven't opened it
  else throw e;
}

candidate.decided then goes out with the decision and its reason code.