GET /v1/response_stream

The single server-sent events stream carrying command settlements and inbound messages.

GET /v1/response_stream

One 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-alive

The first bytes on the wire are a comment confirming the stream is live:

: connected

Then, every 25 seconds while idle, a heartbeat:

: ping

Both 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 no Last-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. succeeded means 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 errorCode values and new status values can appear. Write a default branch:
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

StatusCodeCause
401unauthenticatedNo credential
404no_connectorYour account has no Mac connector provisioned
405method_not_allowedNot a GET
429rate_limited30 connections per minute
503stream_unavailableThe stream service is not configured

Quick reference

Eventscommand, message
Heartbeat: ping every 25s
Resumenone
Deliveryat-most-once
Orderingnot guaranteed
Concurrent streamsallowed, all receive everything
Rate limit30 per minute

On this page