How the API changes
Version 1 of the API lives at /api/v1. We add to it without notice: new fields in responses, new optional fields in requests, new endpoints and webhook events, and new values in fields that already list several, such as a rejection reason or an error code. Write your code to ignore fields and events it doesn't know, and to handle a value it hasn't seen as it would the nearest one it has.
Anything that could break an integration written that way is announced on this page and emailed to every account owner before it happens. That covers removing or renaming a field, changing what a field means or its default, making an optional field required, and lowering a limit.
7 November 2026
Changes to how existing calls behave
- Test keys can no longer read live data. Contacts, lists, opt-outs, conversations, tracked link clicks and live texts answer
403withtest_key_live_data, and so does a send or quote that namescontact_idsor alist_id.GET /messageswith a test key lists only the test texts that key sent, and test texts no longer fill merge fields from your contacts. See test keys. - A webhook created with a test key now receives events about test texts only (
test_onlyis true), and a test key sees and changes only those webhooks. Webhooks that already existed keep receiving every event; manage them with a live key or in the dashboard. - A send to a list through the API ends with your opt-out line, as a campaign always has. Send
append_opt_out: falseto leave it off. Sends to typed numbers orcontact_idsare unchanged, and the quote follows the same rule. - A message's
error_codenow comes from a fixed list, in the API, in webhooks and in exports. A text waiting to be tried again showsretryingwhere it used to show the route's own code (such asgateway_timeout), refusals and delivery failures map to the documented codes, and a plain cancel has no error code. The list, with whether each code is final and refunded, is on errors. - An
Idempotency-Keyis kept for 24 hours. A retry within that time still gets the first answer; after it, the same key starts a new send. - Webhook deliveries, with their payloads, are deleted after 30 days, so
GET /webhooks/{id}/deliveriesshows the last 30 days and an older delivery can't be sent again. GET /opt-outspages by cursor like every other list: newest first, 25 to a page, withmeta.next_cursorand a newsincefilter. A request that passesoffsetstill gets the old answer (50 to a page, withmeta.total) for now. We'll announce it here before that stops working.- For 24 hours after you rotate a webhook secret,
X-Connect-Signaturecarries twov1values, the old secret's first and the new one's last. Code that reads only the lastv1carries on working with the new secret; checking everyv1lets servers still on the old secret accept deliveries too. The webhooks guide has updated examples. - An endpoint that fails every delivery for three days is switched off, its waiting deliveries are marked failed and the account owner gets an email. Until now it was switched off after 100 failures in a row. Webhooks show
failing_sinceandauto_disabled_at. POST /messagesanswers a body that isn't valid JSON with400invalid_json, like every other route. It used to answer422validation_failed.opt_out.createdfires whenever a number joins your opt-out list: STOP, the opt-out link, the dashboard, the API or an import, withdata.sourcesaying which. It used to fire for STOP and the opt-out link only.- A reply of six words or fewer that starts with STOP, STOPALL, UNSUBSCRIBE, QUIT or OPTOUT now opts the number out, so "Stop please" counts. Longer texts, and short ones starting with CANCEL or END, still need to be the opt-out word on its own.
- A contact's
customvalues are checked against your custom fields. A date field takesYYYY-MM-DDorDD/MM/YYYYand is stored asYYYY-MM-DD, a number field takes a number, and a value that doesn't fit answers422validation_failedwith the field's key indetails.field. A key that isn't a field yet is added as a text field while the account has fewer than 20 fields, so existing integrations keep working; past that it is refused. GET /contacts/fieldslists the keys of your custom fields, oldest field first. Keys your contacts already held are added to the list as text fields the first time it is read.GET /contacts/exporthas one more column per custom field afterupdated_at, headed by the field's key. ThecustomJSON column is still there, in the same place.- A quote with
source: "campaign"now prices thefromnumber you give it, since a campaign can be sent from one.
New
- API keys can be limited to a scope:
full,send(send, bulk send, quote and look up, and read or cancel only the texts and batches it sent) orread(GETonly). They can also have an expiry date and a list of up to 20 IP addresses or CIDR ranges. Requests outside those limits answerinsufficient_scope,key_expiredorip_not_allowed. Existing keys arefull, with no expiry and no address limit. POST /messages/bulksends up to 1,000 texts in one call, each with its own number, body,client_refand merge values. Every item is checked on its own and reported indata.results; oneIdempotency-Keycovers the call.GET /batches/{id}reads a send with its texts counted by status, andPOST /batches/{id}/cancelcancels and refunds everything in it that hasn't left yet.- A send to more than 1,000 recipients with
Prefer: respond-async(orasync: true) answers202at once with its batch and goes out in the background. Follow it withGET /batches/{id}. expires_inon sends and bulk sends, from 60 to 86,400 seconds: a text that hasn't left by then is cancelled withexpired_before_sendand refunded, which suits one-time codes.POST /lookupchecks a number for free: E.164, whether it's valid, its region, its line type by prefix and whether it has opted out of your texts. No network is asked.smart_encoding: trueon sends and quotes swaps curly quotes, en and em dashes, the ellipsis and special spaces for their plain forms before counting, so one pasted apostrophe no longer doubles the parts. The quote now listsnon_gsm_characters, the characters that made a text Unicode.- Test numbers: with a test key, 07700 900002 comes back undelivered, 07700 900003 replies and 07700 900004 replies STOP, with the webhook events each would raise.
DELETE /messages/{id}removes a sent text's body for good while keeping its status and cost.POST /erasureserases a number from your account for someone who has asked: contact, text bodies, link clicks and unsent texts, with a number that had opted out kept only as a hash so it is still never texted. An account can also have a retention period, from 30 to 3,650 days, after which text bodies are removed every day.message.cancelledandopt_out.deletedwebhook events, andPOST /webhooks/{id}/deliveries/{delivery_id}/retryto send a failed delivery again.- Every response carries an
X-Request-Idheader, and a500repeats it aserror.request_idfor you to quote to support. - A Postman collection, built from the OpenAPI document. The document now gives every operation an
operationId, types every message field, and lists what it was missing:GET /webhooks/{id},DELETE /sender-ids/{id},track_links,ignore_quiet_hourson a campaign send, thenumberfilter onGET /messagesandsegments_per_recipientin the quote. Errors lists every error code the API returns. GET /campaigns/{id}/reportreports on one campaign: its texts by status, the failed and undelivered ones grouped by error code with what each code means, how many people tapped a tracked link, and how many of the people it reached replied or opted out in the seven days after it started, each person counted once.POST /contacts/bulk-deletedeletes up to 1,000 contacts in one call and one transaction. It answers with how many were deleted and how many ids weren't found; an id that isn't one of your contacts doesn't stop the rest.sinceanduntilonGET /messagesandGET /messages/exporttake a plain date such as2026-11-01as well as a date-time. A date covers the whole of that day in UK time, sountil=2026-11-07runs to midnight at the end of 7 November.- Custom fields:
GETandPOST /custom-fields,PATCH /custom-fields/{id}to rename one andDELETEto remove it with its values. An account can have up to 20, each text, date or number. Each works as a merge field by its key, and a date field reads as 14 March 2026 in the text. - Segments, saved filters worked out each time they are used: list membership, a custom field (including a date that is today or within the next few days, every year for birthdays), added, replied or clicked a link in the last so many days, and never texted. Opted-out numbers are never in one.
/segmentshas the usual create, read, update and delete, plusGET /segments/{id}/count,GET /segments/{id}/contactsandPOST /segments/previewfor conditions you haven't saved. Send to one withsegment_idonPOST /messages, the quote and campaigns. - Campaigns take
segment_idin place oflist_id, andfrom, one of the numbersGET /numbers/fromlists, in place of a sender name. - Recurring campaigns at
/campaign-series: weekly on chosen days or monthly on a day of the month, at a UK time, with an optional end date or number of runs. Each run is a campaign of its own withseries_idset and its own report (GET /campaigns?series_id=lists them). A run that can't go for lack of credit pauses the series.POSTto/pause,/resumeor/stopon a series to change it. - Date-based reminders at
/automations: a text some days before, on or after each contact's date field at a set time, to all contacts, a list or a segment, optionally every year. They send like any other send, quiet hours and opt-outs included, and nobody gets one reminder's text twice for the same date.GET /automations/{id}/previewsays when it runs next and for how many people;GET /automations/{id}/sendsis its log. - Three new error codes:
field_exists,in_use(a segment or reminder still uses the field or segment you're deleting) andseries_ended. Test keys can't read segments, custom fields or reminders, as with contacts.
Fixes
- A path under
/api/v1that doesn't exist answers404not_foundas JSON, where it used to return the website's HTML page. GET /conversations?unread=truepages correctly: every page and its cursor count unread threads only.- Conversation paths find a thread however the number is written:
%2B447412000001,447412000001and07412000001are the same thread.