GET /v1/response_stream

The server-sent events stream of a key's project and environment — inbound messages, command settlements and, on request, receipts, delivery verdicts, shared locations and a contact's edits and unsends, each resumable by id.

GET /v1/response_stream

One long-lived server-sent events connection carries what happens to your key's project in your key's environment: every inbound message as a message frame, and every command's terminal status as a command frame. Ask for them and it also carries a receipt frame for each message a recipient's device delivered or read, and a delivery_failed frame for each message the provider accepted and later reported not delivered, a location frame for each change to a location a contact shares with the line, a moderation frame for each message a contact edits or unsends, and a poll frame for each moment of a poll. A stream that ends because your key or your project stopped working says so in an error frame before it closes.

The frames are served from Mapier's own stored events in both environments, so a test key and a live key read the same shapes, and every frame but the closing error carries an id: you can resume from.

The stream resumes

Every frame carries an id:, and reconnecting with Last-Event-ID resumes where you left off — see Resuming. There is no separate catch-up call.

Anatomy of a connection

curl -N https://api.mapier.ai/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; charset=utf-8
cache-control: no-store
x-accel-buffering: no

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

retry: 3000
: connected

Then, every 25 seconds, a heartbeat — on a fixed interval, whether or not frames were written in between:

: ping

Lines starting with : are SSE comments, not events. Five are written: : connected, : ping, : catching up and : replay window exceeded (the last two under Resuming), and : bye when the server is being replaced during a deploy — reconnect on the retry: it gave you. A client that acts only on event: and data: lines ignores all of them, which is correct.

Query parameters and headers

ParameterIn
eventsqueryFrames to add beyond message and command, comma-separated. See Asking for more frames.
channelquerySend only the frames about conversations on this channel. One value. See Narrowing a stream.
conversationIdquerySend only the frames about this conversation. One value, a uuid.
Last-Event-IDheaderThe id: of the last frame you processed. See Resuming.
x-mapier-versionheaderRead on every route; nothing this operation does differs between the versions yet.

A value events, channel or conversationId does not accept is a 422 validation_failed before the stream opens. Pass events once, as one comma-separated value: ?events=receipts&events=delivery_failures repeats the parameter, and only the first is read, so no delivery_failed frame arrives and nothing says why. A repeated channel or conversationId, by contrast, is refused. A Last-Event-ID that is not a uuid is ignored rather than refused (see Resuming).

Asking for more frames

Omit events and the stream is exactly two frames, message and command. Each value adds one frame:

events valueAdds the frameFor every message
receiptsreceipta recipient's device reported delivered or read
delivery_failuresdelivery_failedthe provider accepted and later reported not delivered — Apple's "Not Delivered"
locationslocationa contact's shared location started, moved or ended
moderationmoderationa contact edited or unsent a message they sent
pollspolla poll was created, its choices changed, someone voted or took their vote back

The values combine: ?events=receipts,delivery_failures,locations,moderation,polls. A value not listed here is a 422 whose details entry names it in unknown and lists the values this stream frames in supported:

GET /v1/response_stream?events=receipt — 422
{
  "error": {
    "code": "validation_failed",
    "message": "That stream option is not one this API frames.",
    "details": [
      {
        "path": "events",
        "message": "`events` takes a comma-separated list of: receipts, delivery_failures, locations, moderation, polls.",
        "unknown": ["receipt"],
        "supported": ["receipts", "delivery_failures", "locations", "moderation", "polls"]
      }
    ]
  }
}

Narrowing a stream

channel sends only the frames about conversations on that channel, and takes one of imessage · rcs · sms · whatsapp · voice · telegram · signal · apple_business · discord · slack · messenger. A frame's channel is its conversation's, so a command frame for a command that names no conversation is on a stream without channel and on no channel's.

conversationId sends only the frames about one conversation: its inbound messages, the commands that name it and — on a stream opened with an events value — the receipt, delivery_failed, location, moderation and poll frames about it. It must be a conversation in your key's project and environment, and on channel when you give both. Otherwise the answer is a 422: at conversationId for a conversation this key cannot stream, or at channel — carrying the channel the conversation is on in the entry's own channel — when the two disagree. More than one value for either parameter is a 422 too.

