GET /v1/response_stream
The single server-sent events stream carrying command settlements and inbound messages.
GET /v1/response_streamOne long-lived server-sent events connection carries everything that happens to your account: how each command settled, and every message people send you.
Anatomy of a connection
curl -N $MAPIER_BASE_URL/v1/response_stream \
-H "Authorization: Bearer $MAPIER_API_KEY" \
-H "Accept: text/event-stream"Response headers:
HTTP/1.1 200 OK
content-type: text/event-stream
cache-control: no-cache
connection: keep-aliveThe first bytes on the wire are a comment confirming the stream is live:
: connectedThen, every 25 seconds while idle, a heartbeat:
: pingBoth are SSE comments, not events. A client that only reads data: lines
ignores them — which is correct; they exist to keep intermediaries from closing
an idle socket.
Event: command
A command reached a terminal state.
event: command
data: {"commandId":"cmd-1","logicalActionId":"order-2481","status":"succeeded","errorCode":null}Prop
Type
Only terminal statuses are emitted. The intermediate lifecycle —
claimed, executing, accepted — is deliberately suppressed, so you get
exactly one command event per command, not a progress feed.
logicalActionId echoes your Idempotency-Key only for commands on the agent-turn path. For
group.* and recipient-target sends it equals commandId, so you cannot use it to correlate with
a key you chose. Correlate on commandId instead — it is always reliable.
Event: message
Someone sent you a message.
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":[]}Prop
Type
Only inbound messages appear. Your own outbound sends do not echo back here —
their outcome arrives as a command event instead.
from and text are always present in the JSON and either can be null. A photo sent with no
caption arrives with text: null and a populated attachmentIds, so code that reaches straight
for event.text.toLowerCase() throws the first time someone sends one.
What you don't get
Documenting this precisely matters more than the happy path, because every one of these is a wrong assumption a client can silently make.
- No
id:field, so noLast-Event-ID. SSE's built-in resume mechanism is not implemented. - No replay and no backfill. Anything that happens while you are disconnected is gone. There is no cursor, no sequence number, and no endpoint to fetch missed events.
- At-most-once delivery. An event can be lost; it is never redelivered.
- No ordering guarantee. Events arrive in roughly the order they occur, but nothing enforces it and nothing lets you detect a gap.
- No delivery receipts.
succeededmeans iMessage accepted the send, not that it appeared on the recipient's device. - No typing or read events. These exist internally and are deliberately not exposed.
Unconfirmed
At-most-once with no resume is the behaviour today, not a committed contract. If your application cannot tolerate a lost settlement, keep your own record of commands you queued and reconcile on a timer rather than relying on the stream alone.
Forward compatibility
Fields are added to both event shapes without a version change. Two rules keep a client working:
- Ignore unknown fields. Do not use exhaustive destructuring that throws.
- Tolerate unknown enum values. New
errorCodevalues and newstatusvalues can appear. Write adefaultbranch:
switch (event.status) {
case "succeeded":
markDelivered(event.commandId);
break;
case "failed":
case "expired":
markFailed(event.commandId, event.errorCode);
break;
default:
// ambiguous, cancelled, or something added later.
markNeedsReview(event.commandId, event.status);
}Reconnecting
The stream drops periodically even when healthy. Reconnect immediately and unconditionally — there is no resume token to manage, and no penalty for reconnecting often.
const BASE = process.env.MAPIER_BASE_URL!;
for (;;) {
try {
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}`);
await consume(res.body);
} catch (err) {
console.warn("stream dropped, reconnecting", err);
}
await new Promise((r) => setTimeout(r, 1000));
}A worked parser is in Reconnection and delivery guarantees.
Multiple connections
You may hold several streams open at once. Every connection receives every
event — they are a broadcast, not a work queue. Two workers on one account both
see the same settlement, so deduplicate on commandId if that matters.
Errors
| Status | Code | Cause |
|---|---|---|
401 | unauthenticated | No credential |
404 | no_connector | Your account has no Mac connector provisioned |
405 | method_not_allowed | Not a GET |
429 | rate_limited | 30 connections per minute |
503 | stream_unavailable | The stream service is not configured |
Quick reference
| Events | command, message |
| Heartbeat | : ping every 25s |
| Resume | none |
| Delivery | at-most-once |
| Ordering | not guaranteed |
| Concurrent streams | allowed, all receive everything |
| Rate limit | 30 per minute |