The response stream
Why every asynchronous outcome arrives on one SSE connection, and how to start consuming it.
A command request never tells you what happened. POST /v1/commands/:type
returns 202 the moment the command is durably queued — before the line has
taken it, before iMessage has seen it, and long before anything
appears on anyone's phone.
{ "commandId": "cmd-1", "status": "queued", "disposition": "queued" }That is a receipt, not a result. GET /v1/response_stream is where the result
arrives. If nothing in your system is reading the stream, nothing in your
system knows whether your message was sent. (The console's Commands page shows
every settlement to a person, which is the other way to learn it and the only
one that needs no code.)
One connection, a handful of frames
A single stream carries both directions of your project's activity in one environment. Two frames are always on it, one closes it, and five more arrive only when you ask for them:
| Frame | Fires when |
|---|---|
command | one of your commands reaches its terminal status |
message | someone sends a message to one of your lines |
error | the stream is ending because your key or project stopped working |
receipt | a recipient's device reported a message delivered or read (?events=receipts) |
delivery_failed | the provider reported a message not delivered (?events=delivery_failures) |
location | a contact's shared location started, moved or ended (?events=locations) |
moderation | a contact edited or unsent a message they sent (?events=moderation) |
poll | a poll was created, changed or voted on (?events=polls) |
?channel= and ?conversationId= narrow what a stream sends to one channel
or one conversation. The reference has every
parameter. New event: names can appear without a version bump. Ignore a name
you do not recognise; do not close the socket.
Opening it
The stream is ordinary server-sent events over HTTP, so you can watch it with
curl before writing any code. The -N flag disables output buffering, which
you need to see events as they land.
curl -N https://api.mapier.ai/v1/response_stream \
-H "Authorization: Bearer $MAPIER_API_KEY" \
-H "Accept: text/event-stream"The raw bytes look like this — a reconnect hint and a comment on connect, then
frames as things happen, each with an id:, and a heartbeat every 25 seconds
whether anything else was written or not:
retry: 3000
: connected
id: 0192a7c4-6a02-7e11-8c4d-2b9f7e3a5d10
event: message
data: {"conversationId":"a0000000-0000-4000-8000-000000000001","from":"+14155550100","text":"is it here yet?","externalId":"guid-1","replyToExternalId":null,"occurredAt":"2026-08-24T18:03:11.000Z","attachmentIds":[],"attachments":[],"messageId":"0192a7c4-6a02-7e11-8c4d-2b9f7e3a5d22"}
id: 0192a7c4-6b17-7c02-9d5e-3c0a8f4b6e21
event: command
data: {"commandId":"3f1a0c2e-8b47-4d19-9a5e-2c6f0b8d4e71","logicalActionId":"order-2481","status":"succeeded","errorCode":null,"conversationId":"a0000000-0000-4000-8000-000000000001","failureClass":null}
: pingComments and the heartbeat
Some of those lines are not events. retry: 3000 tells an SSE client how long
to wait before reconnecting. Anything starting with : is an SSE comment, and
the ones you will see are infrastructure rather than data:
: connectedis written immediately on a successful connection. Seeing it confirms the socket is open and authenticated.: pingis written every 25 seconds. It exists to stop proxies and load balancers from closing a stream that has been quiet.: catching upand: replay window exceededbelong to a resume — see Resuming — and: byesays the server is being replaced; reconnect.
A parser that acts only on data: lines ignores all of them without any special
handling, which is the behaviour you want. Going the other way is also useful:
if you have received no bytes at all for well over 25 seconds, the connection
is probably dead even though it has not reported an error, and you should drop
it and reconnect.
A minimal consumer
This is enough to see events arrive in Node. It reads the response body, accumulates bytes into a buffer, and keeps the trailing partial line back for the next read — a chunk boundary can fall anywhere, including the middle of a JSON payload.
const BASE = "https://api.mapier.ai";
const res = await fetch(`${BASE}/v1/response_stream`, {
headers: {
Authorization: `Bearer ${process.env.MAPIER_API_KEY}`,
Accept: "text/event-stream",
},
});
if (!res.ok || !res.body) throw new Error(`stream status ${res.status}`);
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
for (;;) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() ?? ""; // the last piece may be half a line
for (const line of lines) {
if (!line.startsWith("data:")) continue; // skips ": connected" and ": ping"
console.log(JSON.parse(line.slice(5)));
}
}This prints payloads but discards the event: line, so it tells you what
arrived without telling you which kind it was. It also stops at the first
disconnect. Both are fine for a first look and wrong for production — the full
parser and the reconnect loop are on
Reconnection and delivery guarantees.
Every frame carries an id:. Keep the last one you processed and send it as Last-Event-ID when
you reconnect: the stream replays what followed it, at most 500 per batch and as far back as 90
days, so a drop costs you nothing but the reconnect. A frame can arrive twice across a reconnect;
apply frames by id.
What the stream does not carry
- No reaction or conversation frames. A new conversation reaches the
console, and your webhooks as
conversation.created. A contact's tapback is thereaction.addedorreaction.removedwebhook, outside group chats. - No typing events. Nothing reports that a contact is typing.
- No intermediate command states. A
commandframe is terminal and emitted once;queuedis the202, and nothing in between is framed.
For message.send, succeeded is the transport's settlement of the send, not
a delivery receipt. Delivered, read and not delivered are the
receipt and delivery_failed frames, on
request.
Read next
Command settlement events
What happened to each command you sent, and what each terminal status means.
Receipts and delivery verdicts
receipt and delivery_failed, and the webhooks that carry the same facts.
Inbound message events
Messages people send you, and the ids you need to reply.
Location events
A contact's shared location, on request.
Poll events
A poll's creation, choices and votes, and the ids a vote needs, on request.
Reconnection and delivery guarantees
A correct parser, resuming with Last-Event-ID, and what the stream does not promise.