A filter narrows what a stream sends, not what it reads: a narrowed stream reads the project's events as an unnarrowed one does and sends the frames that match, so it costs what any stream costs and counts toward the same four open streams. Narrow by channel, or follow one conversation; to follow many, open one stream and read conversationId off the message, command, location, moderation and poll frames. The receipt and delivery_failed frames carry none: correlate them by commandId or messageId.

Resuming with Last-Event-ID

Every frame but error carries an id: line before its event: line. Keep the id: of the last frame you finished processing, and send it as Last-Event-ID when you reconnect:

curl -N https://api.mapier.ai/v1/response_stream \
  -H "Authorization: Bearer $MAPIER_API_KEY" \
  -H "Accept: text/event-stream" \
  -H "Last-Event-ID: 0192a7c4-5b1e-7c3d-9f2a-6e8d4b1c0a37"
  • At most 500 are replayed per batch. The stream sends the frames after that id, 500 at a time, in order. If you were further behind, nothing is dropped: the rest follows on the same connection, and a : catching up comment marks each batch that stopped at the cap, so 500 frames and then silence is not mistaken for having caught up.
  • Events are kept for 90 days, which is how far back an id can resume. An id older than that is not an error: you are sent everything that remains after it, preceded by a : replay window exceeded comment so you can tell it from a quiet period.
  • On a narrowed stream the 500 counts the events a batch read, not the frames it sent, so a batch may send fewer frames and still be followed by : catching up.
  • A Last-Event-ID that is not a uuid is ignored. Every id: this stream writes is one, so anything else did not come from it: the stream starts at the live edge rather than refusing it, because a browser's EventSource re-sends the same header on every reconnect and a refusal would come back for ever.

A frame can therefore arrive twice across a reconnect — the one you were reading when the socket went. Apply frames by id and a repeat is a no-op. Reconnection and delivery guarantees has a loop that keeps the id and a parser that reads it.

Event: command

A command reached its terminal status. Emitted once per command.

id: 0192a7c4-5b1e-7c3d-9f2a-6e8d4b1c0a37
event: command
data: {"commandId":"3f1a0c2e-8b47-4d19-9a5e-2c6f0b8d4e71","logicalActionId":"order-2481","status":"succeeded","errorCode":null,"conversationId":"a0000000-0000-4000-8000-000000000001","type":"message.send","messageId":"0192a7c4-5b1e-7c3d-9f2a-6e8d4b1c0a99","externalId":null,"projectId":"b0000000-0000-4000-8000-000000000002","environment":"live","failureClass":null}

Prop

Type

A 202 — an adopted one included — says the command was queued; this frame says how it ended. For message.send, succeeded is the transport's settlement of the send and not a delivery receipt — delivery is a receipt frame on a stream opened with ?events=receipts, and a provider's later "not delivered" is a delivery_failed frame, which moves no status.

Switch on failureClass

errorCode is the transport's own word and the set is open; failureClass is closed, the same for every transport, and it is the one to branch on — what each value means is in Error codes. The same class rides the command.settled and message.failed webhooks, and the 409 that hands a retried Idempotency-Key the first attempt's code.

Event: message

Someone sent a message to one of your lines. Your own sends do not echo here.

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"}

Prop

Type

text is always present in the JSON and 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.

A null text does not promise an attachment either. On a live iMessage line a chat background set or removed in a conversation with one person also arrives as a message from that person with no text, whoever made the change — a chat_background.set or a chat_background.remove of yours, or the person on their own phone. Skip a received message that has no text and no attachment rather than reply to it.

Event: receipt

Sent only on a stream opened with ?events=receipts. A recipient's device reported a message delivered or read.

id: 0192a7c4-7d55-7a90-b3e1-4c6a8f2d9e04
event: receipt
data: {"commandId":"3f1a0c2e-8b47-4d19-9a5e-2c6f0b8d4e71","messageId":"0192a7c4-5a31-7bd2-8e6f-1d0c9b8a7f65","status":"delivered","occurredAt":"2026-08-24T18:03:14.000Z","simulated":false}

Prop

Type

In test the sandbox produces receipts. Live, each transport reports what it can, and an iMessage line reports from Apple's status of the send, which is first asked about 75 seconds after the send and last about 10 minutes after it — so on iMessage a delivery or a read later than that is never framed, and a read arrives only from a recipient who shares read receipts. To ask about one message now, whatever the stream said, call GET /v1/messages/{messageId}/receipts.

Event: delivery_failed

