Changelog
Dated changes to the public /v1 contract, newest first. This page is the
record — marketing pages are not the contract, and neither is a blog post.
Additive changes (a new field, endpoint, event type or enum value) ship on
/v1 without notice, so parse defensively. A breaking change ships under a
new path prefix or after a deprecation window; see
Versioning for which is which.
The page is generated from the changelog the API is developed against, so an entry lands in the same change as the behaviour it describes.
2026-11-04 — action.form_submitted webhook
- Additive: a new webhook type,
action.form_submitted(data.object=action_form_submission, field names only, never values), fired when a visitor submits acollect_infoform whose webhook destination is on. Dormant untilACTION_UI_WIDGETS_ENABLEDis on (Events). - Spec only:
POST /v1/chatactions[]items are documented as a plain open object. The old document carried an inferredhandoffvariant inside ananyOfwith an open object, which already admitted any entry; no response changed.
2026-11-03 — Chat webhook destinations on handoff / notify actions
- Additive:
POST /v1/actionsandPATCH /v1/actions/{id}acceptconfig.targets[].typeslack_webhook,teams_webhook(Microsoft Teams Workflows) anddiscord_webhookbesideemail, each with anid(^[A-Za-z0-9-]{8,64}$) and an optionallabel(≤ 80). The webhook URL goes only incredentials.webhook_url_<id>, is checked against the vendor's host (422 otherwise) and is never returned. A create needs every webhook destination's URL.config.targets[].typeis now an enum of the supported types: an unknown type, which was stored and silently skipped, is 422.
2026-10-30 — Widget settings bag is partial
- Spec correction:
PUT /v1/chatbots/{id}/widgetalready accepted a partialsettingsobject. The committed OpenAPI document listed every widget key as required because Scramble treated Laravel'sarray:k1,k2allowlist as a required-key object. The bag rule is nowAllowedKeys, andUpdateWidgetSettingsRequest.settingshas norequiredlist. Callers that sent one key keep working; generated clients no longer demand the rest.
2026-10-30 — Crawl and knowledge error codes are listed
- Crawl create:
POST /v1/crawlersandPOST /v1/sitemap_crawlersdocument the 422 crawl familyBLOCKED_DOMAIN,CHALLENGE_BLOCKED,CONTENT_TOO_LARGE,CORRUPT_CONTENT_ENCODING,CRAWLING_ERROR,INVALID_SITEMAP,ROBOTS_BLOCKED,UNSUPPORTED_CONTENT_ENCODING,UNSUPPORTED_CONTENT_TYPEandUPSTREAM_HTTP_ERROR, besideVALIDATION_FAILED. - URL check:
POST /v1/crawlers/validate_urlreturns 200.error_codeisnullwhen the URL answers, otherwisehttp_errororunreachable. - Knowledge writes:
PATCH /v1/knowledge/{id}can answer 409INGESTION_IN_PROGRESSorKNOWLEDGE_NAME_TAKEN.POST /v1/knowledge/{id}/replace_filecan answer 422TEXT_EXTRACTION_FAILED.POST /v1/knowledge/import/cloudcan answer 404IMPORT_PROVIDER_UNAVAILABLE, 409KNOWLEDGE_NAME_TAKEN, or 422CLOUD_IMPORT_FAILEDorCOLLECTION_NOT_OWNED_BY_CHATBOT. - Behaviour: on those three knowledge operations, a dashboard code outside
the published set is returned as the code for its HTTP status (
CONFLICTfor 409,VALIDATION_FAILEDfor 422, and the same for the other statuses) instead of an undocumented string. Codes these operations already return are unchanged, including replace-file'sIDEMPOTENCY_IN_FLIGHT,IDEMPOTENCY_KEY_REUSEDandINVALID_IDEMPOTENCY_KEY.
2026-10-30 — Self-revoke does not repeat a revoke that already landed
DELETE /v1/api_keys/{id}emitsapi_key.revokedonce. If another revoke wins before this call's row lock, the response is still 200{ "object": "api_key", "id", "deleted": true }— the same body as a revoke this call performed — and no secondapi_key.revokedevent or owner email is sent. The first revoke's time, and who revoked the key, stay as that first revoke wrote them. A later request that presents the revoked secret is still 401. Behaviour change, no response-shape change.
2026-10-28 — Hosted agent links use /{handle}-chatbot
- Share URL: a dashboard prints a hosted agent's link as
{site}/{handle}-chatbot(for examplehttps://agentency.com/daftra-bot-chatbot). A handle with no session answers 307 to a fresh/{handle}-chatbot/{uuid}. Existing/agent/{handle}/{uuid}and/bot/{handle}/{uuid}links 308 there and keep working. No/v1request or response field changes.NEXT_PUBLIC_AGENT_ROOT_LINKS=falseis the emergency off-switch (rebuild): builders print/agent/…again, and a root URL already shared keeps being served.
2026-10-28 — Default launcher glyph is now sparkles
- Rendering default: a chatbot whose
settings.launcher_iconisnullor absent now shows thesparklesglyph on its closed launcher instead of thechatglyph. Stored values and the request / response shape are unchanged; setlauncher_icontochatto keep the previous look.
2026-10-28 — Agent status on dashboard analytics top chatbots
- Additive response fields: each
breakdowns.top_chatbots[]row ofGET /v1/analytics/dashboardgainsstatus(active,inactive,draft) andtraining_status(never_trained,pending,training,updating,needs_retraining,completed,failed). Both are strings, always present, and carry the same values as the chatbot object's fields of the same name. The rows still list only your own chatbots; nothing else in the response changes.
2026-10-27 — Launcher and bubble styling keys in chatbot settings
- Additive request fields:
POST /v1/chatbots,PATCH /v1/chatbots/{id}andPUT /v1/chatbots/{id}/widgetaccept eight nullablesettingskeys:launcher_coloranduser_bubble_color(a theme catalog flat token or a custom#rrggbb),dark_accent_color(a custom#rrggbb),bubble_style(sharp,soft,round),launcher_shape(circle,rounded),launcher_icon(chat,message,sparkles,help,headset,bot,custom),launcher_icon_path(thepathof a PNG, JPEG or WebP image your account uploaded as an avatar) andlauncher_label(plain text, at most 30 characters). Hex colours are normalised to lowercase#rrggbb(#RGB, upper case and a missing#are accepted). Invalid values, and an image uploaded by another account, return 422VALIDATION_FAILEDon thesettings.<key>field. - Additive response fields: the chatbot and widget-settings objects echo
the stored keys under
settings. All of them are cosmetic: setting them never retrains the chatbot or clears its answer cache.
2026-10-27 — Custom hex chat theme colours
- Additive grammar:
POST /v1/chatbotsandPATCH /v1/chatbots/{id}accept a custom colour wherever a theme catalog token was accepted.colortakes a catalog token or#rrggbb.gradient_styletakes a catalog token or a two-stop#rrggbb,#rrggbbpair. Every catalog token is still accepted. - Normalisation: hex input is stored in lower case.
#RGBis expanded and a missing#is added, soFC0is stored as#ffcc00. The response echoes the stored form. Alpha hex (#rrggbbaa,#rgba) is 422VALIDATION_FAILEDon the field. - Readability: any single colour is accepted, because the chat
surfaces pick white,
#1f2937or black text automatically to reach WCAG AA 4.5:1. A gradient pair is refused with 422VALIDATION_FAILEDongradient_stylewhen none of those three text colours reaches 4.5:1 on both stops. Only a pair that joins a dark stop to a light one can fail. - Spec: the two request fields are now
anyOfthe catalogenumand the hexpattern(openapi.json). The generated TypeScript type widens tostring. Responses are unchanged in shape:color/gradient_stylemay now hold the hex forms.
2026-10-27 — More header colours, light ones included
- Additive enum growth. The chatbot
colorenum gains 16 members (bg-white,bg-gray-700,bg-gray-900,bg-neutral-950,bg-amber-900,bg-stone-600,bg-red-800,bg-pink-400,bg-blue-900,bg-sky-400,bg-cyan-400,bg-teal-700,bg-teal-400,bg-lime-400,bg-yellow-400,bg-amber-400), andgradient_stylegains 12 (from-blue-900 to-blue-700,from-slate-900 to-indigo-900,from-neutral-800 to-black,from-red-900 to-rose-800,from-fuchsia-600 to-purple-700,from-amber-900 to-orange-900,from-yellow-300 to-amber-400,from-lime-300 to-green-400,from-cyan-300 to-teal-300,from-sky-200 to-sky-400,from-orange-200 to-rose-300,from-white to-slate-100). Every existing value is still accepted; clients that switch exhaustively on either enum should add the new members. The enum lists the dashboard's swatches first, so sixcolorvalues that were already accepted (bg-green-500,bg-orange-500,bg-blue-600,bg-red-600,bg-pink-600,bg-fuchsia-600) moved up the list. - Text colour. Light swatches get dark header text (
#1f2937), dark ones white; every swatch clears WCAG AA (4.5:1) on both gradient stops.
2026-10-26 — Website embed is its own plan entitlement
- Additive field: each plan's
entitlementsinGET /v1/billing/plansnow carrieswebsite_embed_enabled(boolean). It says whether the plan may put an agent on a website: the widget snippet, the iframe embed and the platform plugins. Every plan has it, Free included.integrations_enablednow covers messaging channels only. - Behaviour:
POST /v1/plugins/installnow succeeds on the Free plan. A plan withoutwebsite_embed_enabledgets 402WEBSITE_EMBED_NOT_AVAILABLE(details.resource: "website_embed") where it used to getINTEGRATIONS_NOT_AVAILABLE; no plan in the catalog lacks it today. Connecting a messaging channel (POST /v1/chatbots/{id}/channels/{platform}) on Free is still 402INTEGRATIONS_NOT_AVAILABLE.
2026-10-25 — Each error code has a one-line description
Documentation only: no request or response on the wire changed.
Every x-agentency-error-codes row now carries description, one sentence
saying what that error.code means. The Errors guide table shows it.
2026-10-25 — Chatbot name search matches Arabic spellings
- Behaviour change, no new parameters.
GET /v1/chatbots?search=is still a substring match on the chatbot name, and it now also matches the interchangeable Arabic spellings of the term (hamza-carrier alef ا/أ/إ/آ, a final ه/ة, a final ي/ى). A literal%or_in the term stays escaped. The list stays limited to the API key's account. The same expansion is on the dashboard chatbot list.
2026-10-25 — A second channel connect answers busy
- Behaviour:
POST /v1/chatbots/{id}/channels/{platform}no longer waits while another connect for the same chatbot is still running. It answers 409CHANNEL_CONNECT_BUSYat once. Nothing is written, no platform is called, nochannel.connectedevent fires, and the refusal is not stored forIdempotency-Keyreplay — the same key succeeds once the first connect has finished. A connect for a different chatbot is unaffected. Another account's chatbot stays 404NOT_FOUND.
2026-10-23 — Cloud import list items are typed
Documentation only: no request or response on the wire changed.
POST /knowledge/import/cloud already returned the knowledge dataset
object (the same one as GET /knowledge/{id}) and a top-level
failures list. The OpenAPI document now types data[] as
KnowledgeDatasetResource and documents failures (name and
reason, each a string or null). Generated clients see the narrower
type.
2026-10-21 — The OpenAPI document describes what the API returns
Documentation only: no request or response on the wire changed.
openapi.json (and GET /v1/openapi.json) now:
- types every success body with its resource schema (
ChatbotResource,KnowledgeDatasetResource, …) instead of a bare object or array, types listdata[]items, and declares 201 on every create that answers 201; - lists every error status an operation can answer with its
error.codevalues: the handler's own codes plus those of the platform (401UNAUTHENTICATED, 403INSUFFICIENT_SCOPE/ACCOUNT_SUSPENDED, 429RATE_LIMITED, 500SERVER_ERROR), the idempotency gate (409IDEMPOTENCY_IN_FLIGHT, 422IDEMPOTENCY_KEY_REUSED), the billing gates (402 onPOST /chatand/chat/stream) and plan gates. Every error body is the envelope (error.code,request_id); a sharedErrorschema describes it; - declares the
Idempotency-Keyheader andIdempotency-Replayedresponse header on gated operations, thelimit/starting_after/ending_before/created[gte|lte]parameters on paginated lists, andchatbot_idonGET /knowledgeandGET /customers/summary; - declares HTTP Basic (
basicAuth, the key as username) besidebearerAuth; - carries
x-agentency-rate-limits,x-agentency-error-codesandx-agentency-webhook-deliverycatalogues.
Generated clients see new, narrower types; the TypeScript SDK's
src/openapi.ts is regenerated in the same change.
2026-10-21 — Plugin install parity and plugin-claimed allowlist hosts
- Behaviour:
POST /v1/plugins/installnow runs the same lifecycle as the dashboard/plugins/install: a plan without integrations is 402INTEGRATIONS_NOT_AVAILABLE;site_urlis at most 255 characters (422VALIDATION_FAILED, was a 500 above that); anhttp://site is 422PLUGIN_SITE_NOT_HTTPS(the widget only runs on HTTPS origins); the host is normalized, sohttps://Shop.Example.COM./andhttps://shop.example.comare one installation; the per-chatbot registry cap is 422PLUGIN_INSTALLATION_LIMIT_REACHED. - Behaviour:
plugin.installedis emitted once per activation (first connect or reconnect of a disconnected site), not on every install retry;plugin.uninstalledonce per disconnect. The dashboard surface now emits both events too. - Behaviour:
DELETE /v1/chatbots/{id}/widget/domains/{host}refuses a host an active plugin installation claims: 422WIDGET_DOMAIN_PLUGIN_MANAGED,error.details={host, installation_id, platform}. Uninstall the installation (DELETE /v1/plugins/{id}) to release it.
2026-10-21 — Middleware order, budgets and openapi.json serving
- Behaviour (kill switch): while the API is disabled every
/v1route answers 404FEATURE_DISABLEDbefore any key is looked up — a valid key and an invalid one now get the same answer (a valid key used to get 404 and an invalid one 401). - Behaviour (headers): 401
UNAUTHENTICATEDand the burst / auth-failure 429RATE_LIMITEDnow carryAgentency-Version. - Behaviour (budgets): the write and chat budgets are charged only after
the scope check — a key without the route's scope gets 403
INSUFFICIENT_SCOPEwithout spending them. Each minute budget (general, write, chat) now also applies per account and mode: all live keys of an account share one ceiling, test keys another. A key'srate_limit_per_minutecan only lower its general budget; it no longer raises any budget and no longer applies to the write / chat budgets. - Behaviour (auth-failure brake): only a missing credential, an unknown key or a wrong secret counts towards the per-IP 401 budget; a revoked, expired or IP-blocked key no longer does, and a valid key is never refused by it.
- Behaviour (unauthenticated routes):
GET /v1/statusandGET /v1/openapi.jsongain a per-IP minute budget (default 120, 429RATE_LIMITED). - Fix (
GET /v1/openapi.json): serves the committed document byte for byte (empty JSON objects such as"items": {}were served as[]), with a strongETag,Cache-Control: public, max-age=300and304 Not Modifiedon a matchingIf-None-Match. The spec now types its 200 as an object. - Behaviour (keys): keys minted without an expiry use the deployment default (0 keeps them non-expiring).
2026-10-21 — Resource-layer fixes: status families, grants, dedupe, budgets
- Breaking (status only, codes unchanged). One status per family
(errors § Status families): state conflicts are
409 —
NOTHING_TO_CANCEL(POST /knowledge/{id}/cancel),COLLECTION_HAS_DATASETS(DELETE /knowledge/collections/{id}) andDATASET_NOT_UPLOAD_SOURCED(POST /knowledge/{id}/replace_file) were 422. Feature-off is 404 —MCP_NOT_ENABLED(POST /actions/mcp/discover) was 422. Switch onerror.code, which did not change. - Breaking (behaviour).
POST /conversations/{id}/reviewtakesreviewed: true|false(defaulttrue) and sets that state; it no longer toggles, so a retry cannot flip it back. A body withoutreviewedon an already-reviewed conversation now leaves it reviewed — sendreviewed: falseto reopen it. - Behaviour (fix).
POST /customersanswers 200 with the existing customer and emitscustomer.updatedwhen the contact (external_id→email→phone, per chatbot) matches one; only a new customer is 201 +customer.created. - Behaviour (fix, grants).
DELETE /knowledge/collections/{id}honours the key's agent grants: a key restricted to selected agents gets 404 for another agent's collection (it could delete any collection of the account). - Behaviour (fix, grants). When workspace context is on, a key restricted to
selected agents is refused 403
FORBIDDEN(details.reason=key_restricted_to_selected_agents) on everywebhook_endpoints,webhook_deliveriesandeventsroute — those streams carry every agent's activity.GET /event_typesstays open; unrestricted keys are unchanged. - Behaviour (fix).
GET /knowledgeandGET /customers/summaryvalidatechatbot_id(422VALIDATION_FAILEDforabc,0, …; it used to cast to 0 and answer an empty 200) and answer 404 for a chatbot the key cannot reach. Both parameters are now in the spec. - Behaviour (fix).
POST /crawlers/{id}/retry,POST /sitemap_crawlers/{id}/retryandPOST /sitemap_crawlers/{id}/scancount the concurrent-crawl slots like create and resume: at the cap they are 429CRAWLER_LIMIT_REACHEDand the crawler keeps its state (the refusal is not cached under theIdempotency-Key). - Behaviour (fix). Channel disconnect / verify of a stored channel whose platform
connector is switched off answer 404
FEATURE_DISABLEDinstead of a 500. - Additive.
POST /searchandGET /search/similar/{id}ride a per-account budgetapi_v1_search(120/min, shared by every key of the owner) on top of the write budget → 429RATE_LIMITED. - Additive.
POST /knowledge/import/cloudreturns the/v1dataset objects (nouser_id/workspace_id) and a top-levelfailureslist ({name, reason}per file not imported); nothing imported stays 422CLOUD_IMPORT_FAILEDwithdetails.failures. - Additive.
GET/PATCH /me/profileadd the read-onlyphone_verified.phoneis display-only text and never the verified signup number. - Validation. Customer
extraand action-testarguments: at most 50 keys, 5 levels deep, 16 KB JSON-encoded; knowledgetags: at most 50. Larger is 422. - Errors. Translated dashboard errors (knowledge edit / replace-file / cloud
import) keep their
errorsfield bag,retry_afterandRetry-After, and an error without a code gets the code of its status instead ofVALIDATION_FAILED(errors § Translated dashboard errors). - Fixes, no contract change.
POST /chat/streamfrees itsIdempotency-Keywhen the turn fails before streaming (an honest retry was 409IDEMPOTENCY_IN_FLIGHTfor up to 5 minutes); a failedPOST /filesbatch deletes the blobs it already wrote;DELETE /conversations/{id}also deletes the conversation's Call Action run logs and still emitsconversation.ended(there is noconversation.deleted). - Docs.
POST /actions/{id}/teststates that it is not idempotency-gated and that every retry with amessageruns and bills another completion.
2026-10-21 — Idempotency keys are per API key; secrets are never replayed
- Behaviour: an
Idempotency-Keybelongs to the API key that sent it. Two keys of one account may use the same value without replaying, blocking or 422-ing each other; a rotated successor starts with an empty key space. A retry that spans this release runs again instead of replaying. - Behaviour:
POST /v1/filescounts the uploaded bytes: the same key with a different file is 422IDEMPOTENCY_KEY_REUSED, not a replay of the first upload. - Behaviour: a replay of
POST /v1/webhook_endpointsorPOST /v1/webhook_endpoints/{id}/rotate_secretomitssecretand addssecret_already_shown: true. The signing secret is shown once; a client that lost the first response callsrotate_secretagain with a new key. - Behaviour: a key rotated out from the dashboard and used after its
grace window is logged as
KEY_ROTATION_GRACE_ENDEDinGET /v1/request_logs(it was loggedKEY_REVOKED). The response is still 401UNAUTHENTICATED.
2026-10-21 — Webhook delivery safety and event recording
- Behaviour (breaking for plain-http test endpoints): every webhook
endpoint URL must be HTTPS, whatever its
mode.POST/PATCH /v1/webhook_endpointsanswer 422HTTPS_REQUIREDfor anhttp://URL intestmode too, and an existingtestendpoint on plain HTTP now fails each delivery withHTTPS_REQUIRED(and is auto-disabled after 20 in a row).modeis a label: both modes receive the same events. The only exception is a loopback URL on a local development deployment (production refuses to boot when plain HTTP is allowed). - Behaviour:
POST /v1/webhook_deliveries/{id}/retryre-queues only afaileddelivery. Adeliveredone answers 409DELIVERY_ALREADY_DELIVEREDand is not re-sent;pending/processingkeep 409DELIVERY_IN_FLIGHT. - Behaviour:
POST /v1/webhook_endpoints/{id}/testandPOST /v1/webhook_deliveries/{id}/retryshare a per-key budget (20 per minute by default) on top of the write budget; past it they answer 429RATE_LIMITED. A key's ownrate_limit_per_minutedoes not raise it. - Behaviour: events (
GET /v1/events) are recorded only for an account that has an enabled webhook endpoint or an active API key. An account with neither records nothing, soGET /v1/eventson a key minted later starts from that key's creation. Targeted test pings always record. - Behaviour (fix): rotating a signing secret again inside the 24-hour overlap keeps the oldest unexpired previous secret (and its original expiry) and replaces only the current one. Before, the second rotation stopped the original secret immediately. The rotate description no longer claims previous signatures stop at once.
- Behaviour (fix): due retries are delivered within about a minute even when nothing else happens on the account (a per-minute drain tick); a receiver body that is not UTF-8 or is very large no longer stalls other deliveries. Response snippets are the first 512 characters, scrubbed to valid UTF-8.
2026-10-21 — File batches are capped by total size; an oversized body is a JSON 413
- Behaviour (fix):
POST /v1/filesrefuses afiles[]batch whose total size exceeds the deployment's total upload ceiling (90 MB by default — the dashboard upload's ceiling) with 422VALIDATION_FAILEDonfiles; nothing is stored and the refusal is not cached under theIdempotency-Key. Before,max_files×max_file_mb(200 MB) passed validation but could never arrive: the edge cut the body first. - Behaviour (fix): a request body over the API host's
110 MB is answered by nginx with the JSON
envelope Laravel uses for 413 (
error.codeHTTP_413), not an HTML page; that body carries norequest_id. Cloudflare's own edge cap is unchanged.
2026-10-19 — Chatbot names are unique per workspace
- Behaviour:
POST /v1/chatbotsandPATCH /v1/chatbots/{id}answer 409CHATBOT_NAME_TAKENonly when another chatbot in the key's workspace already has the name (plus the account's chatbots no workspace is stamped on yet). A name the account uses in another of its workspaces no longer collides. Accounts with a single workspace see no change. The code and the response shape are unchanged.
2026-10-16 — Conversations list exposes the review status
- Additive.
GET /v1/conversations(one chatbot or the account stream) acceptsreview_status_id. A custom status matches explicit assignments; a system status also matches legacy rows whoseis_reviewedresolves to it (the same effective-status rule as the dashboard). An id outside the account catalogue is 422VALIDATION_FAILEDonreview_status_id. Each conversation object, on the list and on show, gainsreview_status: {id, name, color, counts_as_reviewed}.is_reviewedis unchanged.
2026-10-11 — Crawler resume respects the concurrent-crawl cap
- Behaviour (fix):
POST /v1/crawlers/{id}/resumeandPOST /v1/sitemap_crawlers/{id}/resumenow count the account's concurrent-crawl slots the same way the create endpoints do. A paused crawler holds no slot, so pausing, creating new crawlers and then resuming could run past the account's concurrent website-crawl and sitemap-crawl caps. At the cap, resume answers 429CRAWLER_LIMIT_REACHED(the create endpoints' existing code and envelope) and the crawler stayspaused; the refusal is not cached under theIdempotency-Key, so the same key succeeds once a slot frees. The 404 / 409 answers are unchanged.
2026-10-11 — Channel connect enforces the per-chatbot cap
- Behaviour (fix):
POST /v1/chatbots/{id}/channels/{platform}enforces the per-chatbot channel cap (default 3) like the dashboard connect flows. Connecting a platform the chatbot does not yet hold while it already has that manyactiveorpendingchannels is 402CHANNEL_LIMIT_REACHED(error.details.max_connected); no row is written, no vendor call is made, nochannel.connectedevent fires, and the 402 is not cached under theIdempotency-Key. Reconnecting a platform the chatbot already holds never takes a new slot. The code was documented in Errors but never returned: the endpoint connected past the cap. The cap check and the connect now run under a per-chatbot lock, so two concurrent connects cannot both pass it. The operation's 402 (CHANNEL_LIMIT_REACHED|INTEGRATIONS_NOT_AVAILABLE) is now in the spec.
2026-10-10 — Customer export requires customers:write
- Security:
GET /v1/customers/exportrequirescustomers:write. A key with onlycustomers:readis 403INSUFFICIENT_SCOPE(error.details.required=["customers:write"]).customers:writealready impliescustomers:readand is the scope whose workspace permissions includecontacts.export, the same permission the dashboard export requires. The route stays on the per-keyapi_v1limiter. List, summary, show and timeline stay oncustomers:read. There is noPOST /v1/customers/export.
2026-10-09 — Chatbot readiness reports a trained bot as ready
- Behaviour (fix):
GET /v1/chatbots/{id}/readinessanswersready: truefor anactivechatbot whosetraining_statusiscompleted. It comparedtraining_statuswithtrained/ready— values the column never holds — soreadywas alwaysfalse. Every other field, status and code is unchanged.
2026-10-09 — Customer export row cap and audit
- Behaviour:
GET /v1/customers/exportcounts the filtered set first and answers 422EXPORT_TOO_LARGE(detailslimit=rows,rows,max_rows; documented in the spec) instead of streaming a file over the export row cap (50 000). Every export is now audit-logged (filter names and the key id only) and, when scoped to one chatbot, adds acustomer_exportedtimeline event. The CSV columns, thecustomers:readscope and the per-keyapi_v1budget are unchanged; the dashboard's JSON / PDF and row-selection params are not part of/v1.
2026-10-08 — Legacy ?key= hosted-agent share links end on 2027-04-01
- Deprecation (outside
/v1). Recorded here because shared links outlive integrations. A protected agent's share link carries its access key in the fragment (/agent/{handle}#key=…). Links in the older/agent/{handle}?key=…form keep opening the agent until 2027-04-01 00:00 UTC; from then on they answer 410 with a "This link has expired" page and start no conversation.#key=links are unaffected. Copy a fresh link from Share your agent in the dashboard and replace any old ones you have published.
2026-10-08 — Webhook endpoint create / update serialise per account
- Behaviour (fix):
POST /v1/webhook_endpointsandPATCH /v1/webhook_endpoints/{id}check the account cap and the duplicate URL under the account's row lock, so two concurrent requests for one URL now yield one 201 / 200 and one 409ENDPOINT_URL_ALREADY_REGISTERED, and concurrent creates can no longer exceed the account's endpoint cap. Codes, statuses and bodies are unchanged.
2026-10-08 — Chatbot rename enforces the per-account name
- Behaviour:
PATCH /v1/chatbots/{id}answers 409CHATBOT_NAME_TAKEN(nothing written) whennamechanges to the name of another of the account's chatbots — the rulePOST /v1/chatbotsalready applied. Sending a chatbot's current name unchanged is never refused, even when an older duplicate exists. Another account's chatbot names never collide. - Spec: the 409
CHATBOT_NAME_TAKENresponse is now declared on bothPOST /v1/chatbotsandPATCH /v1/chatbots/{id}inopenapi.json.
2026-10-08 — Customer date filters validate strict ISO-8601
- Behaviour (fix, backward compatible):
date_from/date_toonGET /v1/customersandGET /v1/customers/exportaccept aY-m-dday or an RFC 3339 date-time (2026-10-01T14:30:00Z,2026-10-01T14:30:00.250+03:00) — the advertiseddate-timeformat — and answer 422VALIDATION_FAILEDon the field for anything else. Free text the former lenient rule let through (next tuesday,yesterday,@1700000000), an overflowed day (2026-02-31) or an instant without seconds or offset is now refused instead of being silently ignored. An instant still filters by the UTC calendar day it falls on. - Spec: both parameters now carry a description and an example; the
schema (
string,date-time, nullable) is unchanged.
2026-10-08 — Invoice pages are briefly cached
- Behaviour:
GET /v1/billing/invoicesserves each page (per account,limitand cursor) from a short cache (60 seconds by default, the dashboard history's TTL), so a new invoice can take up to that long to appear. Request, response shape and cursors are unchanged; only a successful page is cached.
2026-10-07 — Collection add-dataset asks the plan knowledge quota
- Behaviour:
POST /v1/knowledge/collections/{id}/datasetsanswers 402KNOWLEDGE_PLAN_SIZE_EXCEEDED(nothing written) when adopting an unowned dataset into a chatbot's collection would put that chatbot over its plan knowledge size — the check the dashboard route already made (billing review M-U13). Moving a dataset the chatbot already owns is not counted again. The 402 is not replayed for the sameIdempotency-Key.
2026-10-07 — Scheduled downgrades on the subscription snapshot
- Additive:
GET /v1/billing/subscriptiongainspending_change(nullor{plan, interval, effective_at}— the plan the subscription moves to at the end of the current period) andpending_addon_changes(a list of{addon, quantity, effective_at}— recurring add-ons whose quantity drops at period end;[]when none). Same keys on the dashboard snapshot.
2026-10-07 — Billing snapshot: has_payment_issue, honest plan fields
- Additive:
GET /v1/billing/subscriptioncarrieshas_payment_issue: boolean— true while the Stripe subscription ispast_due,unpaid,incompleteorpaused, even when a complimentary plan is in effect. Every existing key is unchanged. - Behaviour: the plan in effect is the higher of the paid plan and an
operator comp (a comp no longer lowers what a customer pays for);
intervalis null unless the paid subscription is the plan in effect;is_subscribedis false for apausedsubscription and for one on a price outside the catalog (which resolves to Free); a trial cancelled before it converts reportsgrace/canceled, nottrialing. - Behaviour: every
/v1/billing/*read uses the same billable as the dashboard — with billing on the workspace, the workspace's customer and credits.GET /v1/billing/invoicesfor a Stripe customer the billing account no longer knows is an empty list (was 500SERVER_ERROR).credits.usedonGET /v1/usage/GET /v1/billing/usagecounts not-yet-flushed credits likecredits.remaining, soused + remaining = limit + balance.
2026-10-06 — API-key resource grants enforced
- A key restricted to selected agents is enforced on every route
while workspace context is on:
rows bound to an agent it does not reach — or reaches below the level
the route's scope needs — are 404
NOT_FOUND/ absent from lists, search covers only its agents,GET /request_logslists only its own calls, and a create it holds the manage level for on no agent is 403FORBIDDEN. Expired or revoked grants never widen it back to every agent. A key on a workspace pending deletion, or stamped with a workspace its owner does not own, is 401UNAUTHENTICATED. - A restricted key reaches the agent it creates —
POST /v1/chatbotsgrants itadminon the new agent — and rotating a restricted key keeps the restriction: the successor inherits its reach and grants instead of reaching every agent. - No response shape changed; a key without grants (every key until an owner restricts one) is byte-for-byte unchanged.
2026-10-04 — chatbot audience / knowledge_mode; verdict general
- Two optional fields on
POST /v1/chatbotsandPATCH /v1/chatbots/{id}, echoed by every chatbot body (GET /v1/chatbots,GET /v1/chatbots/{id}and the write responses, next toresponse_length):audience(customers|personal— who the agent serves;personalis the owner's private assistant; a create request that omitslead_capturestores it disabled, andenable_handoffkeeps its off default) andknowledge_mode(strict|general—strictanswers from the knowledge base only,generalmay answer from general knowledge when the knowledge base is silent, flagged as such; business-specific facts stay knowledge-only in both). Omitted → the server defaultscustomers+strict, the pre-existing behaviour, so a client that never heard of them is unchanged; any other value is 422VALIDATION_FAILED. The dashboard requires both at create time;/v1does not. Additive; both are response-affecting (a change invalidates the chatbot's cached answers). - Additive enum growth:
verdictgainsgeneralonMessage/ChatTurn(POST /v1/chat,GET /v1/messages/{id},GET /v1/conversations/{id}/messages) and on theevent: verdictframe ofPOST /v1/chat/stream— ageneralknowledge-mode chatbot answered from general knowledge because its knowledge base did not cover the question. It maps to conversationstatusanswered(not a refusal) and is never served from the answer cache. Astrictchatbot never emits it. Clients that switch exhaustively onverdictshould add the new member.
2026-10-03 — search on conversations requires every word
- Behaviour change, no new parameters.
GET /v1/conversations?search=(account-wide or narrowed to one chatbot) now returns conversations with ONE visitor message containing every word of the term, in any order, with Arabic spelling variants (final ه/ة, final ي/ى, hamza-carrier alef ا/أ/إ/آ) matching each other. The previous natural-language match on the ngram index returned any conversation sharing a single two-letter fragment with the term (an Arabic term matched every Arabic conversation) and could drop real hits.
2026-10-01 — confidence_score is the grounded answer confidence
- Description-only change, no new fields.
Message.confidence_scoreandChatTurn.confidence_scoreonPOST /v1/chat,GET /v1/messages/{id}andGET /v1/conversations/{id}/messageskeep their type and range (0–100) but their meaning for afull/partialreply is now the grounded model (the default): the evidence strength (retrieval_confidence, now the rank-weighted relevance of the retrieved chunks rather than their plain mean) fused in log-odds space with how much of each sentence of the reply one source supports.error→ 0,none≤ 10 and the conversational verdicts are unchanged. Thecalibratedrollback restores the previous meaning exactly.
2026-10-01 — list filters applied, cursors bound to their list, strict created[]
Correctness fixes to the /v1 list contract
(Pagination). Every item below is behaviour the spec
already described and the server did not deliver.
GET /conversationsfilters are applied.status,date_from,date_toandsearchwere validated and documented but never reached the query, so?status=not_answeredreturned every conversation. They now filter (status∈answered·not_answered·partial_answer; days are UTC onstarted_at,date_toinclusive;searchmatches visitor messages). Stricter validation:statusoutside that list (including the dashboard'sall), a day not spelledYYYY-MM-DD, ordate_tobeforedate_fromis now 422VALIDATION_FAILED(was accepted and ignored). The account-wide stream (nochatbot_id) resolves the owner's chatbot ids first and lists bychatbot_id IN (…).GET /customerssorts.sort=id,sort=-created_at,sort=+last_seen_atandorderare honoured through the shared list syntax. The request no longer validatessortagainst the dashboard's offset-page columns (the documentedidwas a 422) nor acceptsdirection; both are now ignored like any unknown parameter, and asortoutsideid/created_at/last_seen_atfalls back toid.created[gte]/created[lte]are strict.YYYY-MM-DDor an RFC 3339 timestamp with a zone, UTC; a date-onlylteis the end of that day (it used to be 00:00:00, excluding the day the docs called inclusive). Anything else is 422INVALID_PARAMETER(details.parameter) instead of a silently unfiltered 200. New code; the catalog row is in Errors.- Cursors are bound to their list.
next_cursor/previous_cursornow carry a marker of the endpoint and the effective sort order; a cursor replayed under anothersort, withorderflipped, or on another endpoint is 422INVALID_CURSOR(it paged the wrong list before). Cursors minted before this release no longer verify — a client holding one restarts from the first page; cursors are short-lived paging state, never stored ids. - Every list carries the full envelope.
GET /event_types,GET /actions/types,POST /actions/mcp/discover,GET /billing/plans,GET /chatbots/{id}/channels, thePOST /filesbatch response andPOST /knowledge/import/cloudnow includeprevious_cursor: null(additive). The spec documents their item shapes. GET /billing/invoicespages.limit/starting_after/ending_beforework through Cashier's Stripe cursor andhas_moreis real; the list includes open / draft invoices (each row carriesstatus) where it previously dropped them client-side and capped at Stripe's first 24 withhas_more: false. Rows areApi\V1\InvoiceResource(object,id,total,status— unchanged keys).- List
statusfilters are validated.GET /chatbots(status,training_status,search≤ 100 chars),GET /crawlers,GET /webhook_endpointsandGET /webhook_endpoints/{id}/deliveriesanswer 422VALIDATION_FAILEDfor a value outside the owning enum (a typo used to return an empty page with a 200);?status=is still "no filter". DELETE /files/{id}refuses a file that backs a dataset with 409FILE_BACKS_DATASET(details.dataset_id) — parity with the dashboard. It used to delete the dataset's source blob out from under it.- Docs: the
GET /chatbotsdescription no longer advertisesexpand[], which never existed.
2026-09-30 — plan catalog without price ids; dunning + trial events
GET /v1/billing/plansitems are shaped byApi\V1\PlanResource. Each item is{object: "plan", key, name, is_popular, is_free, contact_sales, price: {monthly, yearly}, purchasable_intervals, entitlements, features}. Breaking for clients that read the previously serialised raw catalog object: the camelCase fields (isPopular,contactSales,priceMonthly,priceYearly,stripePriceMonthly,stripePriceYearly) are gone — the two Stripe price ids were a leak, never a contract. Select a plan bykey;purchasable_intervals(monthly/yearly) says which intervals a checkout accepts.- Two new event types (additive enum growth in
enabled_eventsand thewebhooksobject):billing.payment.failed(a renewal or first charge was declined;data.objectcarriesinvoice,subscription,attempt_count,next_payment_attempt) andbilling.trial.ending(Stripe's three-day trial reminder;trial_ends_at). Both are emitted once per Stripe event (Events).
2026-09-29 — a login lockout no longer refuses the owner's keys
- 423
ACCOUNT_LOCKEDis gone. A dashboard login lockout (ten wrong passwords — an unauthenticated action) gates signing in only. A locked owner's API keys keep working. Before, anyone who knew the owner's email could take every integration offline for 15 minutes, repeatably. 403ACCOUNT_SUSPENDED(suspended / banned owner) is unchanged.
2026-09-24 — sky header swatch
- Additive enum growth. The chatbot
colorenum gainsbg-sky-700andgradient_stylegainsfrom-sky-700 to-blue-700(the wizard's new sky swatch, white header text at 5.9:1). Every existing value is still accepted; clients that switch exhaustively on either enum should add the new member.
2026-09-24 — custom_instructions items up to 500 characters
- Each
custom_instructions.*item on chatbot create / update now accepts up to 500 characters on every plan (was 200; the item count is still the plan cap). Additive (a looser limit):maxLength200 → 500 inopenapi.json; a longer item is still a 422.
2026-09-24 — event: final_text on POST /v1/chat/stream
- When post-processing (URL validation, the merged reference-links block,
redaction) changed the streamed answer, the stream now carries one
event: final_textframe —{"type":"final_text","content":"…"}, the answer exactly as persisted — after the last content chunk and beforedata: [DONE]. A client that renders the answer replaces the text it accumulated from the content chunks withcontent. Unchanged answers carry no frame. Additive: both SDKs already yield unknown frames as untyped{ event, data, json }(no SDK release needed); shared fixturesdks/fixtures/sse-final-text.json.
2026-09-24 — crawler training_options.locale_policy / primary_locale
POST /v1/crawlersandPOST /v1/sitemap_crawlersaccepttraining_options.locale_policy(all|primary_only) andtraining_options.primary_locale(a BCP-47 tag as the site uses it in its URLs —en,ar,es-mx; stored lower-cased).primary_onlyskips URLs whose first path segment is another language prefix (/fr/…); pages without a language prefix are always kept.primary_onlywithoutprimary_locale, any other policy value (includingseparate_collection, which is not offered) or a malformed tag is 422VALIDATION_FAILED. Omitted → the platform default (all). Additive; no response shape changes.
2026-09-24 — Typed message and chat-stream responses in the spec
Documentation only — no wire change. The OpenAPI document now describes bodies it declared as bare objects:
POST /v1/chat/stream200 is declared astext/event-stream, each event aChatStreamEvent(messagecontent chunks and the[DONE]terminator,status,options,media,verdict,metadata— the frames of SSE contract), plus theapplication/jsonChatStreamReplaya completedIdempotency-Keyanswers withIdempotency-Replayed: true.- New
Messageschema onGET /v1/messages/{id},POST /v1/messages/{id}/feedbackand thedata[]ofGET /v1/conversations/{id}/messages(GET /v1/messages/{id}was wrongly declared as an array);ChatTurn(Message+session_id,actions,usage) onPOST /v1/chat.confidence_score,verdictandretrieval_confidenceare documented: with answer verdicts on,confidence_scoreis the answer confidence derived fromverdict(full70–100,partial40–70,none≤ 10,error0); with verdicts off it equalsretrieval_confidence. The recorded message example now shows the verdict-on value (full, 96.3 over a 91.31 knowledge match).
2026-09-24 — crawler state transitions replay
POST /crawlers/{id}/pause|resume|cancelandPOST /sitemap_crawlers/{id}/pause|resumecheck the crawler's state inside the idempotency gate, likeretry/scan: a same-key network retry of a transition that succeeded replays the stored 200 instead of answering 409 (CRAWLER_NOT_RUNNING,CRAWLER_NOT_PAUSED,CRAWLER_TERMINAL). A new key still gets the 409. The 409 responses of these routes and ofretry/scanare now documented with their exact codes. Behaviour change, no shape change.
2026-09-24 — error envelope and error-code casing fixes
- BREAKING (shipped under the calendar version; no known live consumers):
quota and widget-domain
error.codevalues are SCREAMING_SNAKE on/v1.knowledge_plan_size_exceeded,knowledge_dataset_quota_exceeded,knowledge_character_quota_exceeded(402) are nowKNOWLEDGE_PLAN_SIZE_EXCEEDED,KNOWLEDGE_DATASET_QUOTA_EXCEEDED,KNOWLEDGE_CHARACTER_QUOTA_EXCEEDED, andwidget_domain_limit_reached,widget_domain_invalid(422) are nowWIDGET_DOMAIN_LIMIT_REACHED,WIDGET_DOMAIN_INVALID, as Errors always required. A client switching on the old spelling must match the new one. Dashboard and/v1/admincodes are unchanged. - Billing-gate 402s on
POST /v1/chatand/v1/chat/streamuse the/v1envelope (MESSAGE_CREDITS_EXCEEDED,CHATBOT_BUDGET_EXCEEDED,USER_BUDGET_EXCEEDED): the undeclared dashboardsuccess: falsekey is gone andrequest_idis present. Contract fix — the spec never declaredsuccess.
2026-09-24 — DELETE replays and the action-test billing gates
- A same-key retry of a successful DELETE replays the stored response.
Every
/v1DELETE now looks the row up inside the idempotency gate, so a network retry with the sameIdempotency-Keygets the original 2xx withIdempotency-Replayed: trueinstead of 404 (or 409DATASET_DELETINGforDELETE /knowledge/{id}). A new key on the removed row is still 404 (409 while a dataset's removal is queued), and another account's id is still 404.POST /crawlers/{id}/retry,POST /sitemap_crawlers/{id}/retryandPOST /sitemap_crawlers/{id}/scanreplay the same way after the crawl started (they used to answer 409CRAWLER_RUNNING).DELETE /api_keys/{id}is unchanged: the revoked key cannot authenticate its own retry (401). Behaviour change, no shape change. POST /v1/actions/{id}/testenforces the billing gates. Amessageprobe (a real, billed model completion) is now 402CHATBOT_BUDGET_EXCEEDED/USER_BUDGET_EXCEEDEDwhen the chatbot's or the workspace's monthly AI budget is spent and 402MESSAGE_CREDITS_EXCEEDEDwhen the account has no message credits left — the same codes anderror.detailsas/v1/chatand the dashboard probe. The explicit-argumentsdry run is never billing-gated. While Call Actions are switched off platform-wide the endpoint is 404ACTIONS_DISABLED. New error responses; the 200 shape is unchanged.
2026-09-24 — chat replay and webhook co-commit fixes
- A retry of a billed chat turn is never refused for credits.
POST /v1/chatand/v1/chat/streamnow answer a same-key, same-payload retry from the stored replay before the billing checks, so retrying a turn that spent your last credit returns that turn (Idempotency-Replayed: true) instead of 402MESSAGE_CREDITS_EXCEEDED. A new key, no key, or a changed payload behave as before (402 / 422). - Webhook events commit with their write. Most
/v1mutations (actions, customers, chatbot update / status, conversation delete, profile, widget, crawler update, files, knowledge collections, knowledge removal, API-key self-revoke) now write the domain change and its webhook event in one transaction. If the event cannot be recorded the call answers 500 with nothing changed, and a retry with the sameIdempotency-Keysucceeds once with exactly one event — before, the change stuck and no webhook was ever sent.DELETE /v1/files/{id}removes the stored blob only after the delete commits. Channel connect / disconnect, chatbot and crawler delete, and crawler create are not covered yet (Webhooks). No response shapes change.
2026-09-24 — owner status, key revocation and action delete fixes
- Suspended / banned / locked owners are refused. Every tenant
/v1request now answers 403ACCOUNT_SUSPENDED(suspended or banned key owner) or 423ACCOUNT_LOCKED, as Errors already documented. Before, the account-status check never ran, so a suspended owner's key could still read and write. The keys are not revoked; reinstating the account restores them. Still a flat 404 while the public API is off. - Revoking a rotated-out key ends its grace. Dashboard revoke and
DELETE /v1/api_keys/{id}now end the predecessor's remaining grace, so the predecessor secret stops working instead of answering 200 until the 24 h grace ran out. DELETE /v1/actions/{id}now bumps the chatbot's knowledge version and writes thedeletedaction audit record, like the dashboard and admin deletes, so the deleted tool is no longer offered to the model from the enabled-actions cache. No response shape changes.settings.theme_modedescription. The embed widget now honours the colour mode too (its own light / dark palette; adata-themeembed attribute overrides it per page), so the OpenAPI description no longer says the widget renders light only. Documentation only — the field, its values and every response are unchanged.
2026-09-23 — settings.widget_page_context
PUT /v1/chatbots/{id}/widgetand the/v1chatbot create / updatesettingsbag acceptwidget_page_context:off|manual|auto(nullrestores the default,manual). It controls whether the embed widget sends the host page's context (title, URL, text set throughAgentencyWidget.setPageContext(), or captured automatically inauto) with each chat turn;offis also enforced server-side. Any other value is 422VALIDATION_FAILED. Additive; no response shape changes (the key is echoed insettingslike the other widget keys).
2026-09-23 — ai_model_selection plan entitlement
GET /v1/billing/subscription:plan.entitlementsgainsai_model_selection(boolean — whether the plan may pick a per-chatbot answer model;falseonfree). Additive; no other shape changes./v1cannot read or set a chatbot's model yet; the dashboard owns selection.
2026-09-23 — Invalid list cursors are a 422
- Every cursor list answers 422
INVALID_CURSOR(error.details.parameter=starting_afterorending_before) for a cursor that does not decode, or that lacks a scalar value for one of the list's order columns — a forged cursor, or one minted under a differentsort/order. The second case was a 500SERVER_ERROR; the first silently restarted at page one, which could loop an auto-pager forever. An emptystarting_after=is still the first page. Error-code addition; no success shape changes. DELETE /v1/chatbots/{id}/widget/domains/{host}: the idempotency scope now includes{host}, so oneIdempotency-Keyreused for a second host removes that host instead of replaying the first host's 200.
2026-09-15 — Knowledge parity with the dashboard
Knowledge mutations on /v1 now run the same flows the dashboard runs,
and refuse the same states. No live consumer existed for the previous
behaviour; the items marked shape are the only response changes.
Correctness and security
POST /v1/knowledgewithfile_id:knowledge_collection_idis tenant-scoped (a foreign collection is 422VALIDATION_FAILED, no row is written), a collection bound to another chatbot is 422COLLECTION_NOT_OWNED_BY_CHATBOT, and a bot-bound collection with nochatbot_idis 422CHATBOT_REQUIREDrather than inheriting that bot. The dataset is created the way an upload is: text extracted from the stored file,typederived from the extension, word/character stats and the plan quota applied to the real content, OCR or ingest dispatched whenauto_trainis on. An unreadable file is 422TEXT_EXTRACTION_FAILED; a file already backing a dataset is 409FILE_ALREADY_ATTACHED. With achatbot_idand no collection (text or file), the bot's default collection is used so the dataset trains.POST /v1/knowledge/{id}/replace_fileapplies the same extension + content-sniffed MIME gate as the dashboard, extracts the new text, archives the previous file row, rewrites the content and stats, and re-embeds every chatbot trained on the old content (or sends the file to OCR). Non-upload datasets are 422DATASET_NOT_UPLOAD_SOURCED;source_typeis never rewritten.POST /v1/knowledge/{id}/retryandPOST /v1/knowledge/retryqueue the full re-embed (force) path, clear the previous error and any pending cancellation, and answer 409INGESTION_IN_PROGRESSfor a dataset that is currently being processed. Bulk retry skips such rows — and rows being removed or without a chatbot — and reports them inskipped/skipped_by_reason(shape, additive).PATCH /v1/knowledge/{id}pins its field set (name,description,category,tags,is_public,content,retrain) and runs the dashboard edit flow:contentreplaces the inline text of atext-sourced dataset (other sources: 422VALIDATION_FAILED),retraincontrols the re-embed, renaming onto an existing active name is 409KNOWLEDGE_NAME_TAKEN.content/retrainwere previously validated and silently dropped.POST /v1/knowledge/{id}/recrawlaccepts every fetchable source (url,url_links,sitemap), stampscrawler_status: running, and answers 409RECRAWL_IN_PROGRESSwhile a fetch is running.
Datasets being removed
Lists and reads include datasets whose background removal is in flight,
with the new field is_deleting: true on knowledge_dataset and
knowledge_status (shape, additive). Every mutation on such a row
— update, delete, retry, recrawl, cancel, replace_file — is 409
DATASET_DELETING; remove_from_chatbot / remove_from_system refuse
the whole batch with the offending ids in error.details.ids. Inactive
rows with no removal in flight are no longer served. GET /v1/knowledge/stats datasets counts active rows only.
GET /v1/chatbots/{id}/knowledge/status (shape)
counts is now the dashboard vocabulary
{ total, processing, failed, completed, queued, removing } over the
bot's active datasets (processing = pending + processing, queued =
not yet trained), with removing counting rows being torn down. It was
a raw embedding_status → count map that omitted empty buckets.
2026-08-15 — Pre-launch hardening
All of this landed before any external integration existed, so nothing below is a breaking change to a live consumer.
Surface reduction (180 → 153 operations)
Twenty-nine operations removed, and with them the scopes jobs:read,
team:read, team:write, billing:write. They exposed dashboard
internals (/v1/scopes, /v1/reference/*, /v1/me/preferences,
/v1/chatbots/templates, queue introspection under /v1/jobs/*),
duplicated an existing endpoint (/v1/queries vs
/v1/conversations/{id}/messages, /v1/knowledge/edit_impact vs
removal_impact), or described something only a human in a browser
can finish (checkout, portal, team administration). The legacy
PAT-authenticated POST /v1/knowledge/ingest alias went too. Full
list: the public changelog page.
An unknown scope on an existing key is ignored, not rejected.
Correctness and security
POST /v1/chat/streamno longer double-charges on an idempotent retry, and the credit gate runs before the stream opens.ending_beforepages backwards correctly (it was double-reversing against Laravel's own cursor handling).- Duplicate webhook URLs are rejected with
409; they previously fanned every event out twice to the same destination. - Authentication failures are attributable in
GET /v1/request_logs. - The
/v1error envelope no longer carries asuccessfield on any failure path. - The dashboard webhook endpoint honours the SSRF guard.
- Three write endpoints gained the request validation they were missing.
Events
Nine declared-but-never-emitted event types now fire: dataset
trained/failed, crawl completed/failed, action run succeeded/failed,
and the two credit thresholds. conversation.handoff.requested is
new. Four orphaned team events were dropped.
Capability
Citations on stored messages, handoff state on conversations,
account-wide conversation listing, cross-channel session lookup,
message credits in GET /v1/usage, batch file upload, and
POST /v1/sitemap_crawlers/validate_url.
Documentation
The OpenAPI document now carries the required scope per operation
(x-agentency-scope), the grantable scope catalogue
(x-agentency-scopes), and a real recorded response example for
thirty read endpoints, captured from live responses. The docs site reads all three from the spec
instead of maintaining its own copies.
expand[] was removed from both the code and the documentation: no
controller ever passed an expansion map, so it was accepted and
silently ignored on every list.
2026-08-15 — Operations removed before launch
The full list behind the surface reduction above. None had shipped publicly, so no integration broke.
GET /v1/scopes— the scope catalogue isx-agentency-scopesin the OpenAPI document.GET /v1/reference/countries,GET /v1/reference/timezones— picker data.GET /v1/search/suggestions— a name-prefix scan, not retrieval. UsePOST /v1/search.GET /v1/queries,GET /v1/queries/stats— duplicatedGET /v1/conversations/{id}/messages.GET /v1/jobs/*— queue introspection. UseGET /v1/knowledge/{id}/status.GET|PATCH /v1/me/preferences— dashboard theme and date-format settings.GET /v1/accountsand/v1/account/*— team administration stays in the dashboard.POST /v1/billing/checkout,/v1/billing/addons/checkout,/v1/billing/portal— hosted URLs only a human can complete.GET /v1/chatbots/templates— onboarding-wizard data.GET /v1/api_keys,GET /v1/api_keys/{id}—GET /v1/medescribes the calling key; self-revoke throughDELETE /v1/api_keys/{id}remains.GET /v1/knowledge/removal_status— always returned zero.POST /v1/knowledge/edit_impact— identical toPOST /v1/knowledge/removal_impact.POST /v1/knowledge/ingest— the legacy alias with a different credential and envelope. UsePOST /v1/knowledge.
2026-08-14 — Launch (/v1)
The public API and webhook delivery now default on. Rollback remains one switch each.
The first public catalog is the committed
openapi.json. Resource ids
are MySQL integers. Auth is an Agentency API key
(agy_live_ / agy_test_), not a PAT.
Notable pre-launch contract choices (no prior production clients):
- Error
error.codevalues areSCREAMING_SNAKEon every/v1response (includingFEATURE_DISABLEDwhen the master flag is off). - Cursor lists read
limit/starting_after/ending_beforefrom the query string.ListResponderno longer appends a silentorderByDesc('id')when the caller already sorted (knowledge chunks staychunk_indexorder). profile:writeis high-risk (team:writeandbilling:writewere removed on 2026-08-15 with the endpoints they gated) (ADR-0019): owner-only at mint, individually confirmed, never PAT-grantable.- Password, 2FA, phone OTP, session revocation, and account deletion
are not on
/v1(dashboard-only, by design).