> ## 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.

# Inbound message routing

> How tenant-level inbound (MO) SMS routing rules decide where a received message goes: priority-ordered match on number, sender, keyword, or regex, with webhook, inbox, queue, team, and SMS-menu targets — and what happens when no rule matches.

# Inbound message routing

Inbound message routing is the tenant-level rule engine that decides where a received (MO) message goes. Each rule tests one field of the incoming message and points at one destination. The first enabled rule that matches, in ascending priority order, wins; a message that matches nothing follows the same default path it always has.

This page explains the routing model and when to use it. Request and response schemas for the rule endpoints live in the [Messaging API reference](/api-reference/endpoints/messaging); the sibling concept [How routing picks a sender](/concepts/sender-and-routing) covers the outbound side of the same decision.

## Why inbound routing exists

Without rules, every inbound message on a number follows one fixed path: the per-DID inbound hook you configure on that number. That hook is a single destination per number, so it cannot express "when the body contains STOP, don't touch my application endpoint" or "messages to the support line go to the support team, but only when they mention a booking." Anything conditional had to live in your own webhook receiver.

Inbound routing rules move that conditional logic into the platform. A rule set can describe cases like:

* anything containing the keyword `appointment` goes to the scheduling webhook;
* anything from `+441632960000` lands in the VIP inbox;
* everything else is left to the default handling.

The per-DID hook and the rule set answer different questions. The hook owns "which URL should hear about messages to this number"; the rules own "which destination should this specific message go to." Keep the per-DID hook for the catch-all; add rules for the exceptions.

## How a message flows through the rules

```text theme={null}
Inbound (MO) message arrives
          │
          ▼
Load enabled rules, ordered by priority ascending
(ties broken by creation time, oldest first)
          │
          ▼
      First match? ── no ──▶ Fall-through: default routing
          │ yes                (inbox persistence + the
          ▼                     tenant-wide received event)
   Dispatch to the rule's target:
   webhook · queue · inbox · team · ivr · auto_reply · appointment
```

Evaluation is first-match-wins, not collect-all. Once a rule matches, later rules are never consulted.

Two consequences follow directly:

* **Priority is the ordering tool, not rule names or creation order.** A lower `priority` value evaluates first. Leave gaps between values (10, 20, 30) so you can insert a rule between two existing ones without reordering everything.
* **Specific rules need lower priority values than general ones.** If a broad keyword rule sits at priority 10 and a VIP-sender rule at priority 20, the broad rule consumes messages the VIP rule was meant to catch. Put the exceptions first.

## Match types: what a rule can test

Each rule inspects exactly one field of the inbound message, selected by `matchType`, and compares it to `matchValue`:

| `matchType` | Field tested                           | Comparison                                                            |
| ----------- | -------------------------------------- | --------------------------------------------------------------------- |
| `number`    | Destination DID the message arrived on | E.164 equality after normalisation (spaces, dashes, and case ignored) |
| `sender`    | Originating address (`from`)           | E.164 equality after the same normalisation                           |
| `keyword`   | Message body                           | Case-insensitive substring                                            |
| `regex`     | Message body                           | Case-insensitive regular expression                                   |

Three properties of the matcher are worth knowing before you build on it:

* **Keyword is a substring match, not a word match.** A rule on keyword `book` also matches "booking" and "rebooked". Use a `regex` rule with word boundaries (for example `\bbook\b`) when you need whole-word matching.
* **Regex rules are validated twice.** A syntactically invalid pattern is rejected with a 422 when you create the rule, and a pattern over 512 characters is refused. If a stored pattern somehow fails to compile at runtime, that rule simply never matches — it does not break evaluation of the rest of the rule set.
* **Equality is normalised.** `number` and `sender` comparisons strip whitespace and dashes and ignore case, so `+1 415-555-0100` in a rule matches the E.164 form the carrier delivers.

## Target types: where a matched message goes

`targetType` selects the destination; `targetConfig` carries its parameters.

| `targetType`               | `targetConfig`                                                      | What happens on a match                                                                      |
| -------------------------- | ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `webhook`                  | `url`, optional `secret`, optional `method` (`POST` default, `PUT`) | A signed JSON POST to your endpoint describing the routed message                            |
| `queue` / `inbox` / `team` | `queueId` / `inboxId` / `teamId`                                    | The matched conversation is handed to the inbox layer with an assignment to that destination |
| `ivr`                      | `menu` — a keyword menu tree                                        | A stateful SMS menu session opens for the sender                                             |
| `auto_reply`               | templated reply text, optional cooldown                             | One templated SMS reply is sent back to the sender                                           |
| `appointment`              | keyword sets plus reply copy                                        | The reply is classified as confirm/reschedule/cancel and acknowledged                        |

### Webhook targets

A webhook rule proves itself useful when the destination infrastructure is yours: your own receiver gets the routed message as a JSON POST with the sender, destination number, body, and the matched rule id.

Three guarantees apply to every webhook dispatch:

