September 29, 2026
Behavior Changes
-
A
human_handoffnumber has to be a real number, not just the right shape. Until now aphoneNumberwas accepted if it looked like E.164: a+and up to 15 digits. A number with a digit missing, or a national number behind the wrong country code, passed that check, was saved, and then made every call on the agent fail before anyone was dialled. The number is now also checked against its country’s numbering plan when you create or update an agent, or send an inlineagenton Create Conversation:Nothing that dials today stops working: a number this rejects is one no call on the agent could have used. An agent already saved with such a number is not blocked: requests that do not sendflowstill go through, and the next request that sends it has to fix the number.
September 23, 2026
New Features
-
A conversation now records which agent configuration it ran on. Editing an agent used
to overwrite its compiled flow in place, so once a question set changed there was no way to
tell what an earlier call had actually said. Every call placed by an agent with a designed
flow is now stamped at dial time with
flowVersion: a content hash of the compiled configuration (flow, guardrails and context). It appears onGET /conversations/{id}and the list, on the conversation and on each entry incallAttempts.Two calls with the same value heard the same configuration; a retry placed after an edit carries the new one, asattempt-2does above. We keep the configuration behind each value for as long as a conversation that used it exists, so an audit or a contact exercising their data rights can be answered from our records. It identifies the configuration, it does not reveal it: the prompt stays on our side. Calls placed before today, inbound calls and agents without a designed flow readnull: there is nothing true to backfill them with. The field is not yet part of the webhook payloads.
September 14, 2026
Behavior Changes
-
The agent composer now answers immediately and works in the background. Designing an
agent is a language-model call that we measured at 15 to 45 seconds, and both composer write
routes used to hold your request open for the whole of it. That is longer than a lot of API
gateways are willing to wait — 30 seconds is a common default — and when one gave up, the
request had already succeeded on our side: the session existed, the agent had been designed,
and the id you needed to reach it went to a connection that was no longer there.
No timeout we control could fix that, because the one that decides the outcome is yours. So
the work moved off the request:
POST /agents/composerandPOST /agents/composer/{sessionId}/messagesnow return 202 with{ "sessionId": "…", "status": "processing" }.GET /agents/composer/{sessionId}is the poll: 202 while a turn is running, 200 when it has finished, withstatusofreadyorfailed. Every five seconds is a sensible interval.- Or pass
webhookIdwhen you start a session and be told instead — see below.
201is now a202. It is the same pattern the sourcing endpoints have used since May. -
A brief the composer cannot build is no longer a
422. There is no request left to fail by the time we know, so the session ends upfailedand the poll carries the reason, with the flow validator’s errors undererror.detailsexactly as the422used to carry them.COMPOSER_INVALID_RESULTis worth rephrasing for; any other code is worth retrying unchanged. The422on these routes now means only that awebhookIdyou sent is not subscribed to the composer events. -
One turn at a time, refused before you pay for it. Sending a refinement while another is
still running is a
409within milliseconds. Previously the second turn ran a full model generation and was rejected when it tried to save, which cost you a generation to be told no. -
A failed refinement no longer costs you the agent you had. The session keeps the preview
its last successful turn produced: you can read it, create from it, or try a different
instruction against it. Only a session whose first turn failed has no agent, and posting
that session’s id to
POST /agentsis now a409telling you to poll, rather than a complaint about missing required fields.
New Features
composer.completedandcomposer.failedwebhooks. PasswebhookIdwhen you start a session and we will call that webhook for whichever of the two outcomes it subscribes to —composer.completedcarrying the same agent the poll would have given you,composer.failedcarrying the reason and no agent. Subscribing to one is enough to be accepted, and you then hear nothing about the other, so subscribe to both if you want every outcome. We check the subscription when you send thewebhookIdrather than silently delivering nothing later. Pinned once per session and reused by every refinement of it. Polling always works as well, and is always authoritative.
Fixed
-
A superseded sourcing run no longer reports results that are not its own. A run that lost
ownership of its session partway through — because a
PATCHqueued behind it took over, or because an internal outage lasted long enough for its claim to lapse — used to carry on regardless. It then marked the session completed and sentsourcing.completedwith whatever candidates were on the session at that moment: candidates the other run had found, reported before that run had finished finding them. The superseded run now stops as soon as it notices, and says nothing. Whoever owns the session reports its outcome, and a session that ends up with no owner at all is still failed asTIMED_OUTby the usual 30-minute sweep. If you were reconciling sourcing webhooks byrequestIdand occasionally saw one arrive with candidate counts that did not match what a later read returned, this was why.
September 10, 2026
Fixed
-
An agent no longer says a sentence with a word missing out of the middle of it. A
{{contact.*}}placeholder we hold no value for has always resolved to nothing at all, which is the right behaviour — the alternative is an agent reading “contact dot organization” out loud — but nothing told the agent it happens. A first message written as"am I through to {{contact.organization}}?"was spoken, to a contact with no organisation, as"am I through to?". The agent is now told what an absent value does and is instructed to reword the line as it says it, rather than reading the fragment or announcing that something is missing. Write prompts that still read correctly without the value anyway — the repair is a safety net, not a substitute for a sentence that survives losing a word. -
A custom agent asked for a human now transfers instead of hanging up. If your
flowcontains ahuman_handoffsection, the agent used to be carrying two contradictory instructions: the handoff step told it to transfer, and a standing call-handling rule told it that a request for a person means saying “a colleague will contact you soon” and ending the call. The standing rule won, so the one thing a handoff exists for was the one thing that never happened. The standing rule is now the transfer-flavoured version for any flow that can transfer: a request for a person is still honoured immediately, wherever in the call it comes, but it hands the call to your number rather than ending it. Where a flow reaches several destinations the agent picks bylabel, which is why a multi-destination flow has always required one on each. Nothing changes for an agent with nohuman_handoff. There is nowhere to transfer to, so it still takes a message and ends the call. -
instructionsreaches a custom agent’s call. The field was accepted, stored and returned in full for an agent with aflow, and then never compiled into the prompt. The effect was worse than an error: an agent given its reference data there, a stock list or a price card, answered from nothing at all and made the figures up. It is now compiled as its own reference section, the same way it always has been for a template agent. If you have been sendinginstructionsalongside aflow, the agent will start using it on its next call. Worth a read of what you sent, in case it holds something you did not mean the agent to be able to say out loud.
Documentation
-
{{system.today}},{{system.now}}and{{system.day_of_week}}are documented. They are not new — every call has substituted them into flow text for as long as{{contact.*}}has worked — but nothing said so, so the only way to find them was to be told. They are filled from the platform’s clock in the contact’s own timezone, and they are on Create Agent with the contact variables. -
{{contact.profile}}is written down, as something not to use in a spoken line. It is the rendered contact-context block — a paragraph, not a value — and an agent that meets it in a first message reads that paragraph out loud. -
{{contact.company_name}}is your company, and the docs now say so where you would look. It resolves from the company placing the call, so “I see you work at{{contact.company_name}}” is fluent, wrong on every call, and rejected by nothing. The contact’s own employer is an ordinary catalogued field.
Notes
- All of these reach agents you have already created. Nothing to re-save.
September 9, 2026
Changed
-
Contact context stopped being a curation chore.
contextConfig.useContactContextused to volunteer a summary built from the fields you had ticked as contact context, and everything else the contact held was invisible to the agent. It now gives the agent two things: a short platform-decided set of fields in its prompt, and a generated document it can query mid-call holding everything else the contact has a value for. Their documents (a CV, files you uploaded) are on the same knowledge base as before. Nothing about the agent switch changed, and no request shape you send changed. What changed is how much reaches the agent, and it is strictly more. -
includeInContactContextis removed from/contact-fields. It was a per-field allowlist that shipped empty, so the feature was effectively off for anyone who had not walked the catalog. Two properties replace it, and neither needs curating:Take it out of your payloads. SendingincludeInContactContexton a create or update is now a400, as any unknown property is:Beyond that, if you were setting it on a non-personal field you need do nothing: that field now reaches the agent by default, through the document. If you were setting it on a field markedisPii, that decision was migrated toallowPiiInContextand still holds. “By default” is not “always”. A field reaches the agent when it is notreadOnlyand either is notisPiior has been let past that:company_name,company_description,agent_nameandprofilearereadOnly, describe the caller rather than the person being called, and are in neither the prompt nor the document whatever you set on them. -
isPiinow enforces rather than defaults. A personal field is excluded from the prompt AND from the document, which is what keepsemailandphoneoff every call without anyone deciding.allowPiiInContextis the way past it, per field, and it puts the value in both halves — a queryable value is one the agent might never think to look up, and for something like an amount owed that is the whole point. SendingallowPiiInContexton a field that is notisPiiis a400rather than a silent no-op.
Notes
- A contact’s document is generated the first time a context-enabled agent is scheduled to call them, and refreshed before a later call only when a value or the catalog actually changed. A contact nobody calls costs nothing. An immediate ad-hoc call for a contact with no document yet runs prompt-only rather than waiting on one, and it is ready for the next call.
- A value written between generation and the dial is not in that call’s document. The window is bounded by the freshness check at dial time.
- The document follows the contact’s retention, exactly as their uploaded documents do: it is deleted, provider copy included, when the contact is deleted or reaches its GDPR expiry.
- It does not appear in
GET /contacts/{id}/documentsand does not count against the five uploads per contact. It is our projection of data you already hold, not a file you attached.
September 8, 2026
Behavior Changes
-
A conditional branch’s
conditionis now validated. It never was. A branch could be stored with a condition the platform could not read — atypeoutsideintentandexpression, anintentwith nodescription, anexpressionwith nofield— and the flow was accepted. Nothing told you, and the agent still took that branch on calls: the compiled instruction for it came out describing nothing, so the route was chosen arbitrarily. That is the failure this closes. Four new codes join theerrorsarray on Create Agent and Update Agent, each with thepathof the offending branch:That last one closes a hole of its own. Anullinbranchescounted toward the branch total and then every per-branch rule skipped it, so the flow validated clean and the failure surfaced later as a500while the prompt was being built. It is a422naming the branch now.What to check in your own integration. A payload that sent a condition of your own shape, or left one off, was accepted before and is a422now. Everything the API has ever documented forFlowBranchConditionstill passes unchanged,EXISTSstill needs novalue, andlogicalOperatoris still accepted and ignored. The branch you nominate asdefaultBranchis checked like the rest: its condition is not read while it is the fallthrough, but movingdefaultBranchlater would put it back in charge of a route. Agents already holding an unreadable condition were repaired rather than left to fail: each one became{ "type": "intent", "description": "<the branch's label>" }, the label being the only authored statement of what that route means. Their cached prompts were dropped so the next call compiles from the repaired flow. If a repaired branch does not route the way you meant, the label is what to rewrite.
September 7, 2026
Added
-
Contact documents: a contact can now hold documents an agent may look up on a call — a cover letter, application notes, and the contact’s own processed CV. Five endpoints under
/contacts/{id}/documents, mirroring the agent knowledge base exactly: list, start an upload (presigned URL), complete it, rename or deactivate, delete. Same request and response shapes, same file types, same 20 MiB per-file limit, same upload rate limit. Scopes areread:contacts,write:contactsanddelete:contacts.A contact’s CV shows up on its own. Once a CV has been processed, the contact lists a document withsource: "CV". You did not upload it and cannot delete or rename it here — it is produced from the anonymized text of the CV, which is what the agent can look up. You can deactivate it, and that is how you keep a particular contact’s CV out of calls. Two limits instead of one. A contact holds at most 5 uploaded documents (the CV does not count). On top of that, your company’s total contact-document storage is capped by plan, and both are reserved when the upload is started — pending uploads included — so parallel requests cannot all pass the same check. When the company ceiling is the problem, the400says so in numbers: -
sourceon knowledge documents: every document — agent or contact — now carriessource, either"UPLOAD"or"CV". On an agent’s knowledge base it is always"UPLOAD". Additive; nothing else about the shape changed. -
Whether documents reach a call is not decided by the API. One switch decides: the agent must have
contextConfig.useContactContextenabled. With it on, the contact’s active documents are attached to the call as thecandidateknowledge base and the agent is told they exist and may be looked up, never read aloud. With it off, the call runs exactly as before. Every other knowledge base (company,position,reference_documents) is unaffected. Contact documents follow the contact’s retention: they are deleted — stored files and provider copies included — when the contact reaches its GDPR expiry date. -
The contact-field catalog now ships thirty-six default fields instead of eleven:
GET /contact-fieldsreturns a seeded set covering identity, the contact’s employer, orders and invoices, the relationship, appointments and screening, so most of what you would have defined by hand already exists. Only three of the previous eleven (order_number,is_vip,lead_source) could hold a per-contact value at all; the rest resolve from the contact record, your company or the agent. The new rows are the ones an agent would actually say:job_title,organization,department,company_size,industry,website,city,secondary_phone,invoice_number,amount_due,due_date,renewal_date,contract_end_date,plan_name,deal_value,lead_status,owner_name,customer_since,last_purchase,tags,appointment_at,years_experience,notice_period,salary_expectationandavailability_date. Nothing breaks — the response is a list and you iterate it — but two things follow. If you render every field you get back as a form row, expect thirty-six rows. And most seeded fields are still yours to shadow: define your own field with the same key and it overrides the label and type for your company alone. That is the intended path forcompany_size,industryandlead_status, which are seeded asstringbecause one company’s option sets are not another’s. The eight reserved keys (first_name,last_name,email,phone,company_name,company_description,agent_name,profile) refuse even that, because the platform resolves them itself. Five of them default toincludeInContactContext, so an agent withcontextConfig.useContactContexton can refer to them without a token in the flow:lead_source,job_title,plan_name,owner_nameandlast_purchase. Everything else is off, because every field that defaults on lengthens every prompt on every call.Superseded on September 9:includeInContactContextis gone and there is no per-field opt-in any more. See that day’s entry.organizationis the contact’s employer;company_nameis yours.{{contact.company_name}}resolves the company placing the call and always has — it is labelled “Your company name” for that reason. An author writing “I see you work at{{contact.company_name}}” got a fluent wrong sentence with no signal, soorganizationnow exists for what that author meant. -
categoryon a contact field: a new response property onGET /contact-fieldsnaming which panel the field belongs to —identity,organisation,commercial,relationship,scheduling,recruiting,platformorcustom. The list is now ordered bycategoryfirst, thendisplayOrder, thenkey, so iterating the response and breaking on a change ofcategorygives you the same panels the dashboard shows. It is set by InstaView and not writable: a field you define is alwayscustom, and a field of yours that shadows a seeded one reports the seeded field’s category, sojob_titlestays underorganisationfor everybody even after you relabel it. BecausedisplayOrdernow only separates fields inside one category, each category’s numbering restarts at 10 rather than continuing one long sequence. If you were sorting ondisplayOrderalone, sort oncategoryfirst. -
A
tagsdata type:dataType: "tags"stores astring[], and it is the one type that accepts two shapes on the way in — an array, or one comma-separated string, so a single CSV cell holding several tags maps onto one field without your side splitting it first.Entries are trimmed, blanks are dropped ("vip,, renewal,"is two tags) and duplicates are removed case-insensitively, keeping the spelling that came first. An empty list is the erase:[],[""]and","all clear the field, exactly asnulland""do, so you never get a key present with an empty array behind it. A number, a boolean or an object is a422rather than a one-element list — a caller sending42for a tags field has a mapping bug, and a silent["42"]would be spoken on a call. The seededtagsfield uses it.enumis still the right type when the set is closed and a value outside it should be refused. -
CSV aliases on every writable seeded field, in English, Slovak and Czech —
renewal_datecarriesrenewal,renewsanddátum obnovenia;amount_duecarriesbalance,outstandinganddlžná suma. Nothing consumes them yet. The current CSV importer maps against a fixed list of built-in contact properties and does not readcsvAliases, so this changes no import behaviour today — it is catalog metadata, seeded so the catalog is complete before the contact-field import lands. Adding your own withPATCH /contact-fields/{id}does not touch InstaView’s. -
fieldson the contact resource itself:POST /contactsandPATCH /contacts/{id}now accept catalogued field values in the body, so creating a contact with its values is one request instead of two.Before this, the read of a contact’s values sat on the contact (GET /contacts/{id}returnsfields) while the write sat somewhere else — andPOST /contactsdropped any inbound values and always answeredfields: {}, so the one request where you most want to set them was the one that could not. Field values are contact attributes; the resource for a contact’s attributes is the contact. The shape is the flat oneGET /contacts/{id}already returns, so read and write agree.fieldsmerges andmetadatareplaces, in the same request body, on purpose:metadatais one opaque document you own,fieldsare individually catalogued values. -
The
/contacts/{id}/fieldssub-resource is gone.GETreturned a strict subset ofGET /contacts/{id}, and its write did exactly whatPATCH /contacts/{id}with onlyfieldsdoes — same scopes, same guards, same blast radius, since a partial update touches only what you send. One resource, one shape.?createMissingFields=truemoved with it and now hangs off both contact writes. Off by default: a key infieldsthat your catalog does not know is a422unless you ask for it to be defined, because a typo would otherwise mint a permanent field visible in every agent’s variable palette.
Behavior Changes
-
metadatano longer has a reserved key.metadata.contactVariablesused to be cut out of what you sent and cut out of what you were shown, because a contact’s field values were stored inside that same object. They are stored separately now, sometadatais once again exactly the document you put there,contactVariablesincluded.Nothing aboutfieldschanges, in either direction. A value still reaches{{contact.<key>}}only by being sent asfieldsand passing catalog validation, and replacingmetadatastill cannot disturb the values you have stored — that guarantee is stronger now, not weaker, because the two are no longer the same column. If you were avoiding the key to work around the old reservation, you no longer need to.
September 3, 2026
Improvements
-
idealAnsweris now optional on a flow question: aloopingquestion needs only itsquestiontext.POST /agentsandPATCH /agents/{id}no longer answer422 LOOPING_QUESTION_INCOMPLETEfor a question that omitsidealAnswer, which makes a question set with no right answers, an internal survey or a satisfaction check, expressible for the first time.idealAnswerwas only ever scoring metadata: it is not spoken, it never reaches the agent’s instructions, and scoring has always skipped a question that has none. Requiring it therefore forced callers to invent an ideal answer for a question that has none, and that invented value then decided whether the question looked scoreable. Nothing changes for a question that sends one, and the scoring rules are unchanged:importanceorweightopts a question into the match score, and a question with noidealAnswercannot be scored at all, so settingimportanceon one has no effect. The same relaxation applies in the visual builder, where the ideal answer field can now be left blank.
September 2, 2026
Improvements
-
Owners invited through the API can now set a password: creating or updating a company with
ownerEmailstill sends the invitation email, and for an address that has no InstaView account yet it now also creates the login identity and sends a separate set your password email from Auth0. Before this, the invitation was the only thing the new owner received. Its link leads to a page that asks them to sign in, and having never signed up anywhere they naturally reached for “Reset password” — which for an address with no account is answered with a success message and no email, deliberately, so that the reset form cannot be used to find out which addresses have accounts. The invitation was valid the whole time and there was no way to tell. Nothing changes in the request or the response, and nothing changes for anownerEmailthat already belongs to an InstaView user: they are assigned asOWNERas before, with no extra email. An address that already has a login but no company gets the invitation only, not a password email it did not ask for. -
The invitation page opens without signing in first:
/invite/acceptnow renders for a visitor with no session, shows which email address the invitation was issued for, and leads with Create an account and accept. Signing in is the second option, for people who already have one. Previously the link redirected straight to the sign-in form, so an invitee with no account saw neither the invitation nor a way to create one. Signing up with the invited address accepts the invitation automatically once the email is verified.