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 Mac
connector has claimed 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, and it is the only push channel this API has. There are no webhooks,
and there is no endpoint to poll for a command's status. If nothing in your
system is reading the stream, nothing in your system knows whether your message
was sent.
One connection, two kinds of event
A single stream carries both directions of your account's activity.
| Event | Fires when |
|---|---|
command | one of your commands reaches a terminal state |
message | someone sends an inbound message to your account |
You do not subscribe to one or the other, and there are no filters — you open the connection and receive everything for your account.
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 $MAPIER_BASE_URL/v1/response_stream \
-H "Authorization: Bearer $MAPIER_API_KEY" \
-H "Accept: text/event-stream"The raw bytes look like this — a comment on connect, then frames as things happen, then a heartbeat when the account is idle:
: connected
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":[]}
event: command
data: {"commandId":"cmd-1","logicalActionId":"order-2481","status":"succeeded","errorCode":null}
: pingComments and the heartbeat
Two of those lines are not events. Anything starting with : is an SSE
comment, and both of 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.
A parser that acts only on data: lines ignores both 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 = process.env.MAPIER_BASE_URL!;
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.
The stream drops periodically even when everything is healthy, and there is no resume token or backfill. Anything that happens while you are disconnected is gone. Treat reconnection as the normal case, not an error path.
Read next
Command settlement events
What happened to each command you sent, and what each terminal status means.
Inbound message events
Messages people send you, and the ids you need to reply.
Reconnection and delivery guarantees
A correct parser, a reconnect loop, and what the stream does not promise.