Skip to main content

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

FieldTypeRequiredNotes
consent_modestringyesexplicit or integrator — see Consent.
candidate_refstringnoYour opaque reference for the candidate (e.g. an ATS id). Max 256 chars. Stored for correlation.
metadataobjectno{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.
tagsstring[]noUp to 20 distinct tags, each 1–40 chars. Echoed back and filterable.

Before the session is created, the platform runs two pre-checks:

  1. Permission — your client must have the interview permission.
  2. 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_in seconds 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" with error_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.

Set consent_mode per session:

ModeBehaviour
explicitThe candidate frontend shows a consent gate before the interview starts. ZenHire captures consent.
integratorYou (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:

ParamNotes
statusinitiated | active | completed | failed
limitPage size, 1–100 (default 20).
offsetRows 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.