Skip to main content

August 3, 2026

Breaking Changes

This lands for existing integrations on 4 September 2026, 00:00 Europe/Prague. Until then production keeps serving the old shape, so nothing breaks today — you have until that date to migrate. The new shape is live on staging now; test against it there.While you are still receiving the old shape, every response carries two headers: Deprecation: true, which marks the response as coming from a superseded contract, and Sunset, holding the exact deadline. Watch for either in your own logs — when they stop appearing, you are on the new shape.
  • Responses no longer carry the { statusCode, message, data, traceId } wrapper: The body of a successful response is now the resource itself. Reading a field that used to live at response.data.x now means reading response.x.
    List endpoints lose a level of nesting in particular. The page object’s own array is called data, so under the wrapper it sat at data.data — the reason for this change. The counts move up beside it:
    Affected: every route under /agents, /candidates, /companies, /interviews, /jobs, /phone-numbers, /billing and /webhooks. /sourcing and /voice already answered without the wrapper and are unchanged. Nothing else about these responses changed — the same fields carry the same values, one level higher. The HTTP status code already told you the status the statusCode field repeated, and message was the constant string "Success". If you generated a client from our OpenAPI specification, your client already expected this shape: the published spec has always declared the payload type rather than the wrapper, and this change makes the API match the document rather than the other way round.
  • Delete routes answer with the deleted id instead of a bare boolean: DELETE on /agents/{id}, /candidates/{id}, /companies/{id}, /interviews/{id} and /jobs/{id} returned "data": true inside the wrapper. Without the wrapper that body would have been the literal true, which names neither what was deleted nor which resource it was, so these routes now return { "id": "…", "deleted": true }. A failed delete still answers with an error status, not deleted: false.
  • The trace id moved to the x-trace-id response header: It was the wrapper’s traceId field. Every response now carries it as a header instead — including 204s and the audio stream from /voice, which have no JSON body to put it in. Error bodies keep their traceId field as well, unchanged. Quote this value when contacting support.

Behavior Changes

  • /jobs/{id} no longer distinguishes a foreign id from a missing one: GET, PATCH and DELETE on a job that belongs to another company previously answered 403 Forbidden with "Access denied to this job", while an id that existed nowhere answered 404 Not Found. The pair was usable as an oracle — one request per id told you whether that id existed in someone else’s company — and job ids are the ones most likely to be guessed, because they travel through ATS integrations. All three routes now answer the same 404 Not Found with "Job not found" for both cases, byte for byte. This matches /agents/{id}, /interviews/{id} and candidate deletion, which have always answered this way. If you branch on 403 to detect a cross-company job, that branch is now dead. Treat 404 as “this job is not available to this key” and do not read it as confirmation that the id exists elsewhere. 403 still means what it always meant on these routes: your API key is missing the required scope.
  • DELETE /jobs/{id} answers 404 for an id that never existed, instead of 200: The route previously returned 200 with "data": false when asked to delete an id it could not find, so a typo and a real deletion were told apart only by the body. It now answers 404 Not Found, the same as DELETE /agents/{id}. A 200 from this route means a job was really deleted; for the body it returns, see the response-shape change above. If you treat this route as an idempotent delete — repeating it and accepting the second 200 — the repeat now returns 404. Deletion itself is unchanged: the first call still permanently removes the job and its candidate assignments.

Documentation

  • The by-id 404 is documented as deliberate: openapi.json and the job and agent reference pages now state that an id that exists nowhere and an id in another company answer identically on purpose, so the response cannot be used to tell them apart. The DELETE operations on both resources were also described as soft deletes in openapi.json; both have always been permanent, and the descriptions have been corrected.

August 2, 2026

Breaking Changes

  • New interview status: UNREACHABLE: Interviews that exhaust their call attempts without ever reaching the candidate now settle on UNREACHABLE rather than FAILED or CANCELLED. The terminal vocabulary was COMPLETED / FAILED / CANCELLED, none of which described “we called the permitted number of times and nobody picked up” — so that outcome was split across the other two depending on internal timing.
    • Where it appears: the status field on GET /interviews, GET /interviews/{id}, and PATCH /interviews/{id}, plus the status query filter on GET /interviews. Generated clients built from a previous OpenAPI snapshot will reject responses carrying the new value until regenerated.
    • What lands there: the last attempt ended in no answer, busy, or voicemail and the retry budget is spent. Technical failures — the call could not be placed or held — remain FAILED. Deliberate cancellations remain CANCELLED.
    • If you filter or report on FAILED or CANCELLED interviews, this cohort has moved. Historical interviews were backfilled, so the change is visible in past data too, not only in newly created interviews.
  • This supersedes the July 20, 2026 behavior change. That entry announced that interviews exhausting their call attempts would be CANCELLED rather than FAILED. That reclassification was only ever applied on one of the two internal paths that end a retry-exhausted interview; the other left them FAILED, and in the most common cases left them stuck in SCHEDULED indefinitely (see below). Both paths now agree on UNREACHABLE.

Bug Fixes

  • Interviews no longer stall in SCHEDULED forever: An interview whose call attempts had all finished could remain SCHEDULED permanently, never emitting a terminal webhook and never settling into a terminal status. Two independent causes, both fixed:
    • Retry exhaustion stamped the final call attempt with the specific reason it failed — NO_ANSWER, BUSY, VOICEMAIL, TIMEOUT, FAILED, DISCONNECTED — and the interview was only closed for statuses considered final. Those reasons are individually retryable, so they were not on that list, and the ordinary outcomes never closed the interview.
    • Removing a queued call attempt from the internal admin panel cancelled the attempt without closing its interview. Because such attempts were never dispatched, no provider callback ever arrived to close it either.
    • Affected interviews have been backfilled to the status they should have reached, reaching back to interviews created in October 2025. If you polled these and gave up, or reconciled them as abandoned on your side, they now carry a terminal status and a finishedDate.
  • Terminal webhooks now fire for exhausted interviews: Because those interviews never reached a terminal status, they never emitted interview.cancelled either, contradicting the “exactly one terminal event per interview” guarantee documented in July. They now emit it.
  • Wrong-number, language-barrier and anti-robot calls now end the interview: These three outcomes were recorded on the call attempt but classified as neither final nor retryable, so no follow-up attempt was scheduled and the interview was never closed — it stalled on the very first such call. They are now terminal. A wrong number or an automated gatekeeper that ended the call before the candidate was ever on the line resolves to UNREACHABLE; a language barrier resolves to FAILED.

Behavior Changes

  • interview.cancelled payload status vs. the interview resource: The webhook payload still carries "status": "CANCELLED" for every cancellation reason, including MAX_ATTEMPTS_EXCEEDED. The interview resource fetched afterwards will read UNREACHABLE for that reason. Branch on the payload’s reason field rather than data.status, and re-read the resource if you mirror interview status into your own system. The payload field is unchanged so existing consumers keep working.

Documentation

  • Interview status values corrected and documented: The Interviews resource guide listed status values in lower case (scheduled, in_progress) and omitted UNDEFINED. The API has always returned upper-case values. The section now lists every value with its correct casing, whether it is terminal, and what puts an interview there.