Sessions
A session is one interview for one candidate against one persona. Creating a session mints a candidate link; the rest of the interview happens between the candidate's browser and ZenHire's interview backend.
Start a session
POST https://platform.zenhire.ai/api/v1/interview/personas/{personaId}/sessions
| Field | Type | Required | Notes |
|---|---|---|---|
consent_mode | string | yes | explicit or integrator — see Consent. |
candidate_ref | string | no | Your opaque reference for the candidate (e.g. an ATS id). Max 256 chars. Stored for correlation. |
metadata | object | no | {key:value} correlation map (same limits as Speech/CV DeepMatch: ≤ 50 keys, key ≤ 40 chars, value ≤ 500 chars, ≤ 8 KB total). Echoed back and filterable. |
tags | string[] | no | Up to 20 distinct tags, each 1–40 chars. Echoed back and filterable. |
Before the session is created, the platform runs two pre-checks:
- Permission — your client must have the
interviewpermission. - Credits — your balance must be at least the minimum to start a session
(otherwise
402 INSUFFICIENT_CREDITS).
Creating a session only mints a candidate link — it does not consume a
concurrent-interview slot, so you can mint as many links as you need (each is
bounded only by its expires_in token TTL). Your maxConcurrentRequests cap
applies to live interviews: it is enforced when a candidate actually starts
their interview, not when you create the link. If your client is already at its
cap of concurrent live interviews, the candidate's start is held off until a
running interview finishes.
curl -X POST \
"https://platform.zenhire.ai/api/v1/interview/personas/0f2c9e2a-.../sessions" \
-H "X-API-Key: zh_api_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "consent_mode": "explicit", "candidate_ref": "candidate-12345" }'
Response (HTTP 201):
{
"session_id": "7c3a1b2d-9e8f-4a6b-8c1d-2e3f4a5b6c7d",
"candidate_link": "https://interview.zenhire.ai/#t=Zk9xQ2pYb1pUcWp5d3R...",
"expires_in": 604800
}
The candidate-token model
The candidate_link carries a token in its URL fragment (#t=), not a
query string. A fragment is never sent to a server, so the token survives an
email link-preview or a security scanner opening the link. The token is:
- Per-candidate — it admits a single candidate to one interview. Reloads
and brief reconnects are tolerated, but once the interview reaches a terminal
state (
completed/failed) the link no longer starts a new interview. - Time-limited — it expires
expires_inseconds after creation (a 7-day pre-use TTL). An unused, expired token can no longer start an interview.
The raw token is returned only in this response. The platform stores only
a hash of it and never logs or re-emits it — so persist the candidate_link
(or session_id) immediately; you cannot retrieve the token again.
Send the link to the candidate by any channel — email, SMS, or your own UI.
Concurrency: live interviews only
Your maxConcurrentRequests cap limits the number of live interviews
running at once — sessions in the active state, where a candidate has actually
started the conversation. It is not a cap on links or sessions you create.
- Creating a session (and minting a link) never counts toward the cap — you can prepare as many links as you like.
- The cap is enforced at the start of the live interview: when a candidate begins, if your client is already at its cap, the start is held off until a running interview finishes.
- A live interview leaves the cap when it ends — whether it
completed,failed, or hit the maximum-duration auto-close described below.
Maximum interview duration
A live interview must complete within a fixed time budget. If a live interview runs past that maximum duration without a completion signal (for example the candidate closes the tab mid-interview, or a network drop is never recovered), the platform automatically closes it:
- the session moves to
status: "failed"witherror_code: "SESSION_TIMEOUT", - its concurrent-interview slot is freed.
The maximum duration is comfortably longer than a normal interview, so a completed interview is never cut short — only genuinely abandoned ones are reaped. The candidate UI shows the duration budget up front (the platform surfaces it to the candidate frontend when the link is opened).
A session closed this way has no result. You can re-invite the candidate from the ZenHire console.
Consent
Set consent_mode per session:
| Mode | Behaviour |
|---|---|
explicit | The candidate frontend shows a consent gate before the interview starts. ZenHire captures consent. |
integrator | You (the ATS) have captured consent contractually. No gate is shown. |
Candidate voice recordings are sensitive PII. Choose the mode that matches the consent you actually hold.
Verify-and-fetch lifecycle
You only call "start a session" and the results endpoints. The middle of the flow is a server-to-server verify-and-fetch handshake — documented here so you understand the lifecycle:
① you POST /personas/:id/sessions {consent_mode}
② platform mints a one-time token, creates the session as `initiated`,
returns the candidate link
③ you send the link to the candidate
④ candidate opens the link (consent gate if explicit)
⑤ interview exchanges the token with the platform (verify-session-token, S2S)
⑥ platform validates the token (unexpired, not over its join cap, not
terminal) → marks the session `active`, returns the persona
prompt/language/consent to the interview backend
⑦ candidate ↔ interview real-time conversation with live captions
⑧ interview POSTs the signed completion webhook to the platform
⑨ platform marks `completed`, deducts credits
⑩ you review results in-platform
Steps ⑤ and ⑧ are internal S2S contracts — the verify endpoint and the completion webhook. You never call them and are never given their secrets. See Completion webhook for the webhook contract.
List sessions
GET https://platform.zenhire.ai/api/v1/interview/sessions
Lists your client's sessions, newest first. Optional query params:
| Param | Notes |
|---|---|
status | initiated | active | completed | failed |
limit | Page size, 1–100 (default 20). |
offset | Rows to skip. |
curl "https://platform.zenhire.ai/api/v1/interview/sessions?status=completed&limit=20" \
-H "X-API-Key: zh_api_YOUR_KEY"
Get one session
GET https://platform.zenhire.ai/api/v1/interview/sessions/{sessionId}
Returns the session catalog row. The recording_s3_key / transcript_s3_key
fields are storage references only — to play the recording or read the
transcript, use the Results endpoints, which return a
short-lived URL.
{
"id": "7c3a1b2d-9e8f-4a6b-8c1d-2e3f4a5b6c7d",
"client_id": "9a1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d",
"persona_id": "0f2c9e2a-7b1d-4a8e-9b3c-1f2a3b4c5d6e",
"candidate_ref": "candidate-12345",
"status": "completed",
"source": "requests",
"consent_mode": "explicit",
"project_id": "3b9c1d2e-...",
"tags": ["q2-hiring"],
"metadata": { "ats_id": "abc-123" },
"started_at": "2026-05-29T10:10:00.000Z",
"completed_at": "2026-05-29T10:22:47.456Z",
"duration_sec": 742,
"credit_charged": 65,
"recording_s3_key": "recordings/.../audio.wav",
"transcript_s3_key": "transcripts/.../transcript.json",
"interview_svc_session_id": "ses_a1b2c3d4e5f6",
"created_at": "2026-05-29T10:00:00.000Z",
"persona": {
"id": "0f2c9e2a-7b1d-4a8e-9b3c-1f2a3b4c5d6e",
"role_name": "Senior Backend Engineer",
"language": "en",
"system_prompt": "You are an AI interviewer for the role of …",
"base_prompt_override": null
}
}
Sessions you start with an X-API-Key are tagged "source": "requests" and
inherit the calling key's project_id. The list endpoint
(GET /api/v1/interview/sessions) returns the same fields plus a top-level
role_name / language for each row (instead of the nested persona), and
accepts ?source=, ?project=, ?tags=, and ?metadata_key=/?metadata_value=
filters.
Re-inviting a candidate after a failed interview
When an interview ends without a result — for example one that hit the
maximum-duration auto-close (status: "failed",
error_code: "SESSION_TIMEOUT") and captured no recording or transcript — you
can re-invite the candidate from the ZenHire console. Reactivating from the
console resets the session to initiated, clears its failure reason, and mints a
fresh candidate link while preserving the session's persona, candidate reference,
project, tags, and metadata.
Re-invitation is a console action; there is no X-API-Key endpoint for it. A
session that already has a result (completed, or failed with a
recording/transcript) cannot be re-invited.