Skip to main content

Two-way SMS conversations without the Inbox

Most two-way SMS needs a handful of webhook handlers and a thread map in your own database — not the Inbox product. If you call POST /messages/sms and subscribe to message.received, you already have everything a conversation loop is built from. This guide shows how to hold that loop yourself: receive the reply, attribute it to an ongoing thread, answer on the same sender, and upgrade each step to a managed feature only when the step outgrows a Map. You will:
  1. Receive replies on a webhook
  2. Track a thread per sender
  3. Use keyword auto-reply instead of code when it suffices
  4. Upgrade to the Inbox when humans enter the loop
  5. Reply through a sender pool
  6. Sample: receive, read the thread, reply
  7. Handle STOP on any thread message
  8. SMS, WhatsApp, and cross-channel threading

1. Receive replies on a webhook

Prerequisites: an API key (sandbox dv_test_sk_… while you build), an SMS-capable number, and a public HTTPS URL. The Send & Receive Messages quickstart covers the full send → track → receive setup; start there if this is your first message. Subscribe message.received plus the delivery lifecycle events on one endpoint:
Two rules keep a conversation handler honest:
  • Verify the X-Orbit-Signature header before reading the body, and deduplicate on the event id — delivery is at-least-once, so the same message.received can arrive twice.
  • Return 200 immediately and do the thread work asynchronously. Slow receivers cause retries, and a retried message.received is indistinguishable from a second reply unless you deduplicate.
If you have not stood up a receiver before, the first webhook quickstart walks the tunnel → register → verify → replay loop, and the webhook event catalog shows every message.* payload schema before you wire against it.

2. Track a thread per sender

SMS has no reply headers or in-reply-to metadata — a reply is a new inbound message that shares the two endpoints. Thread identity comes from the direction-aware endpoint pair: the customer’s number and your number, ordered so an inbound and an outbound from the same conversation hash to one key:
Normalize to E.164 before keying (+14155552671, not (415) 555-2671) and deduplicate inbound rows on message_id — not on the pair, or two contacts messaging one number collapse into a single thread. Store a thread record per key: thread_id, a sequence of { message_id, direction, body, status, timestamp } entries, a state field (open / pending_reply / closed), and the endpoint pair. Set pending_reply when your outbound goes out and open when the customer answers — that state is what “unanswered conversations” reporting and per-thread SLA timers hang off. Always reply on the same from the thread started with. Swapping numbers mid-thread splits the conversation into two threads on the customer’s handset.

3. Use keyword auto-reply when it suffices

Before you write conversation logic, check whether the reply the customer needs is deterministic. STATUS → tracking link, HOURS → opening times, HELP → support line: each is one inbound keyword matched to one stored reply, and that is exactly what the auto-reply layer executes — match a keyword, send a configured answer — without a state machine in your code. The keyword rules recipes page shows worked match outcomes per match type, precedence between overlapping rules, and where the evidence lands. Reach for your own thread logic when a reply depends on conversation state (“¿tienes la orden?” is not a keyword question you can pre-key), when more than one keyword pattern fires in a thread, or when keywords must join a transactional fetch — order status against live data. Configure the deterministic layer in the auto-reply rules guide; let your webhook handler branch to it instead of reinventing a matcher.

4. Upgrade to the Inbox when humans enter the loop

Strip the Inbox out of the loop until one of these breaks your Map:
  • Replies that need a person, or routing between teams — assignment, internal notes, and SLA timers are Inbox features, not thread-map fields.
  • Customers in more than one channel — the Inbox unifies SMS, WhatsApp, email, RCS, and web chat onto one queue with a single assignment model.
  • Supervision — a queue that needs macros, first-response SLAs, AI-drafted replies, and agents working the same thread.
Until then the webhook model covers it. When you cross one of those thresholds, stand the queue up with Inbox setup — inbound SMS keeps arriving on the same numbers, and your existing messages become conversations on the queue with no re-wiring. If you keep transactional traffic off the queue intentionally, the transactional inbox/no-conversation pattern keeps OTP-class traffic out of the Inbox while this page’s mapping holds.

5. Reply through a sender pool

If you send outbound through a sender pool, a reply must come back from whichever pool member the thread started on — otherwise the handset splits your reply into a second conversation. Pick the sticky strategy for anything conversational: the same recipient is pinned to the same sender on every send, so your outbound and the customer’s inbound share one number and your thread key stays stable across threads and days. Key the thread map on the effective sender — the specific pool member actually used for that customer — not on the pool id. With sticky semantics both are equivalent per customer, but read the sender off your own outbound send (or its delivery webhook) rather than re-deriving it from the pool. See sender pools for the sticky vs round_robin trade.

6. Sample: receive, read the thread, reply

The receive → fetch → reply loop reduces to two tables: a per-message row keyed on message_id, and a thread row keyed on the endpoint pair.
Flags worth noting:
  • The reply is a plain POST /messages/sms with to/from swapped — there is no special reply endpoint and no header to set.
  • “Fetch thread” is your table read; sending on the same from keeps the handset ThreadView together even though Orbit delivers SMS as individual messages.
  • The message.delivered status webhook for the reply is what flips pending_reply back — subscribe it and mark the delivery outcome on the thread as it lands.

7. Handle STOP on any thread message

Exit handling has to trigger regardless of which thread a message arrives on — a STOP on an appointment thread suppresses the customer, not the flow. Every tenant gets carrier-mandated STOP / HELP / START handling by default; per-brand aliases and response copy attach to a custom opt-out list. Suppression is enforced at send time, so a subsequent POST /messages/sms to an opted-out number returns an error rather than leaking a send. Your thread map only owns the bookkeeping, not the filtering: run opt-out detection on body before any keyword branching (a STOP on thread 3 closes all threads for that customer), suppress at the customer level — by phone number, not by thread — and treat STOP as a terminal thread state so your replyFor logic never answers a suppressed contact. The same pre-branch step is where START reopens threads after an opt-in returns. The preference-center opt-out page guide covers the customer-facing suppression surface if you want STOP to redirect into a page rather than terminate silently.

8. SMS, WhatsApp, and cross-channel threading

The map above is SMS-shaped: one number in the customer’s SMS app, message-pair threading. Two channel realities bound when the pattern needs replacing:
  • WhatsApp is threadlike but windowed. A free-form reply is only deliverable inside 24 hours after the customer’s last message; past that you fall back to pre-approved templates. The thread key holds, but the reply with replyFor must branch to a template send once the window closes.
  • Crossing SMS → web chat / email / voice mid-thread. Moving a conversation between channels without restarting it keeps one conversation id and full transcript — that is the continue a conversation on another channel guide, and it subsumes the to-from map once the thread leaves SMS.
Same-model flows stay on this page. Channel changes graduate to the cross-channel guide.

Next steps