Sent only on a stream opened with ?events=delivery_failures. The provider accepted a message and later reported it not delivered — Apple's "Not Delivered" on iMessage, and in test the sandbox's Sends, then not delivered delivery mode. It is the same fact as the message.delivery_failed webhook.

id: 0192a7c4-8e20-7f43-a1b6-9d3c5e7f0a28
event: delivery_failed
data: {"commandId":"3f1a0c2e-8b47-4d19-9a5e-2c6f0b8d4e71","messageId":"0192a7c4-5a31-7bd2-8e6f-1d0c9b8a7f65","occurredAt":"2026-08-24T18:05:02.000Z","simulated":false}

Prop

Type

That send is not a failed one. The message keeps its status, the frame carries no status, errorCode or failureClass, and the send's own command frame still arrives — possibly after this one, because a live verdict can land before the transport's settlement does. Correlate by commandId, and do not assume you recorded the send before the verdict came.

Event: location

Sent only on a stream opened with ?events=locations. A contact sharing their location with the line started, sent a new position, or stopped — on iMessage from Apple's Find My, about every 30 seconds per contact sharing, a repeated position framed once. The same facts as the location.share_started, location.updated and location.share_ended webhooks.

id: 0192a7c4-9b31-7c02-8d44-2f6e1a3b5c70
event: location
data: {"type":"location.updated","conversationId":"a0000000-0000-4000-8000-000000000001","contactId":"c0000000-0000-4000-8000-000000000003","latitude":40.76203,"longitude":-73.985918,"horizontalAccuracyM":12,"capturedAt":"2026-09-30T18:07:00.000Z","occurredAt":"2026-09-30T18:07:00.000Z","simulated":false}

Prop

Type

A location is filed only on a one-to-one conversation and a contact this project already holds, so a person who shares before any message between you is not framed. The coordinates are a person's, and you are responsible for how you store and use them. See Location events.

Event: moderation

Sent only on a stream opened with ?events=moderation. A contact edited or unsent a message they sent to the line. One frame name for both, told apart by type: the same facts as the message.edited and message.unsent webhooks.

id: 0192a7c4-a412-7e19-b0c5-3d7f2a9e6b18
event: moderation
data: {"type":"message.edited","messageId":"0192a7c4-6c02-7d4e-9a13-5b8f2c1d0e44","conversationId":"a0000000-0000-4000-8000-000000000001","externalId":"guid-1","text":"See you at 7","previousText":"See you at 6","editedAt":"2026-09-30T18:10:04.000Z","simulated":false}

Each names the message by the messageId and externalId its message frame carried. The message's stored text is not rewritten — an edit's new text arrives here alone — and an unsent message is not deleted. Only a message this platform filed is framed, so an edit of one sent before the line was connected is not. The line's own edits and unsends are command frames, never these.

message.edited

Prop

Type

message.unsent

Prop

Type

See Edits and unsends.

Event: poll

Sent only on a stream opened with ?events=polls. A poll in a conversation was created, had its choices changed, got a vote, or had a vote taken back — a contact's poll, or the line's own. One frame name for all four, told apart by type: the same facts as the poll.created, poll.options_changed, poll.voted and poll.vote_cleared webhooks.

id: 0192a7c4-b5e3-7a41-9c28-4e1f6a0b2d93
event: poll
data: {"type":"poll.created","conversationId":"a0000000-0000-4000-8000-000000000001","actor":"line","contactId":null,"pollExternalId":"2F6B1C3D-8E4A-4B5C-9D7E-0A1B2C3D4E5F","question":"Lunch?","options":[{"optionExternalId":"A1B6D8F0-9C2E-4D5F-8A4B-6C8D0E1F2A3B","text":"Tacos"},{"optionExternalId":"B2C7E9A1-0D3F-4E6A-8B5C-7D9E1F2A3B4C","text":"Ramen"}],"occurredAt":"2026-10-01T18:11:40.000Z","simulated":false}

Prop

Type

Your own poll streams here too, with actor: line: its pollExternalId and its choices' optionExternalIds — what poll.vote takes — arrive on this frame and nowhere else.

Event: error

The stream is ending, and this is why. The socket closes after it, and the frame carries no id: — there is nothing after it to resume.

event: error
data: {"code":"invalid_credentials","message":"That API key has been revoked."}

Prop

Type

