Skip to main content

August 9, 2026

New Features

  • Agent knowledge base: give an agent documents to consult mid-call: An agent can now be handed documents — a job spec, a benefits summary, a pricing sheet, an FAQ — and it looks them up on its own while the conversation is running. This was already available in the InstaView dashboard; it is now on the API, for template and custom agents alike. There is nothing to reference in your flow and no publish step: a completed document is attached to the agent’s next call. The file never passes through this API. You ask for a slot, upload the bytes straight to storage, then confirm:
    The alternative would have been sending the file inline as base64, which inflates a 20MB document to roughly 27MB on the wire and caps out well below that in practice. Three small JSON requests carry a full-size file instead. GET /agents/{id}/knowledge lists what is attached, PATCH /agents/{id}/knowledge/{documentId} takes { "isActive": false } to park a document without losing it, and DELETE /agents/{id}/knowledge/{documentId} erases it — the record, the stored file and the copy held by the voice provider. All of these use the scopes you already hold for agents: read:agents, write:agents and delete:agents respectively. Accepted: PDF, DOC, DOCX, TXT, MD, CSV, TSV, JSON, XML, YAML — stored in the format you upload, with no conversion step. Limits are 20 MB per file and 10 documents per agent; upload URLs are valid for 15 minutes. A document is PENDING until you complete it, and a PENDING document is never used on a call. It does hold one of the agent’s ten slots in the meantime, so a 400 about the document limit can mean uploads you started and never finished — GET the knowledge base to see them. Unfinished uploads are reclaimed automatically after 24 hours, or immediately if you DELETE them. Completion is where the file is actually checked, not the request that issues the URL. The sizeBytes you declare up front is a fast pre-check so an unusable upload fails before you transfer 20MB; what is enforced at completion is the object storage really holds — it must exist, be within the size limit, and carry the content type the URL was issued for. A file that fails is deleted and its slot released. Completion is idempotent: completing an already-completed document returns it unchanged, so a call that times out on your side is safe to retry.
  • Two endpoints now carry a rate limit below your plan’s: Starting an upload reserves a slot and, once completed, a stored file, so POST /agents/{id}/knowledge is limited to 6 requests per minute and 60 per hour per API key. Completing one is limited more generously — 20 per minute, 200 per hour — because retrying a failed completion should be cheap. These are an additional ceiling, not a separate allowance: your plan’s limits are charged first, and being inside them is not enough for these two routes. A rejection is the usual 429 with a Retry-After header. Every other endpoint is unaffected. See Rate Limiting.

Documentation

August 5, 2026

New Features

  • overrides: have an agent introduce itself as one of your customers: An agent has always spoken as the company its API key belongs to, which is wrong for anyone calling on behalf of their own customers — the candidate hears the integrator’s name instead of the employer’s. Agents now take an optional overrides object on POST /agents, PATCH /agents/{id} and the inline agent of POST /interviews:
    Both fields are optional and each falls back to your company profile, so every existing agent is unaffected. It works on template and custom agents alike, and applies from the next call — there is nothing to recompile. Send both fields together. They fall back independently, so companyName on its own leaves your own company’s description in place and the agent introduces itself as Acme while describing you. PATCH replaces the object whole rather than merging it — the same contract as guardrails and contextConfig — so send every field you want kept. "overrides": null clears them. Only what the agent says changes. Ownership, billing, analytics and phone-number routing keep your real company; the interview invitation email now names the same company the call will, so the two agree. You are responsible for having the right to speak in the name you send.

August 4, 2026

