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

# Keyword auto-reply worked examples: match types, opt-outs, and precedence in action

> Five worked scenarios for keyword auto-reply rules — matching expectations for exact, contains, and starts-with rules, a full request-to-reply walkthrough, an opt-out keyword and its consent record, precedence between overlapping rules, and how to confirm a rule fired from the Inbox, Delivery log, and webhooks.

# Keyword auto-reply worked examples

The [Auto-Reply Rules guide](/guides/keyword-auto-reply-rules) covers the rule form: fields, match types, scope. This page goes the other direction — concrete scenarios with the exact request, the match outcome to expect, and where the evidence lands afterward. Work through them against your own workspace before you ship a keyword program to customers.

Prerequisites: an API key (live keys are prefixed `dv_live_sk_`), and at least one number on SMS (or your chosen channel) that can receive inbound replies.

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

## 1. Exact, contains, and starts-with with expected results

Matching is never case-sensitive, and Exact and Starts-with trim leading and trailing whitespace before comparing. The table below assumes four active rules on the `sms` channel, created in the order listed:

| Keyword   | Match type   | Action |
| --------- | ------------ | ------ |
| `HOURS`   | exact        | reply  |
| `SUPPORT` | contains     | reply  |
| `HELP`    | starts\_with | reply  |
| `JOIN`    | exact        | opt-in |

| Inbound body       | Result               | Why                                                                 |
| ------------------ | -------------------- | ------------------------------------------------------------------- |
| `HOURS`            | `HOURS` rule fires   | Exact equality after trimming and case folding.                     |
| `hours `           | `HOURS` rule fires   | Trailing whitespace is trimmed; case never matters.                 |
| `HOURS PLEASE`     | nothing matches      | Exact requires the whole body; nothing else covers this text.       |
| `need support now` | `SUPPORT` rule fires | Contains finds the keyword anywhere in the body.                    |
| `HELP me`          | `HELP` rule fires    | Starts-with accepts a keyword followed by more words.               |
| `PLEASE HELP`      | nothing matches      | Starts-with anchors at the beginning; Contains would match instead. |

Create any of these over the API to follow along:

```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": "HOURS",
    "channel": "sms",
    "match_type": "exact",
    "action": "reply",
    "response_text": "We are open Mon–Fri, 9am–5pm ET. Reply SUPPORT for a person."
  }'
```

**Response — 201**

```json theme={null}
{
  "data": {
    "id": "keywordRule_…",
    "keyword": "HOURS",
    "channel": "sms",
    "matchType": "exact",
    "action": "reply",
    "responseText": "We are open Mon–Fri, 9am–5pm ET. Reply SUPPORT for a person.",
    "agentId": null,
    "flowId": null,
    "active": true,
    "createdAt": "2026-08-28T09:12:41.000Z"
  },
  "meta": { "request_id": "req_…", "timestamp": "2026-08-28T09:12:41.000Z" }
}
```

## 2. An inbound text, one fired rule, end to end

A customer texts in; the reply goes out asynchronously. Here is the sequence with the evidence to expect at each step:

1. **The inbound arrives.** The customer sends `hours` to your SMS number from their handset.
2. **The platform matches it.** The body trims and case-folds to `HOURS`, which satisfies the exact rule above. Since this rule is the first — and only — match, its action executes.
3. **The reply is sent.** The response text goes out through the same billed outbound pipeline as any send. A plain body like the example is delivered verbatim; you can also reference the matched keyword or the sender's number with tokens, e.g. `"Thanks for asking about {{extra.keyword}} — we will text {{contact.phone}} during opening hours."` The `{{extra.from}}` and `{{extra.to}}` tokens carry the sender and the receiving number when you need them raw.
4. **Confirm in the dashboard.** The inbound appears in the **Inbox** conversation for that contact; the reply appears in the same thread, and as an outbound row across **Messages → Tools → [Delivery log](/guides/delivery-log)**. Because the log's filters map one-to-one onto the messages API, the API equivalent of the lookup is:

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

