> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Build a two-way SMS support line from scratch

> Assemble the full reply loop: inbound SMS routing, keyword grammar (STOP/START/HELP plus triage words), flow escalation for structured paths, and human handoff into the Inbox — with a complete order-status worked example.

# Build a two-way SMS support line from scratch

A two-way SMS support line is a number customers can text and get a correct answer back — instantly for the questions a keyword can answer, and through a human or an automation for everything else. The pieces exist as separate guides ([keyword rules](/guides/keyword-auto-reply-rules), [flow builder](/flows/builder), [inbound routing](/guides/inbound-sms-routing), [tickets](/guides/inbox-tickets-workflow)); this page is the assembly. You end with a working reply loop: inbound arrives → keyword decides → acknowledged in seconds, or escalated to a flow, or handed to a person — and every exchange stays in the conversation thread.

**You will:**

1. [Understand the two layers](#1-architecture-in-two-layers)
2. [Define the keyword grammar](#2-define-the-keyword-grammar)
3. [Build the START path as a flow](#3-the-start-path-as-a-flow)
4. [Wire escalation to a human agent](#4-escalation-to-a-human-agent)
5. [Read inbound replies via webhook](#5-read-inbound-replies-via-webhook)
6. [Run the complete order-status example](#6-worked-example-order-status)
7. [Hold the compliance guard](#7-compliance-guard)

## 1. Architecture in two layers

The line works because two layers split the traffic, each solving a different latency question:

* **Layer one — keyword rules** answer in seconds with zero agent time. `HOURS`, `LOCATION`, `STATUS ORDER-1234`, `HELP` — a reply the moment the keyword matches, no queue, no staff. Rules live under **Settings → Auto-Reply Rules**; the full reference is [Auto-Reply Rules](/guides/keyword-auto-reply-rules).
* **Layer two — flows and the Inbox** handle what a keyword cannot. Structured sequences (collect an order number, check an opt-in, confirm a status) run as flows built in the [Flow Builder](/flows/builder); unstructured conversation lands in the **Inbox** where a teammate continues the same thread.

Every inbound message passes through both layers in a fixed order, and the order is what you design against:

1. **Built-in consent handling.** `STOP` / `START` (and multilingual equivalents) are processed before anything else; keyword rules never see them.
2. **Keyword rules, in creation order.** Oldest matching rule wins; one message fires at most one rule.
3. **No match → the Inbox.** The message opens or continues a conversation for a human, unless you add a catch-all rule or a routing rule that sends it somewhere more specific.

A rule-scoped caveat: keyword rules only fire on **reply-capable numbers**. An alphanumeric sender ID cannot receive inbound, so the line needs a long code or short code that receives SMS before any of this is live.

## 2. Define the keyword grammar

Pick the vocabulary before you create a single rule — one intent per keyword, specific rules created before broad ones (evaluation is oldest-first, so creation order *is* precedence). A grammatically complete support line needs four families:

| Family | Keywords | Action |
| - | - | - |
| Consent (built-in) | `STOP`, `START` plus equivalents | Handled before your rules — you never configure these, and a custom `STOP` rule would never fire |
| Help | `HELP` | Auto-reply with contact options plus the mandated opt-out notice |
| FAQ | `HOURS`, `LOCATION`, your own info words | Auto-reply with the answer and a pointer back into the tree |
| Triage | `SUPPORT`, `STATUS`, `HUMAN` | Trigger a flow (structured) or forward to an agent / leave for the Inbox |

Two consequences to plan for. First, **HELP is your keyword** — the consent trio has built-in handling, but a `HELP` reply with support contact details and opt-out instructions is a tenant-owned message you author, on every channel you run. Second, **triage words separate fast from slow**: `STATUS` should trigger the order-status flow (section 6) rather than reply statically, because the answer depends on the order; `SUPPORT` and `HUMAN` can open a conversation and route it to a team. The match types, response-text rules, and management API are covered in [Auto-Reply Rules](/guides/keyword-auto-reply-rules); recyclable keyword patterns are collected in [Keyword rules recipes](/guides/keyword-rules-recipes).

## 3. The START path as a flow

`START` itself never reaches a flow — built-in consent handling records the re-opt-in first. What you build as a flow is the **welcome-back experience the re-opted-in contact should receive**: confirm they can receive messages again, restate what your line does, and point at the menu. The consent flip is the platform's; the acknowledgement copy and any conditional logic are yours, and a flow is the right home for that because you get condition nodes, opt-in checks, and branch paths for free.

Open **Flows → Create New Flow** (the full node reference is the [Flow Builder](/flows/builder)) and lay out three nodes:

1. **Trigger — Contact Action `opted_in`, Activation Condition `channel = sms`.** The flow starts when a contact re-consents on SMS.
2. **Condition — check the opt-in status you care about.** Branch on `contact.opt_in_status = opted_in` for the SMS channel so a partial or different-channel opt-in does not receive the "you're resubscribed" text by mistake.
3. **Send SMS — the confirmation.** Something like: `You're re-subscribed to <brand> alerts. Text HELP for help, STATUS <order#> for an order, or STOP to opt out.`

Over the API, the same flow is:

```bash theme={null}
export ORBIT_API_KEY="dv_live_sk_…"
export ORBIT_API="https://api.orbit.devotel.io/api/v1"

curl -X POST "$ORBIT_API/flows" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "SMS re-opt-in welcome",
    "trigger_type": "event",
    "definition": {
      "trigger": { "type": "event", "event": "contact.opted_in" },
      "nodes": [
        {
          "id": "check_sms_optin",
          "type": "condition",
          "data": {
            "expression": "{{contact.opt_in_status}} == \"opted_in\" && {{contact.channel}} == \"sms\""
          }
        },
        {
          "id": "send_welcome_back",
          "type": "sendSms",
          "data": {
            "to": "{{phone}}",
            "body": "You are re-subscribed. Text HELP for help, STATUS <order#> for an order, or STOP to opt out."
          }
        }
      ],
      "edges": [
        { "source": "check_sms_optin", "target": "send_welcome_back" }
      ]
    }
  }'
```

Test the live cycle from a handset: text `STOP`, confirm the outbound side goes quiet, text `START`, and expect the flow's send within seconds — the run appears in the flow's execution history with the trigger event attached. Where the consent record itself lives (per-channel suppression and re-admission) is documented in [Opt-out & opt-in rules](/guides/opt-out-rules).

## 4. Escalation to a human agent

A keyword that a flow cannot finish, or a message with no keyword at all, ends with a person. Two surfaces, chosen by the shape of the work:

* **Long-lived tracked work → Tickets.** "Refund request," "order arrived damaged" — a ticket carries a subject, priority, assignee, and SLA across days and agents. Create tickets from a flow (`HTTP Request` node against the tickets endpoint) or let operators open them from a conversation; the queue mechanics are in [Inbox Tickets workflow](/guides/inbox-tickets-workflow).
* **Live threaded conversation → the Inbox.** The unmatched branch of your keyword tree lands here by default. Add structure on top: a routing rule that assigns inbound SMS to a named team, or a catch-all keyword rule that forwards to a deployed AI agent before it ever waits on a human. Both patterns — with the exact catch-all JSON and the routing-rule precedence — are in [Keyword auto-replies with agent escalation, end to end](/guides/sms-keyword-agent-escalation).

When a triage keyword should escalate *immediately* (not land in the general queue), make the keyword rule itself the router: `HUMAN` with `action: "trigger_flow"` into a flow that opens a ticket and notifies the on-call channel, or a `forward_agent` action for the AI-agent-first variant. The rule still fires in layer one, so the acknowledgment is instant; the expensive part runs asynchronously in the flow.

## 5. Read inbound replies via webhook

The Inbox shows inbound replies to your team, but your own systems often need the same stream — to log threads in a CRM, trigger provisioning, or measure response latency. Two webhook surfaces cover it:

* **Delivery receipts (`message.sent`, `message.delivered`, `message.read`, `message.failed`)** tell you what happened to *outbound* messages — the auto-replies and flow sends your line produces. [Build a DLR-only webhook consumer](/guides/dlr-webhook-consumer) is the minimal receiver for exactly those four events, with HMAC verification, dedupe on the event id, and the `data.status` failure branch.
* **Inbound message events** deliver the reply side. The per-channel event list (SMS `message.received`, WhatsApp, RCS, Viber, and the voice `call.*` family) is in [Wire delivery-report webhooks per channel](/guides/wire-dlr-webhooks-per-channel); subscribe the same endpoint pattern the DLR consumer uses and branch on `data.channel`.

A practical split for a support line: inbound events update your ticketing/CRM integration; delivery events feed your alerting (a `message.failed` with `rejected` on the auto-reply side is a list-hygiene signal, not an agent signal). If you would rather route inbound SMS to your own endpoint *before* the keyword layer, that is the pre-inbox [inbound SMS routing](/guides/inbound-sms-routing) engine — rules with `number` / `sender` / `keyword` / `regex` match conditions targeting a webhook, ordered by priority. Keyword rules still run after routed webhooks ack; use routing only when your endpoint must see the message before the line does.

## 6. Worked example: order status

The complete loop for the most common support keyword — an order-status line where `STATUS <order#>` returns live order data, `HELP` explains the menu, `SUPPORT` opens a tracked ticket, and everything else falls to the team. Run it top to bottom against a reply-capable number.

### Step 1 — create the status lookup flow

A flow that takes the inbound text, extracts the order number, calls your order API, and replies:

```bash theme={null}
curl -X POST "$ORBIT_API/flows" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Order status lookup",
    "definition": {
      "trigger": { "type": "incoming_sms" },
      "nodes": [
        {
          "id": "lookup_order",
          "type": "httpRequest",
          "data": {
            "url": "https://yourapp.example/api/orders/{{trigger.body}}",
            "method": "GET",
            "headers": { "Authorization": "Bearer …" }
          }
        },
        {
          "id": "reply_status",
          "type": "sendSms",
          "data": {
            "to": "{{trigger.from}}",
            "body": "Order {{lookup_order.id}}: {{lookup_order.status}}. Reply SUPPORT to talk to the team."
          }
        }
      ],
      "edges": [
        { "source": "lookup_order", "target": "reply_status" }
      ]
    }
  }'
```

The definition powers both the builder canvas and the API — validate it before publishing with `GET /api/v1/flows/<id>/validate`, then deploy. Keep the flow's output variable naming (`lookup_order`) readable; the reply body references it as a `{{…}}` placeholder.

### Step 2 — register the keyword rules, specific before broad

```bash theme={null}
curl -X POST "$ORBIT_API/settings/keyword-rules" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "keyword": "STATUS",
    "channel": "sms",
    "match_type": "starts_with",
    "action": "trigger_flow",
    "flow_id": "flw_…"
  }'

curl -X POST "$ORBIT_API/settings/keyword-rules" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "keyword": "HELP",
    "channel": "sms",
    "match_type": "exact",
    "action": "reply",
    "response_text": "Text STATUS <order#> for order updates, SUPPORT for the team, STOP to opt out."
  }'

curl -X POST "$ORBIT_API/settings/keyword-rules" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "keyword": "SUPPORT",
    "channel": "sms",
    "match_type": "exact",
    "action": "reply",
    "response_text": "A teammate is on it — reply here with anything useful and it goes straight to them."
  }'
```

`Match type: starts_with` lets `STATUS ORDER-1234` fire the flow with the full body as its trigger context. Create `STATUS` and `HELP` before any fallback — if you add one — because the oldest matching rule wins.

The same tree as the rules JSON for a seed script or an environment clone (verbatim request bodies, created in this order):

```json theme={null}
[
  {
    "keyword": "STATUS",
    "channel": "sms",
    "match_type": "starts_with",
    "action": "trigger_flow",
    "flow_id": "flw_…"
  },
  {
    "keyword": "HELP",
    "channel": "sms",
    "match_type": "exact",
    "action": "reply",
    "response_text": "Text STATUS <order#> for order updates, SUPPORT for the team, STOP to opt out."
  },
  {
    "keyword": "SUPPORT",
    "channel": "sms",
    "match_type": "exact",
    "action": "reply",
    "response_text": "A teammate is on it — reply here with anything useful and it goes straight to them."
  },
  {
    "keyword": ".*",
    "channel": "sms",
    "match_type": "regex",
    "action": "reply",
    "response_text": "Thanks for reaching out — a teammate will reply shortly."
  }
]
```

### Step 3 — route the support branch to a team

The `SUPPORT` keyword acknowledges instantly; route the actual SMS channel to the support team so every thread that keyword opens has an owner:

```bash theme={null}
curl -X POST "$ORBIT_API/inbox/routing-rules" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "SMS support line → support team",
    "priority": 10,
    "conditions": { "channel": ["sms"] },
    "action": { "type": "assign_team", "target_id": "team_support" }
  }'
```

### Step 4 — subscribe for receipts and inbound events

```bash theme={null}
curl -X POST "$ORBIT_API/webhooks/endpoints" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourapp.example/webhooks/orbit",
    "events": ["message.received", "message.delivered", "message.failed"]
  }'
```

Store the `whsec_…` signing secret from the response immediately; it is shown once.

### Step 5 — prove the loop from a handset

1. Text `STATUS ORDER-1234` — expect the flow's reply in seconds, and a run in the flow's execution history.
2. Text `HELP` — expect the menu reply.
3. Text `SUPPORT` — expect the acknowledgement and a conversation assigned to `team_support` in the Inbox.
4. Text free-form ("where is my refund?") — expect no keyword reply, and the message waiting in the same team's queue.
5. Read back both directions to confirm the loop recorded cleanly:

```bash theme={null}
curl "$ORBIT_API/messages?direction=inbound&q=%2B14155550100" \
  -H "X-API-Key: $ORBIT_API_KEY"
```

Each matched keyword shows as an inbound/outbound pair on the same number pair; unmatched inbound shows only the inbound row plus the conversation the routing rule assigned.

## 7. Compliance guard

The line you just built touches consent at three points; each is a tenant-owned control, and the ordering is the guard.

* **STOP/START run before everything.** Built-in consent handling processes the opt-out/opt-in vocabulary before keyword rules, routing rules, and flows. Consequences you design around: never create a custom `STOP` keyword rule (it can never fire); never treat a keyword reply as a channel for a suppressed contact — an outbound Auto-Reply to an opted-out contact fails at send time, which is correct, and the send failure appears on the delivery side.
* **HELP goes out on ask, not on a schedule.** Your `HELP` reply copy — support contact plus opt-out instructions — is yours to author and keep accurate per channel. Add brand-specific aliases (`QUIT`, `UNSUB`) on the [Opt-out & opt-in rules](/guides/opt-out-rules) page when your audience uses them; they write to the same consent record the built-in words use.
* **Preserve the thread through handoffs.** A keyword-triggered reply, a flow send, and a human reply all stay on one conversation as long as you reply **from the same number the customer texted**. Swapping senders mid-thread splits the conversation on the customer's handset and strands the history the next agent needs. During escalation — ticket opened, team assigned, agent forwarded — the inbound keyword and its auto-reply are ordinary messages on that thread, so the human who picks it up starts with the full context instead of a cold ticket.
* **Quiet hours apply to what your line initiates.** Keyword replies answer an inbound, but a flow or agent that *initiates* outreach travels the marketing pipeline — gate it per channel in [Quiet hours](/guides/quiet-hours-configuration) before traffic arrives.

## See also

* [Auto-Reply Rules](/guides/keyword-auto-reply-rules) — match types, precedence ordering, and the rules API
* [Keyword rules recipes](/guides/keyword-rules-recipes) — match-type expectations across common patterns
* [Keyword auto-replies with agent escalation](/guides/sms-keyword-agent-escalation) — catch-all rules, agent forwards, routing into teams, SLA clocks
* [Flow Builder](/flows/builder) — every node type and the definition format the flows API accepts
* [Inbound SMS routing](/guides/inbound-sms-routing) — pre-inbox rules that target webhooks, queues, and inboxes
* [Two-way SMS conversations](/guides/sms-two-way-conversation) — the thread map beneath the Inbox
* [Inbox Tickets workflow](/guides/inbox-tickets-workflow) — assignments, SLA, and status audit for tracked work
* [Build a DLR-only webhook consumer](/guides/dlr-webhook-consumer) — minimal receiver for delivery events
* [Wire DLR webhooks per channel](/guides/wire-dlr-webhooks-per-channel) — event families per channel, including inbound
* [Opt-out & opt-in rules](/guides/opt-out-rules) — built-in consent plus brand-owned vocabulary
* [Quiet hours](/guides/quiet-hours-configuration) — the per-channel gate for initiated outreach


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.