New Features

  • Custom agents: design the whole conversation instead of picking a template: An agent used to mean choosing a focusSCREENING, OUTREACH, GENERIC, LANGUAGE_TEST — and supplying questions. Those are hiring templates, which made the API awkward for anything that is not hiring. You can now send a flow instead and design the conversation yourself: a firstMessage, a list of sections, a lastMessage. Sections are sequential (a topic, written as a free-text prompt), looping (questions, each with the answer it is scored against) or conditional (branches taken on the contact’s intent). An agent is custom the moment it carries a flow — do not send focus as well, it is derived and sending both is rejected. Two optional objects come with it: guardrails (things the agent must always or never do) and contextConfig (role, communicationStyle, callToAction). Both are accepted only alongside a flow.
    Block ids and schemaVersion are assigned by InstaView and must not be sent — so a flow read back from the API has to have them stripped before it is sent again. An invalid flow is rejected with a 422 carrying an errors array listing every problem found, each with the path of the offending block, so fixing a flow does not take one request per fault. The same fields are accepted on the inline agent of POST /interviews, since that mints a real agent and cannot be a side door onto states POST /agents forbids. Any text in a flow may contain {{contact.…}} placeholders, resolved per call rather than when the agent is created, so one agent serves everyone you call. See the custom agents guide. A custom agent has no job, and will not take one. The hiring focuses associate an interview with a job; a flow-based agent is defined entirely by its flow, so a job would never be read. POST /interviews therefore rejects job, jobId and candidate.jobId with a 400 for a custom agent rather than storing an association nothing consumes. The corollary is the useful part: you never have to pick a job to interview someone. A contact assigned to five jobs, or to none, is called the same way — candidateId and agentId are all a custom interview needs.
  • analyticsConfig: get structured data back from a call, not just a transcript: A custom agent can now declare what should come back after every call, and the results arrive as data your own systems can act on. Every item has a key, and it is yours. It is the field name the results come back under, so your code switches on it. Omit it and InstaView derives a slug of the label (Budget confirmedbudget_confirmed) — convenient, but renaming that label later silently moves the results to a new key and breaks your consumer with a 200. Set it explicitly if you read results by name. A duplicate key you sent is rejected with 400 rather than quietly renamed. Scoring a flow question is set inline on the question, not in analyticsConfig — add importance and weight to the question itself, because questions are addressed by ids InstaView mints and you cannot know them while writing the flow. A question carrying an idealAnswer but neither importance nor weight is asked but not scored, so nothing enters your score that you did not ask for.
  • Conversation results: the new analytics object: The configured results appear as analytics on GET /interviews/{id}, on every row of GET /interviews, and on the analysis.completed webhook. It is a sibling of analysis, not a variant of it — analysis is the recruiting pipeline’s output and is typically absent on a custom call, which also has no analysisPdfBase64, since the PDF report is a recruiting artefact. Two details worth building against:
    • value: null on a field means the call did not surface it, which is distinct from a false or empty value that was surfaced. A contact who declines to answer gives you null; one who says “no” gives you false. confidence and evidence are omitted when the value is null.
    • weightPercent is each scored item’s share of the match score, and the shares sum to exactly 100. The weights you configure are relative, not percentages, so a raw 50 would tell a consumer nothing on its own.
    This is additive and custom-agent-only. analytics is a new optional field on an existing event, not a new event type — there is no analytics.completed, and an agent with a hiring focus emits exactly what it always did, with no analytics key at all. An existing HR integration observes no change.

Behavior Changes

  • Interview and analysis events now fire for interviews you did not create through this API: interview.started, interview.completed, interview.failed, interview.rescheduled, interview.cancelled, analysis.completed and analysis.failed were silently limited to interviews created through POST /interviews. An interview a recruiter launched from the InstaView dashboard — including every contact of a bulk calling run — emitted nothing at all, even with an active webhook on exactly those events for that company. Delivery is decided by the company the webhook belongs to, as it always was for choosing endpoints, so these events now cover every interview in that company. If you assumed every interviewId on these events was one you created, that is no longer true. The volume on an existing subscription can rise accordingly. Filter on the interview ids you know about if you only want your own. Note that analysis.completed carries the full analysis and a PDF, so this is the subscription most worth reviewing.
  • A webhook on an ATS parent company now receives its child companies’ events regardless of how the resource was created: The parent was reached through the API key that created the resource, not through the company hierarchy. So a parent-registered endpoint already received a child’s events when the resource was created with that parent’s ATS key — the common case, and unchanged — but received nothing when the same child’s interview was created with the child’s own API key, or from the InstaView dashboard. The parent is now resolved from the company hierarchy, so all three behave the same. If you register on both the parent and a child, the child’s events reach both endpoints. For resources created with the parent’s ATS key that was already true; it now also applies to resources created with the child’s own key. A child’s webhook is unaffected: it still receives only that child’s events, never a sibling’s and never the parent’s. The two copies are separate deliveries with separate deliveryIds, so deliveryId alone will not collapse them — it still does the job it is documented for, which is recognising a redelivery of the same delivery. The key that does collapse them differs per event, and is tabulated under Handling Duplicates. Note in particular that data.interviewId is only a safe key for interview.cancelled; the per-attempt events fire repeatedly for one interview, and keying on the interview alone would drop events you want.

Bug Fixes

  • A cancelled calling run now closes its contacts out: Cancelling a bulk calling run moves every contact it had not yet dialled to CANCELLED, but emitted no interview.cancelled for any of them, so an integrator was left with rows that never closed. Each interview the cancellation closes out now emits interview.cancelled with reason: "USER_REQUEST". Contacts the cancellation deliberately leaves alone — a call already in progress — still close through their own call-ended path and are not reported twice. These particular events are queued rather than sent the instant the run is cancelled, so that cancelling a run with thousands of contacts stays fast for the operator. They are delivered by the same worker that handles retries, normally within a minute or so, and larger runs drain over several minutes. Signing, retries and ordering guarantees are unchanged; only the timing of this one event differs, and every other event still goes out immediately.

Documentation

  • Event scope is stated in the webhooks guide: the Event Types section now says that interview and analysis events cover every interview in the company, not only those created through the API, and that a webhook on an ATS parent company also receives its children’s events.

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.