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
| Type | What it does |
|---|---|
webhook | POSTs the inbound message as a signed JSON body to a URL you own |
beep_inbox | No 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.
- 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. - 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:
- The conversation's
reply_destination_id— set by the most-recent outbound in that thread (re-stamped on every send). - 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}withreply_destination_id). - 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-..."
}fromis the customer;tois your Beep number.message_idis the Beep message ID of the inbound reply. It is stable across forwarding retries and is the same IDGET /api/v1/messages/{id}accepts.event_ididentifies this forward and is stable across retries: every delivery attempt of the same reply carries the same value. Use it, ormessage_id, for idempotent processing.workspace_idis never null.conversation_idties replies in the same thread together. It is always set on a live inbound reply. It isnullin 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_urlis a deep link to the thread in the Beep inbox. It is present only whenconversation_idis set and absent otherwise (nevernull). 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
- Inbound attachments — fetching media from an MMS reply
- Status callbacks — the sender-facing direction
- Opt-outs & consent — how STOP and suppression work
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.
Updated 15 days ago