mapier docs

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.

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, no_effect, 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.

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