Response stream

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.

202 Accepted
{ "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:

FrameFires when
commandone of your commands reaches its terminal status
messagesomeone sends a message to one of your lines
errorthe stream is ending because your key or project stopped working
receipta recipient's device reported a message delivered or read (?events=receipts)
delivery_failedthe provider reported a message not delivered (?events=delivery_failures)
locationa contact's shared location started, moved or ended (?events=locations)
moderationa contact edited or unsent a message they sent (?events=moderation)
polla 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}

: ping

Comments 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:

  • : connected is written immediately on a successful connection. Seeing it confirms the socket is open and authenticated.
  • : ping is written every 25 seconds. It exists to stop proxies and load balancers from closing a stream that has been quiet.
  • : catching up and : replay window exceeded belong to a resume — see Resuming — and : bye says 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 the reaction.added or reaction.removed webhook, outside group chats.
  • No typing events. Nothing reports that a contact is typing.
  • No intermediate command states. A command frame is terminal and emitted once; queued is the 202, 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.

On this page