**Response — 200** (inbound and reply share the customer's number on `from`/`to` in opposite directions):

```json theme={null}
{
  "data": [
    {
      "id": "msg_b2c3d4e5f6a7b8c9d0",
      "channel": "sms",
      "direction": "inbound",
      "status": "received",
      "from": "+14155550100",
      "created_at": "2026-08-28T09:14:03.000Z"
    },
    {
      "id": "msg_c3d4e5f6a7b8c9d0e1",
      "channel": "sms",
      "direction": "outbound",
      "status": "delivered",
      "to": "+14155550100",
      "created_at": "2026-08-28T09:14:04.000Z"
    }
  ],
  "meta": {
    "request_id": "req_…",
    "timestamp": "2026-08-28T09:14:04.100Z",
    "pagination": { "cursor": null, "has_more": false, "total": 2 }
  }
}
```

The reply rows a rule produces are ordinary outbound messages — if the workspace has message suppression enabled, an identical body repeated inside the suppression window is collapsed like any duplicate send.

## 3. An opt-out keyword and the consent record change

Your own opt-out vocabulary (`QUIT`, `UNSUBSCRIBE`, brand-specific words) writes to the same consent records the built-in `STOP` handling maintains:

```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": "UNSUBSCRIBE",
    "channel": "sms",
    "match_type": "contains",
    "action": "opt-out",
    "response_text": "You are unsubscribed. Reply JOIN to start receiving messages again."
  }'
```

When a customer texts `please unsubscribe me`, the rule matches and two records change:

1. The contact's channel preferences flip to opted-out, and an entry is appended to the consent history on that contact: action `opt_out`, method `keyword`, keyword `UNSUBSCRIBE`, with the timestamp.
2. An explicit consent record is written per channel — the same opt-out evidence you can review through the [Opt-Out Lists API](/api-reference/endpoints/opt-out-lists) — so every later send to that contact is refused pre-send with `RECIPIENT_OPTED_OUT` until they opt back in.

Pair every opt-out keyword with the corresponding opt-in (`action: "opt-in"`) so the customer has a way back. The built-in `STOP`/`START` vocabulary is handled before any rule runs — it is not a rule, cannot be shadowed by one, and a rule on `STOP` never fires.

## 4. Precedence in action: oldest matching rule wins

Rules are evaluated in creation order, and one inbound fires at most one rule. Suppose these four exist on `sms`, created in this exact API call order:

| Created | Keyword | Match type        | Behavior on `YES` vs `YES please`                                                                   |
| ------- | ------- | ----------------- | --------------------------------------------------------------------------------------------------- |
| 1st     | `YES`   | exact             | Wins the bare `YES`; the trailing-word form falls through.                                          |
| 2nd     | `YES`   | contains          | Catches `yes please`, `YES that works`.                                                             |
| 3rd     | `YES`   | starts\_with      | Reached only if the contains rule is disabled or deleted.                                           |
| 4th     | `.*`    | regex (catch-all) | Fires on anything nothing earlier claimed — which, with 1–3 active, is everything except YES-forms. |

Flip the creation order — broad rules first — and the specifics never fire: the contains rule swallows every YES-form, and a catch-all created ahead of the rest swallows everything. To re-sequence, delete the broad rule and re-create it so it becomes the newest:

```bash theme={null}
curl -X DELETE "$ORBIT_API/settings/keyword-rules/keywordRule_…" \
  -H "X-API-Key: $ORBIT_API_KEY"
```

...then POST it again. One nuance for pagination consumers: the rules list returns **newest first** (the same order the dashboard table uses), while matching walks **oldest first** — read the API page from the bottom up when reconstructing evaluation order.

## 5. The testing harness: prove a rule fired

There is no rule-hit counter. A fired rule is observable through its effects — the record custodians below are where you prove what happened:

1. **Send a real inbound** from a handset to your number: the keyword, in the case you expect customers to use.
2. **Inbox.** The conversation shows the inbound turn and, for a reply action, the outbound reply in the same thread.
3. **Delivery log.** The reply shows as an outbound row; watch it traverse `queued` → `sent` → `delivered`. The row is an ordinary send — the status alone doesn't name the rule; the sibling inbound in the same conversation is the attribution.
4. **Webhooks.** If you subscribe to message status events, the reply's delivery notification arrives where your sender `id` correlates — the envelope below is the shape to expect. See [Webhook Events](/webhooks/events) for the full status vocabulary.

```json theme={null}
{
  "id": "evt_01J9Y3ZKEDP8K2X9MTVA6B1C03",
  "type": "message.delivered",
  "created_at": "2026-08-28T09:14:06.000Z",
  "data": {
    "message_id": "msg_c3d4e5f6a7b8c9d0e1",
    "channel": "sms",
    "to": "+14155550100",
    "status": "delivered"
  }
}
```

5. **Consent surface (opt-in / opt-out actions).** The customer's contact profile shows the flipped preference with the keyword entry in consent history; the [Opt-Outs view](/guides/opt-out-lists) lists the record.
6. **Flow or agent actions.** A `trigger_flow` match starts a run visible in the flow's run history (the keyword rides into the run context); a `forward_agent` match hands the turn to the deployed agent, visible in the conversation timeline.
7. **Audit log.** Rule *management* — create, update, delete — lands in **Settings → [Audit log](/guides/audit-log)** with the actor and timestamp, alongside the rest of your organization's actions. Use it with creation order to reconstruct why the wrong rule won when precedence surprises you.

When a rule that should match doesn't fire, check in order: the rule is **active**; the keyword casing is irrelevant but accents are not (`CAFE` ≠ `CAFÉ`); an Exact match is being defeated by invisible characters in the body (switch to Contains); or an earlier, broader rule is claiming the message first.

## See also

* [Auto-reply rules](/guides/keyword-auto-reply-rules) — fields, match types, and dashboard setup
* [Delivery log](/guides/delivery-log) — searching message deliveries and the API filter vocabulary
* [Opt-out lists](/guides/opt-out-lists) — reviewing opt-in/opt-out records
* [Webhook Events](/webhooks/events) — message status event payloads
* [Audit log](/guides/audit-log) — who created or changed a rule, and when
