Skip to main content

September 9, 2026

Changed

  • Contact context stopped being a curation chore. contextConfig.useContactContext used 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.
  • includeInContactContext is 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. Sending includeInContactContext on a create or update is now a 400, 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 marked isPii, that decision was migrated to allowPiiInContext and still holds. “By default” is not “always”. A field reaches the agent when it is not readOnly and either is not isPii or has been let past that: company_name, company_description, agent_name and profile are readOnly, describe the caller rather than the person being called, and are in neither the prompt nor the document whatever you set on them.
  • isPii now enforces rather than defaults. A personal field is excluded from the prompt AND from the document, which is what keeps email and phone off every call without anyone deciding. allowPiiInContext is 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. Sending allowPiiInContext on a field that is not isPii is a 400 rather 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}/documents and 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 condition is now validated. It never was. A branch could be stored with a condition the platform could not read — a type outside intent and expression, an intent with no description, an expression with no field — 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 the errors array on Create Agent and Update Agent, each with the path of the offending branch: That last one closes a hole of its own. A null in branches counted toward the branch total and then every per-branch rule skipped it, so the flow validated clean and the failure surfaced later as a 500 while the prompt was being built. It is a 422 naming 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 a 422 now. Everything the API has ever documented for FlowBranchCondition still passes unchanged, EXISTS still needs no value, and logicalOperator is still accepted and ignored. The branch you nominate as defaultBranch is checked like the rest: its condition is not read while it is the fallthrough, but moving defaultBranch later 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 are read:contacts, write:contacts and delete:contacts.
    A contact’s CV shows up on its own. Once a CV has been processed, the contact lists a document with source: "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, the 400 says so in numbers:
  • source on knowledge documents: every document — agent or contact — now carries source, 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.useContactContext enabled. With it on, the contact’s active documents are attached to the call as the candidate knowledge 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-fields returns 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_expectation and availability_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 for company_size, industry and lead_status, which are seeded as string because 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 to includeInContactContext, so an agent with contextConfig.useContactContext on can refer to them without a token in the flow: lead_source, job_title, plan_name, owner_name and last_purchase. Everything else is off, because every field that defaults on lengthens every prompt on every call.
    Superseded on September 9: includeInContactContext is gone and there is no per-field opt-in any more. See that day’s entry.
    organization is the contact’s employer; company_name is 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, so organization now exists for what that author meant.
  • category on a contact field: a new response property on GET /contact-fields naming which panel the field belongs to — identity, organisation, commercial, relationship, scheduling, recruiting, platform or custom. The list is now ordered by category first, then displayOrder, then key, so iterating the response and breaking on a change of category gives you the same panels the dashboard shows. It is set by InstaView and not writable: a field you define is always custom, and a field of yours that shadows a seeded one reports the seeded field’s category, so job_title stays under organisation for everybody even after you relabel it. Because displayOrder now only separates fields inside one category, each category’s numbering restarts at 10 rather than continuing one long sequence. If you were sorting on displayOrder alone, sort on category first.
  • A tags data type: dataType: "tags" stores a string[], 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 as null and "" do, so you never get a key present with an empty array behind it. A number, a boolean or an object is a 422 rather than a one-element list — a caller sending 42 for a tags field has a mapping bug, and a silent ["42"] would be spoken on a call. The seeded tags field uses it. enum is 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_date carries renewal, renews and dátum obnovenia; amount_due carries balance, outstanding and dlžná suma. Nothing consumes them yet. The current CSV importer maps against a fixed list of built-in contact properties and does not read csvAliases, 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 with PATCH /contact-fields/{id} does not touch InstaView’s.
  • fields on the contact resource itself: POST /contacts and PATCH /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} returns fields) while the write sat somewhere else — and POST /contacts dropped any inbound values and always answered fields: {}, 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 one GET /contacts/{id} already returns, so read and write agree. fields merges and metadata replaces, in the same request body, on purpose: metadata is one opaque document you own, fields are individually catalogued values.
    A rejected value takes the whole request with it. Values are validated against the catalog inside the same transaction as the contact write, so an unknown key or a wrong-typed value answers 422 naming it and nothing is applied — not the contact, not the status, not the other values. A contact that exists without the values you sent is a half-write you cannot detect without re-reading.
  • The /contacts/{id}/fields sub-resource is gone. GET returned a strict subset of GET /contacts/{id}, and its write did exactly what PATCH /contacts/{id} with only fields does — same scopes, same guards, same blast radius, since a partial update touches only what you send. One resource, one shape. ?createMissingFields=true moved with it and now hangs off both contact writes. Off by default: a key in fields that your catalog does not know is a 422 unless 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

  • metadata no longer has a reserved key. metadata.contactVariables used 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, so metadata is once again exactly the document you put there, contactVariables included.
    Nothing about fields changes, in either direction. A value still reaches {{contact.<key>}} only by being sent as fields and passing catalog validation, and replacing metadata still 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

  • idealAnswer is now optional on a flow question: a looping question needs only its question text. POST /agents and PATCH /agents/{id} no longer answer 422 LOOPING_QUESTION_INCOMPLETE for a question that omits idealAnswer, which makes a question set with no right answers, an internal survey or a satisfaction check, expressible for the first time.
    idealAnswer was 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: importance or weight opts a question into the match score, and a question with no idealAnswer cannot be scored at all, so setting importance on 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 ownerEmail still 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 an ownerEmail that already belongs to an InstaView user: they are assigned as OWNER as 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/accept now 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.