149 countries · crypto-native · no KYC

Designing SMS Conversation Flows: State Machines Over Spaghetti

A multi-step SMS conversation (confirm, reschedule, ask a question) is a state machine, whether you model it as one or not. Model it explicitly and it's maintainable; don't and it becomes if-statement spaghetti.

$0.035/msg from sub-100ms median 98.6% delivered
Designing SMS Conversation Flows: State Machines Over Spaghetti — smsroute
$0.004
per SMS from
149
countries
60s
to first message
6
crypto rails
The moment an SMS exchange has more than one step (you send, they reply, you respond based on the reply), you've built a state machine, whether you called it that or not. A two-way SMS flow like 'confirm your appointment (reply C), reschedule (reply R), or ask a question (reply Q)' has states, transitions, and inputs. Model it explicitly as a state machine and it stays maintainable as it grows; model it as a pile of if-statements keyed on 'what did they last say,' and it becomes SMS conversation flow spaghetti that breaks every time you add a branch. This guide is about the explicit version, because conversational SMS gets complex fast and the structure is what keeps it sane.

Every SMS conversation is a state machine

The moment an SMS exchange has more than one step (you send, they reply, you respond based on the reply), you've built a state machine, whether you called it that or not. A two-way SMS flow like 'confirm your appointment (reply C), reschedule (reply R), or ask a question (reply Q)' has states, transitions, and inputs. Model it explicitly as a state machine and it stays maintainable as it grows; model it as a pile of if-statements keyed on 'what did they last say,' and it becomes SMS conversation flow spaghetti that breaks every time you add a branch.

The core elements

The core elements — comparison diagram
Element What it is Example
State Where the conversation currently is awaiting_confirmation, rescheduling, done
Input What the user replied 'C', 'R', free text, no reply
Transition State + input → next state + action awaiting_confirmation + 'C' → confirmed, send receipt
Timeout What happens if no reply arrives After 24h in awaiting_confirmation → remind or close
Context Data carried through the conversation The appointment id, the user's number

Everything about a conversation reduces to these five. A user's inbound message is an input; you look up their current state, apply the transition for that (state, input) pair, take the action (send a reply, update a record), and move to the next state. Store the state and context keyed on the user's normalized number in international format (E.164), and the whole flow becomes a lookup rather than a tangle of conditionals.

The details that trip up conversations

  1. Handle unexpected inputs gracefullyUsers reply things you didn't anticipate: 'yes' instead of 'C', a question mid-flow, gibberish. Every state needs a default transition: re-prompt with the valid options, or route to a human, rather than silently dropping the message.
  2. Always honor global keywordsSTOP and HELP work in every state, no matter where the conversation is — they're not part of your flow's branching, they override it. This is a compliance requirement, not a design choice. Per CTIA guidelines, STOP must be honored globally.
  3. Set state timeoutsConversations don't always finish. A user who never replies leaves a dangling state; expire it after a sensible window (a reminder, then close) so stale states don't accumulate or misfire when the user replies days later.
  4. Plan the human handoffWhen the flow can't handle an input (a genuine question, an edge case, repeated confusion), hand off to a person cleanly, passing the conversation context so the human isn't starting blind.

The unexpected-input case is the one teams skip: always define a fallback reply for anything that isn't YES, STOP, or a recognized keyword.

Building it reliably

On the infrastructure side, a conversation flow is inbound handling plus state storage plus outbound sends — the pieces are already familiar. Inbound replies arrive on your webhook (handle them idempotently — a retried inbound shouldn't advance the state twice), you look up and update state, and outbound responses are normal sends with retry. The state machine sits on top, deciding what to say based on where the conversation is. For A2P messaging, ensure your application complies with carrier requirements like 10DLC registration in the US or DLT registration in India. Additionally, be aware of the GSM-7 160-character segment limit when crafting messages; longer texts require concatenation using UDH headers.

SMSRoute is a no-KYC SMS API with crypto billing (BTC, ETH, USDT, XMR, LTC, and SOL) providing the two-way delivery — inbound numbers, webhooks, and outbound sends; the conversation logic lives in your application, which is exactly where it belongs. And a masked-number conversation is just this pattern with an identity layer on top. SMSRoute's published route pages list delivery from $0.004/message (premium direct-carrier corridors up to $0.035) with sub-100ms median submission and ~98.6% delivered success (smsroute.cc route pages, 2026).

FAQ

How do I design a multi-step SMS conversation?
Model it explicitly as a state machine: define states (where the conversation is), inputs (what the user replied), transitions (state + input → next state + action), timeouts (what happens with no reply), and context (data carried through). Store state and context keyed on the user's phone number, so handling a reply becomes a lookup rather than tangled conditionals.
How should an SMS flow handle unexpected replies?
Give every state a default transition. Users reply things you didn't anticipate — 'yeah' instead of 'C', questions mid-flow, gibberish — so each state should re-prompt with valid options or route to a human rather than silently dropping the message. Naive flows that only handle expected inputs break constantly because real users don't reply on-script.
Do STOP and HELP work inside an SMS conversation flow?
Yes, and they must work in every state regardless of where the conversation is — they override your flow's branching rather than being part of it. This is a compliance requirement (CTIA/carrier rules), not an optional design choice, so wire STOP and HELP as global handlers that take precedence over any conversational state.
How do I hand off an SMS conversation to a human?
Build an explicit handoff path for when the flow can't handle an input — a genuine question, an edge case, or repeated confusion. Route the conversation to a person and pass along the accumulated context (what state they're in, what they've said) so the human continues rather than starting blind. Plan this from the start rather than bolting it on.

Send your first SMS in 5 minutes

No KYC. Pay with BTC, ETH, USDT, XMR, LTC, and SOL. Live routes to 149 countries.

Get an API key →