Versions
The x-mapier-version header every /v1 route reads, the version your project gets when you omit it, and what each version changes.
The major version is in the path — /v1 — and a /v2 would sit beside it
rather than replacing it. Within /v1, behaviour that a deployed integration
could trip over changes only behind a dated version, which you choose with
one header:
x-mapier-version: 2026-10-15Every /v1 route reads it. A version is a date, and a date is a promise: it is
the first day a project that mints its first API key defaults to that
behaviour, and nothing changes under an integration that keeps sending it.
The versions
| Version | What it changes |
|---|---|
2026-09-01 | The oldest. What every project that had a key before any later version gets when it sends no header. |
2026-10-01 | A command target that names no channel is resolved from your project's lines rather than meaning iMessage. |
2026-10-15 | Refusals speak the transport-neutral vocabulary: one code and three line_unavailable reasons change their word. The newest version. |
Fields and frames are added without a version, and a new refusal can join a
status an operation already documents without one — service_busy joined
every operation's 503 that way — so parse defensively, as the API
reference says. A version is spent where an integration
already deployed would otherwise be handed a different word for the same
answer, as 2026-10-15 is, or have the same request resolved differently, as
2026-10-01 is.
When you send no header
You get your project's default: the newest version dated on or before the day the project's first API key was created — test or live, revoked or not. So:
- Rotating a key changes nothing, and neither does moving from a test key to a live one: both are keys of the same project, and the project's first key is where the default comes from.
- A project whose first key came after a change has that change without asking. A new project defaults to whatever is newest on the day of its first key.
- Moving an integration to a new project can change its behaviour. An integration started on a project that already had keys gets that project's older default, and the same integration pointed at a new project gets the new one's.
Pin a version when you start, and before you point an existing integration at a new project. Pinned, nothing under you moves until you change the header yourself.
What each version changes
2026-10-01 — the channel of a target
On POST /v1/commands/{type} a target may name the channel
it is on, target.channel. Below 2026-10-01 a target that names none means
iMessage. From 2026-10-01 it is resolved from your project's lines, in this
order:
- Active lines decide first. When every active line is on one channel, the target means that channel — a paused line on another channel beside active iMessage ones does not make an iMessage send ambiguous.
- With no active line, the lines the project still holds decide — every
status but released, so a paused or provisioning line counts and a released
one never does. Held lines on one channel mean that channel, and the send is
then answered by that line's state:
503 line_unavailablewithdetails.reason: "line_paused"for a contact pinned to a paused line, and503 no_linefor a contact not yet pinned to any. - A project that holds no line at all answers
503 no_line, before the recipient's address is read. - Lines on more than one channel — active ones, or held ones when none is
active — answer
422 validation_failedattarget.channel, listing those channels in the entry'schannels. Name one of them.
{
"error": {
"code": "validation_failed",
"message": "This project has lines on more than one channel.",
"details": [
{
"path": "target.channel",
"message": "Name the channel to send on: …",
"channels": ["imessage", "…"]
}
]
}
}Name the channel whenever a target names an address — "target": { "channel": "imessage", … } — and this refusal can never reach you, whatever lines the
project comes to hold. A conversation target is on its thread's channel and is
never refused this way.
No other operation reads a target, so nothing else differs at this version.
2026-10-15 — the transport-neutral words
The same refusals, the same statuses and the same remedies — only the word changes. You receive one word of each pair, never both:
Below 2026-10-15 | At 2026-10-15 or later | Where |
|---|---|---|
no_line_capacity | capacity_exhausted | the 503 code, on POST /v1/commands/{type} |
relay_down | transport_down | a 503 line_unavailable reason |
relay_unconfigured | transport_unconfigured | a 503 line_unavailable reason |
line_released | line_retired | a 503 line_unavailable reason |
The code pair is answered by POST /v1/commands/{type} alone. The reason pairs
are answered wherever line_unavailable is: sending a command,
handle check, attachments and
receipts. A client that switches on both words of each pair
works at every version; one that switches on the older word alone breaks the
day its project, or its pin, reaches 2026-10-15.
The operations where nothing differs
GET /v1/response_stream and
GET /v1/whoami behave the same at every version listed. They
read the header all the same, so a value they do not know is refused there
too.
A version this build does not know
Any other value is 422 validation_failed, whose entry lists every version the
build answers to in supported:
{
"error": {
"code": "validation_failed",
"message": "That is not an API version this build answers to.",
"details": [
{
"path": "x-mapier-version",
"message": "Send one of: 2026-09-01, 2026-10-01, 2026-10-15. Omit the header to keep the behaviour your integration was written against.",
"supported": ["2026-09-01", "2026-10-01", "2026-10-15"]
}
]
}
}The OpenAPI document lists the same versions as the header's enum on every operation, with a sentence per operation on what differs.