Inbound replies

Route inbound SMS — including first-contact texts to your number — to a webhook or the Beep inbox, with signed, retried delivery.

Inbound replies are messages from your recipients. Beep routes every inbound SMS to a reply destination — a webhook you own, or the Beep inbox. This is the opposite direction from status callbacks, which report on messages you send.

Cold inbound is supported

A common worry: "Does this only forward replies to messages we sent, or also texts from numbers we've never contacted?"

Both. If you publish your Beep number as a support line and someone texts it first — before you've ever messaged them — that message still forwards. The first inbound creates a conversation and routes through your workspace default reply destination (see resolution order below). Nothing is dropped on the floor.

The one requirement: a workspace default must be configured. If you set one, every inbound — including first-contact texts — forwards to it from the very first message.

Destination types

TypeWhat it does
webhookPOSTs the inbound message as a signed JSON body to a URL you own
beep_inboxNo external delivery — the reply lives in the Beep inbox

A new workspace ships with a default beep_inbox destination, so replies are always visible in-app even before you wire an external integration.

Set up a destination and the workspace default

Dashboard: Workspace Settings → Webhooks.

  1. Add destination → choose Webhook, give it a name and your HTTPS URL. Beep generates a signing secret (shown once) or you can set your own.
  2. In Default routing & status callbacks, set Default reply destination to your new destination and save.

That second step is what makes cold inbound forward to your endpoint.

API: create destinations and set the workspace default with the reply-destination and notification-settings endpoints (manager- or admin-role key). See the API Reference for POST /api/v1/reply-destinations and PUT /api/v1/notification-settings.

How a reply's destination is resolved

For each inbound message, Beep picks the destination in this order:

  1. The conversation's reply_destination_id — set by the most-recent outbound in that thread (re-stamped on every send).
  2. The per-number default of the Beep number the reply arrived at (set per number in Settings → SMS, or via PATCH /api/v1/phone-numbers/{id} with reply_destination_id).
  3. The workspace default (default_reply_destination_id).

If none resolves — or the resolved destination is inactive or deleted — the reply stays in the Beep inbox and nothing is forwarded.

For a first-contact text there's no prior outbound, so it falls through to the number's per-number default (step 2), then the workspace default (step 3). That's how a support number can route its own inbound — and why a workspace default catches everything else.

Payload (webhook destinations)

{
  "event": "message.received",
  "event_id": "8f3c1e2a-...",
  "workspace_id": "20000000-...",
  "conversation_id": "c1d2e3f4-...",
  "from": "+15557654321",
  "to": "+15551234567",
  "body": "Yes, please call me back",
  "received_at": "2026-06-22T15:04:05.000Z",
  "reply_destination_id": "dd000000-...",
  "message_id": "a1b2c3d4-...",
  "conversation_url": "https://app.beepmessaging.com/workspace/20000000-.../inbox?conv=c1d2e3f4-..."
}
  • from is the customer; to is your Beep number.
  • message_id is the Beep message ID of the inbound reply. It is stable across forwarding retries and is the same ID GET /api/v1/messages/{id} accepts.
  • event_id identifies this forward and is stable across retries: every delivery attempt of the same reply carries the same value. Use it, or message_id, for idempotent processing.
  • workspace_id is never null.
  • conversation_id ties replies in the same thread together. It is always set on a live inbound reply. It is null in exactly two cases: the synthetic payload sent by Test on a reply destination (there is no conversation behind it), and a reply whose conversation was deleted before the forward went out.
  • conversation_url is a deep link to the thread in the Beep inbox. It is present only when conversation_id is set and absent otherwise (never null). The page requires a signed-in Beep user with a seat in that workspace; it is not a public or shareable link and carries no authentication of its own.

If the reply carried media (MMS)

Available in production: the inbound MMS extension was released September 8, 2026.

A reply with a photo or other media adds one more key, attachments, appended after conversation_url. It holds metadata only — no URL and no bytes.

"attachments": [
  {
    "id": "aa000000-...",
    "content_type": "image/jpeg",
    "byte_size": 148230,
    "moderation_status": "visible",
    "created_at": "2026-06-22T15:04:05.100Z"
  }
]

The key is absent — never [] — on a text-only reply, so a body without media is byte-for-byte identical to what it was before attachments existed. See Inbound attachments for how to turn an id into bytes.

Verify the signature

Inbound webhooks are signed with the same HMAC scheme as status callbacks — X-Beep-Signature: sha256=<hmac> and X-Beep-Timestamp, keyed with the secret you set on that reply destination. See Status callbacks → Verify the signature for the verification steps and a code sample.

Each reply destination signs with the secret you configured on it, so inbound webhooks are always verifiable.

STOP and other compliance keywords never forward

Inbound STOP, HELP, and START are handled by Beep for compliance and are never delivered to an external reply destination. (A STOP does still emit a message.opt_out status callback — that's the sender-facing channel, not reply forwarding.)

Retries & idempotency

Forwarding is deduped per (message_id, reply_destination_id) and retried on the same fixed schedule as status callbacks:

1m → 5m → 30m → 2h → 24h   (5 retries, then exhausted)

Make downstream processing tolerant of repeated deliveries. event_id is stable across forwarding retries, and message_id identifies the underlying reply, so deduplicate on either. For attachment processing, deduplicate by attachment id when present.

The Test button sends a synthetic payload

Test on a reply destination (or POST /api/v1/reply-destinations/{id}/test) sends a message.received body with the same keys as a live reply, except that conversation_id is null, conversation_url is absent, and message_id is a random UUID that resolves to nothing. A live reply only ever looks like this when its conversation was deleted before forwarding, so a handler that passes the Test button already handles that edge.

Related

MMS delivery timing and unavailable attachments

Beep waits for MMS media to be stored before forwarding message.received.
Text-only replies are forwarded immediately. attachments contains only stored
parts, so normal ingestion needs no polling. If any part fails, or media remains
unready five minutes after receipt, attachments_unavailable: true accompanies
the reply and any successfully stored parts. Render an unavailable notice;
there is no follow-up media-ready webhook. SMIL layout parts are ignored.

Use test-key recipients +15005550107 and +15005550108 to exercise a sample
image and unavailable-media result respectively. See Inbound attachments.


Did this page help you?