* **HTTPS only.** Rule creation rejects `http` URLs outright.
* **SSRF-guarded.** URLs that resolve to private, loopback, or link-local addresses are rejected both at rule-write time and again at dispatch time, so a rule cannot be used to make the platform call internal addresses.
* **Signed when a secret is set.** If you provide `targetConfig.secret` (minimum 8 characters), every dispatch carries an `X-Orbit-Signature: sha256=<hex>` header — an HMAC-SHA256 of the exact request body, keyed by your secret — so your receiver can verify provenance. Secrets are write-once: they are masked in list and get responses, and you rotate one by replacing it.

Dispatch is fire-and-forget from the sender's perspective. A slow or failing endpoint never delays or drops the inbound message; it only moves the rule's failure counters (below).

### Inbox destinations: queue, inbox, team

These targets keep the conversation inside Orbit's inbox product. A match publishes an assignment for the matched conversation to the named queue, inbox, or team, so the message lands with the right group of agents without passing through your infrastructure. Use these when the response to the message is a human replying from the dashboard.

### SMS menus: the `ivr` target

An `ivr` rule turns a matched inbound message into the start of an interactive SMS session — the messaging analogue of a voice IVR. `targetConfig.menu` carries the tree: a root node plus named nodes, each with a prompt and options keyed by what the sender texts back (`1`, `yes`, and so on). An option can send a reply, descend into another node, or both.

Sessions are stateful across messages with a sliding 30-minute TTL: the sender's next text continues the traversal rather than re-matching the rule set, and someone who walks away mid-menu silently drops out. The menu is validated at rule-write time — dangling branches, duplicate option keys, and oversize prompts fail creation with a 422 instead of failing live. Keep prompts inside 1600 characters (the 10-segment SMS cap used across the platform).

Replies the menu sends are ordinary outbound SMS and exit through the same routing any other outbound message uses.

### One-shot replies: `auto_reply` and `appointment`

These targets answer the inbound message themselves. `auto_reply` sends one templated reply, with an optional cooldown so a contact sending ten keyword messages gets one response, not ten. `appointment` classifies a reply against your confirm/reschedule/cancel keyword sets and sends the matching acknowledgement — enough for self-service appointment handling without a webhook round-trip.

## Failure tracking and the fall-through path

Every rule carries the same operational fields as the inbound-email routing feature: a consecutive `failureCount`, a `lastMatchedAt` timestamp, and a `lastFailureAt` timestamp. A successful dispatch resets the failure count to zero; a failed dispatch (a non-2xx response or a network error on a webhook target) increments it and stamps `lastFailureAt`. These counters are built for health badges and auto-pause behaviour on the dashboard: read them to spot a webhook endpoint that has been failing for a week.

The no-match path is deliberately unglamorous. When no enabled rule matches — or when the whole rule evaluation itself fails, because routing must never eat a message — the inbound message does exactly what it did before any rules existed: it persists to the inbox and fires the tenant-wide received webhook event. Rules are an overlay on that default, never a replacement for it. A tenant with zero rules pays zero overhead: the engine short-circuits before any of the session or matching logic runs.

## What inbound routing never does

Inbound routing is strictly inbound-side. It can match a received message and hand it to a destination; it cannot originate an outbound message. No match type and no target type will ever send a message on your behalf — targets either hand the conversation to the inbox layer or POST to your own endpoint, and even the reply-style targets (`ivr`, `auto_reply`, `appointment`) only reply to a message that already arrived, through the shared outbound sender-resolution path.

If you need to send a message that is not a reply — a campaign, a reminder, a notification — that is the outbound send API, with its own sender-resolution rules documented in [How routing picks a sender](/concepts/sender-and-routing). Inbound rules are never part of that path.

## Create a keyword-to-webhook rule

Creating a rule is one POST. This one routes any inbound message whose body contains the keyword `appointment` to your HTTPS endpoint, signs each dispatch, and evaluates before the default-priority rules:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/messages/inbound-routes \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Appointments to scheduling",
    "matchType": "keyword",
    "matchValue": "appointment",
    "targetType": "webhook",
    "targetConfig": {
      "url": "https://hooks.example.com/orbit/inbound",
      "secret": "whsec_rotate_me_regularly"
    },
    "priority": 10
  }'
```

The response carries the created rule, including its `smsr_…` id for later `PATCH` and `DELETE` calls. Creating, updating, and deleting rules requires the owner or admin role; listing is likewise restricted. For the full field list and the update/delete shapes, see the [Messaging API reference](/api-reference/endpoints/messaging).

## See also

* [Inbound message resolution](/concepts/inbound-message-resolution) — the tenant-ownership resolution layer (number index, routed states, hourly reconcile, per-DID override) underneath the rule engine
* [How routing picks a sender](/concepts/sender-and-routing) — outbound sender resolution and the sticky-reply side of inbound rules
* [Send and receive messages](/guides/send-receive-messages) — end-to-end walkthrough
* [Messaging API reference](/api-reference/endpoints/messaging) — request and response schemas for the inbound-routes endpoints
