Skip to main content

Changelog

Unreleased

Added

  • CV DeepSearch API — corpus search (ingest + search + run status). A new module at /api/v1/cvds/* for finding the best-matching candidates in a corpus of parsed CVs against a position — the inverse of CV DeepMatch. Ingest parsed CVs into a corpus (POST /api/v1/cvds/candidates, idempotent batch upsert, fail-closed corpus_id, up to 500 candidates and ~10 MB per requestBATCH_TOO_LARGE / PAYLOAD_TOO_LARGE above those), check embedding status (GET /api/v1/cvds/candidates[/{external_id}]), then trigger a search (POST /api/v1/cvds/search → run id) and poll the run (GET /api/v1/cvds/runs/{id}). The position_metadata config is the same shape CV DeepMatch consumes as its requirements config. Auth is the unified clients.permissions[] model — calls require the cvdeepsearch permission. A cvdeepsearch completion webhook event type is selectable + test-sendable in the self-serve webhook tool. Results side in progress: the best-first result list, the /runs/{id}/results pagination endpoint, and the final completion webhook payload land in a follow-up release; until then the run poll returns results: null with results_pending: true. See the CV DeepSearch overview and the two flow guides — Ingestion (fill the corpus) and Search (query the corpus).
  • CV DeepMatch API — general availability. The /api/v1/cvdeepmatch/* endpoint family — submit (POST /api/v1/cvdeepmatch/submit), poll (GET /api/v1/cvdeepmatch/{id}), and list (GET /api/v1/cvdeepmatch/requests) — is now generally available. Upload a CV (PDF) plus a job description and a requirements configuration; receive an id and either poll the status endpoint or wait for a signed webhook callback. Auth is the unified clients.permissions[] model — call requires the cvdeepmatch permission. HMAC-SHA256 webhook signatures use the Stripe-style t=<unix-ts>,v1=<hex> header format in X-CVDM-Signature. PDF only at launch — DOCX support and additional matching algorithms are on the roadmap. See the CV DeepMatch integration guide and the CV DeepMatch API reference.
  • Per-run metadata + tags (Speech and CV DeepMatch). Both POST /api/v1/speech/analyze and POST /api/v1/cvdeepmatch/submit now accept an optional metadata object (a {key:value} map — string keys to string/number/boolean values, ≤ 50 keys, key ≤ 40 chars, value ≤ 500 chars, total ≤ 8 KB) and tags array (≤ 20 distinct strings, each 1–40 chars). Use them to correlate a run back to your own records (a candidate ID, an external customer ID, a batch label) without keeping a side table. Both are echoed back on every poll/detail response ({} / [] when none) and are filterable. Submitting them is fully backward compatible — omit them and runs behave exactly as before.
  • Interview API — general availability. The /api/v1/interview/* endpoint family is now reachable with an X-API-Key (the platform's third module, alongside Speech and CV DeepMatch). Create a reusable interviewer persona (POST /api/v1/interview/personas), start a per-candidate session (POST /api/v1/interview/personas/{personaId}/sessions) — which returns a candidate link carrying its token in the URL fragment (#t=<token>, never sent to a server) — then poll the session (GET /api/v1/interview/sessions/{sessionId}) and fetch the recording (streamed audio/wav) and transcript (JSON) once it completes. Eight endpoints in total: create/list personas, get persona, start session, list/get sessions, recording, transcript. Auth is the unified clients.permissions[] model — calls require the interview permission (else 403 MISSING_PERMISSION); a key is pinned to its own client and its sessions inherit the key's project. Sessions accept optional metadata + tags for correlation, identical to Speech and CV DeepMatch. See the Interview API overview and the Interview API quickstart. (This module was previewed as "Voice"; it was renamed to "Interview" — endpoints /api/v1/interview/*, the interview permission, and X-Interview-* signature headers — before this release.)
  • Per-organization webhook signing secret. Generate (and later rotate) a signing secret unique to your organization on the dashboard Settings page. The whsec_-prefixed secret is shown once at generation; afterwards only its last four characters are displayed. Once generated, every CV DeepMatch and Interview webhook callback is signed with your own secret (same t=<unix-ts>,v1=<hex> HMAC-SHA256 format in X-CVDM-Signature / X-Interview-Signature). Until you generate one, callbacks continue to use a default secret. See Verifying the webhook signature.
  • Re-invite a candidate after a closed-without-result interview. When an interview ends without a result — for example one auto-closed at the maximum duration (status: "failed", error_code: "SESSION_TIMEOUT") that captured no recording or transcript — you can re-invite the candidate from the ZenHire console. Re-inviting 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. A session that already has a result (completed, or failed with a recording/transcript) cannot be re-invited. See Re-inviting a candidate.
  • Interview maximum-duration auto-close is now documented + surfaced. A live interview that runs past the maximum duration without a completion signal is automatically closed (status: "failed", error_code: "SESSION_TIMEOUT"), freeing its concurrent-interview slot. The candidate UI now shows the duration budget up front. A candidate whose interview was auto-closed can be re-invited from the console. See Maximum interview duration.
  • GET /api/v1/speech/runs — list your API runs with optional filters (project incl. the none no-project sentinel, status, tags contains-all, metadata_key/metadata_value exact-match, serviceType, externalId, createdAfter, createdBefore) and pagination. The project/tags/metadata filters use the same language as the CV DeepMatch and Interview run lists — learn the filter syntax once and it works across every module.
  • externalId optional field on POST /api/v1/speech/analyze. Non-unique customer-supplied correlation id, echoed on every poll response, filterable on /speech/runs.
  • GET /api/v1/cvdeepmatch/requests now accepts the same project (incl. the none no-project sentinel), tags (contains-all), and metadata_key/metadata_value (exact-match) filters as GET /api/v1/speech/runs and GET /api/v1/interview/sessions, in addition to the existing status/limit/cursor. The filter language is now identical across all three module lists — learn it once and it works everywhere. Fully backward compatible: omit the new params and the list behaves exactly as before.
  • explainability object on successful/partial poll responses: { vocab, fluency } — a short plain-language explanation per dimension, safe to show directly to end users. Optional — omitted on failed runs and when a per-dimension feature breakdown isn't available.
  • Public documentation site at platform.zenhire.ai/docs with llms.txt / llms-full.txt bundles.

Changed

  • API key format updated to zh_api_…. Newly issued keys start with zh_api_ instead of the previous zh_ prefix. All keys issued before this change — including legacy zh_speech_live_…, zh_speech_test_…, zh_cv_live_…, and zh_… keys — continue to work without any action needed on your side. No migration required.
  • Speech run terminal status is now precise: success, partial, or failed. A run that completed but had an optional step degrade (for example the impressive-vocabulary boost couldn't run, or audio trimming fell back to the full clip) now returns status: "partial" — it still carries a full result payload with scores, analysis, and transcript, exactly like success. A run only returns failed when a required step failed (no result, no charge). Previously every completed run returned the same status regardless of whether an optional step degraded, so partial was documented but never actually emitted. No field shapes changed; if you branch only on failed vs. not-failed, no action is needed. See terminal failures and the poll endpoint.
  • Speech runs now fail honestly when accent can't be measured. Previously, if the accent measurement failed on a POST /api/v1/speech/analyze run, the run still completed using a placeholder accent value — which could mis-grade the result. Now such a run terminates with status: "failed" and error.code: "ACCENT_SCORE_UNAVAILABLE" (no credits charged) instead of returning a score we didn't actually measure. Resubmit the run if you hit it. See terminal failures.
  • Interview concurrency cap now applies to live interviews, not link creation. Creating an interview session (POST /api/v1/interview/personas/{personaId}/sessions) no longer counts against your maxConcurrentRequests cap and no longer returns 429 CONCURRENCY_LIMIT — minting a candidate link is unbounded (each link is still bounded by its expires_in token TTL). The cap now counts active (live) interviews and is enforced when a candidate actually starts their interview. You can therefore queue up as many candidate links as you like; only the number of simultaneously-running interviews is capped. Leaked sessions (a candidate joins but never finishes) are automatically closed after a maximum duration so they stop holding a slot.
  • Docs restructured around the 3-module + Universal model. The guides now have one section per module — Speech, Interview, CV DeepMatch — plus a Universal section that documents the cross-module concepts once: authentication, the run id model (the public id vs. your externalId correlation tag vs. the error-envelope requestId), the standard error envelope, credits, and health. Endpoint contracts are unchanged; this reorganizes how the docs are presented so onboarding to any module is consistent.
  • API reference + sandbox grouped by module. The API reference and the in-dashboard API Sandbox are now organized into four sections — CV DeepMatch, Interview, Speech, and Universal — so the cross-module endpoints (GET /api/v1/credits, GET /api/v1/health) sit in their own Universal section instead of being filed under one module. Endpoint contracts are unchanged; this is purely how the surface is presented.
  • Unified run identifier — every run/session response now carries a single id. Across all three modules (Speech, CV DeepMatch, Interview) the run/session response now returns ONE canonical identifier field, id, holding the public id you use to fetch it (GET /api/v1/<module>/{id}). The previous module-specific names are gone: CV DeepMatch no longer returns request_id/external_request_id, Speech returns id instead of requestId, and a created Interview session returns id instead of session_id. The customer-supplied correlation tag (externalId on Speech, candidate_ref on Interview) stays a separate, clearly-named field — it is never folded into id. There is no compatibility alias for the old field names.
  • CV DeepMatch source parameter retired. The source form/query parameter (production | testing) on POST /api/v1/cvdeepmatch/submit is no longer part of the contract. Any value you send — including the previous ?source=testing — is now ignored (not an error) and the run is recorded as ordinary API traffic. No action needed on your side; existing integrations that still send it keep working.
  • CV DeepMatch webhook_url is now optional. You can integrate poll-only — omit webhook_url and poll GET /api/v1/cvdeepmatch/{id} until a terminal state. Supplying a webhook_url (HTTPS + public host) additionally enables the signed callback; it is no longer required to receive results.
  • CV DeepMatch position-config validation enforced server-side. Submit now validates the config body before starting a match — every importance is an integer 1–5, workExperience.from ≤ to, relevant_industries holds 1–3 entries 1:1 with industries_config, hard_skills/soft_skills ≤ 5 each and ≤ 10 combined, minimal_qualifications/preferable_qualifications ≤ 3 each, study_status ∈ {Graduate, Undergraduate, Both}, and languages are plain names (no proficiency suffix). Omitted optional structures are auto-filled with safe defaults. Configs that violate a rule return INVALID_INPUT with error.details.fields rather than producing a silently-wrong score.
  • CV DeepMatch position_id charset tightened to [A-Za-z0-9._-], 1–28 characters. Longer IDs or characters outside that set now return INVALID_INPUT with details.fields.position_id.
  • Terminal-state naming clarified: success | partial | failed. There is no completed status. (The prior customer-facing TXT referenced a non-existent completed status — that's been corrected.)
  • Audio formats supported: MP3, WAV, M4A, WebM, OGG, FLAC (prior docs incorrectly said "MP3 only").
  • Max audio duration: 45 minutes (prior docs incorrectly said 20 minutes).
  • Speech credit admission is now exact-cost, not a flat minimum. POST /api/v1/speech/analyze is admitted when the request's cost (1 credit per started minute of audio, minimum 1) is available — where available = your credit balance minus credits currently held for other in-flight requests. The previous flat "≥ 15 credits to submit anything" floor is gone: a small balance can now fund a short clip (e.g. 3 credits funds a 2-minute request). Credits are held at submit and only charged when the analysis succeeds; a failed run is never charged and its hold is freed automatically. 402 INSUFFICIENT_CREDITS now means "cost exceeds available" and includes details.cost + details.available. If the audio duration can't be read at submit, the request is rejected synchronously with 400 AUDIO_DURATION_UNAVAILABLE (previously surfaced later as a failed run).
  • Concurrency default: 8 simultaneous processing runs per client (prior docs incorrectly said 30).
  • Health endpoint path: GET /api/v1/health (prior docs incorrectly said /api/health).

Removed

  • public/api-documentation.txt (the old hand-maintained flat TXT). Requests to that URL now redirect to /llms-full.txt.