The credential is re-proved every 60 seconds, so a revoked or expired key, or an archived project, closes the stream within a minute even if nothing else noticed — with invalid_credentials or project_archived here rather than a dropped socket. Neither heals on a reconnect. A socket that closes with no error frame — a network blip, a deploy's : bye — is ordinary: reconnect with your Last-Event-ID.

Forward compatibility

Fields are added to frames without a version change, and a frame can carry keys this page does not list. Two habits keep a client working:

  • Read the fields you need by name. Do not reject a frame for an extra key.
  • Tolerate unknown values and unknown event: names. errorCode is open, and a new frame name is ignored rather than thrown on. Give every switch a default:
switch (event.status) {
  case "succeeded":
    markSettled(event.commandId); // settled, not delivered
    break;
  case "failed":
  case "cancelled":
  case "expired":
    markNotDone(event.commandId, event.failureClass, event.errorCode);
    break;
  case "ambiguous":
  case "no_effect":
    markOutcomeUnproven(event.commandId, event.errorCode);
    break;
  default:
    markOutcomeUnproven(event.commandId, event.errorCode);
}

Reconnecting

The stream drops periodically even when healthy, so the outer loop is unconditional: connect with the last id you processed, consume until the socket closes, wait, connect again. The 429 in the table below counts connections, not frames.

const BASE = "https://api.mapier.ai";
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));

const onFrame = (name: string, data: unknown) => {
  console.log(name, data);
};

const MIN_DELAY = 6_000;
const MAX_DELAY = 30_000;

let lastEventId: string | undefined; // keep this somewhere durable
let delay = MIN_DELAY;

for (;;) {
  const startedAt = Date.now();

  try {
    const res = await fetch(`${BASE}/v1/response_stream?events=receipts,delivery_failures`, {
      headers: {
        Authorization: `Bearer ${process.env.MAPIER_API_KEY}`,
        Accept: "text/event-stream",
        ...(lastEventId ? { "Last-Event-ID": lastEventId } : {}),
      },
    });

    if (res.status === 429) {
      await res.body?.cancel();
      await sleep(Math.max((Number(res.headers.get("retry-after")) || 1) * 1_000, MIN_DELAY));
      continue;
    }
    if (!res.ok || !res.body) {
      await res.body?.cancel();
      throw new Error(`stream status ${res.status}`);
    }

    await consume(res.body, (id, name, data) => {
      onFrame(name, data);
      if (id) lastEventId = id; // only once the frame is handled
    });
  } catch (err) {
    console.warn("stream dropped, reconnecting", err);
  }

  delay = Date.now() - startedAt > 30_000 ? MIN_DELAY : Math.min(delay * 2, MAX_DELAY);
  await sleep(delay + Math.random() * delay);
}

The parser behind consume, and what to do with a command whose settlement you never saw, are in Reconnection and delivery guarantees.

Multiple connections

A key may hold four streams open at once, always; a connection past that may be refused with 429 rate_limited and details.reason: "too_many_streams", whose short details.retryAfter means close a stream rather than wait. Every connection receives every frame its filters admit — they are a broadcast, not a work queue. One stream per project is all an integration needs; two workers on one stream's frames should deduplicate on the frame's id.

Errors

StatusCodeCause
401invalid_credentialsNo key, or one that is unknown, revoked or expired
409project_archivedThe project is archived
422validation_failedAn events value this stream does not frame, a channel it does not carry or more than one, a conversationId this key cannot stream, or an x-mapier-version this build does not answer to; details names the field
429rate_limited10 connects per minute, per key; or more than 4 open at once (details.reason: "too_many_streams", whose short details.retryAfter means close a stream rather than wait)
500internal_errorAn unhandled failure, reported; details.requestId names the log line when present
503upstream_unavailableThe service could not open the stream
503service_busyThe platform had no database connection free in time; wait details.retryAfter seconds and connect again

Quick reference

Framesmessage, command, error; receipt, delivery_failed, location, moderation and poll on request (?events=)
Narrowing?channel=, ?conversationId= — one value each
Heartbeat: ping every 25s
ResumeLast-Event-ID; at most 500 per batch; events kept 90 days
Revocationre-proved every 60 seconds; an error frame, then the close
Concurrent streamsallowed, all receive every frame their filters admit
Rate limit10 connects per minute, per key; 4 open at once, per key

On this page