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

# Frequency caps: per-contact rolling send limits

> Set rolling-window send limits per contact in Orbit so you never over-message a person across campaigns, flows, or direct API sends on any channel.

# Frequency Caps

A frequency cap is a rule that says "don't send this contact more than N messages of this kind within a rolling time window." It exists to protect the two things over-messaging damages: the recipient's patience (and their opt-out rate) and your sender reputation with carriers. Frequency caps are enforced centrally, inside the message-send pipeline, so the limit holds regardless of which campaign, flow, or API call is trying to send — a contact who already hit today's cap from Campaign A is also blocked from Campaign B.

For the full endpoint list and request schema, see the [Frequency Caps API reference](/api-reference/frequency-caps).

## When to use a frequency cap

Use a frequency cap when the risk is **too many sends to the same person**, not a duplicate message body (that's [message suppression](/guides/message-suppression)) and not a hard channel block (that's an [opt-out](/api-reference/optouts)). Typical rules:

* "No more than 3 marketing SMS per contact per day."
* "No more than 1 promotional WhatsApp message per contact per week."
* "No more than 5 messages per contact per day, across every channel combined."

## Channels and scope

Every cap counts sends on one of these channels, or across all of them:

`sms`, `whatsapp`, `email`, `voice`, `rcs`, `viber`, `line`, `messenger`, `instagram`, `push`, `telegram`, `in_app`

A cap's `scope` decides what counts toward its limit:

| Scope               | What `max_count` counts                                                                                                                                                                                                     | What the API returns                 |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| `channel` (default) | Sends to the recipient on the one `channel` you named. Send 2 SMS + 2 emails to the same contact and an `sms`-channel cap sees 2 of its slots used.                                                                         | `channel: "sms"`, `scope: "channel"` |
| `global`            | Sends to the recipient **on every channel combined**. One SMS + one email + one push to the same contact is 3 toward the shared limit — so a global cap CAN gate a cross-channel send even though its own channel is clear. | `channel: "*"`, `scope: "global"`    |

Evaluation is shared across the whole `/api/v1/frequency-caps` surface: the rules you `GET /api/v1/frequency-caps` are exactly the rules the send pipeline evaluates, and both the dashboard (Settings → Frequency caps) and the API read them through the same definitions. Two constraints on the schema to know about up front:

* A `global` cap must **omit** `channel` — the API stores it as the `*` wildcard and rejects a create that names both (validation error: `channel must be omitted when scope='global'`).
* A `channel`-scoped cap **requires** `channel` — omitting it with the default scope is a validation error.

## Categories: keyless means fire-on-every-send

`applies_to_categories` is the only filter between a cap and the sends it gates:

* **`applies_to_categories` absent (or null)** — the cap applies to **every** send to the recipient, no matter the category. OTPs, receipts, drip steps, reactivation offers: all of them consume slots.
* **`applies_to_categories: ["marketing"]`** — the cap only fires on sends whose `category` matches one of the listed values. A send with no category, or with `category: "transactional"`, ignores this cap entirely.

The keyless behavior trips people up in one specific place: a bare-and-blanket cap created "just to throttle marketing" with no category filter fires against your transactional traffic too. A contact who receives three password-reset OTPs inside the window can get their fourth transactional send rejected by a cap you thought was marketing-only. Scope your caps deliberately — either list the categories you actually want to throttle, or keep the blanket cap but size it so transactional traffic can never hit the ceiling (see the playbooks below).

## Creating a cap

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/frequency-caps/ \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "sms",
    "applies_to_categories": ["marketing"],
    "max_count": 3,
    "window_seconds": 86400
  }'
```

| Field                   | Notes                                                                                                                                                                                                                                |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `channel`               | One of the channels listed above. Required for a channel-scoped cap; must be omitted when `scope` is `global`.                                                                                                                       |
| `scope`                 | `channel` (default) counts sends on that one channel only. `global` counts a contact's sends across every channel toward one shared limit — use this for an org-wide "don't message anyone more than N times a day, period" ceiling. |
| `window_seconds`        | Rolling window length, 60 seconds to 30 days.                                                                                                                                                                                        |
| `max_count`             | Sends allowed inside the window, 1–10,000.                                                                                                                                                                                           |
| `applies_to_categories` | Restrict the cap to specific send categories (for example `["marketing"]`). Omit it to apply the cap to every category.                                                                                                              |
| `enabled`               | Defaults to `true`. Set `false` to keep a rule on file without enforcing it.                                                                                                                                                         |

## Managing rules

The full surface is plain REST over the org's rule list:

| Call                                | Use                                                                                                                                               |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET /api/v1/frequency-caps/`       | List every rule, newest first — read this back after a create to confirm what other caps are already in force (two overlapping rules both count). |
| `GET /api/v1/frequency-caps/:id`    | Read one rule's window, quota, scope, and category filter before changing it.                                                                     |
| `PATCH /api/v1/frequency-caps/:id`  | Change any field (`window_seconds`, `max_count`, `applies_to_categories`, `enabled`, …). Send at least one field.                                 |
| `DELETE /api/v1/frequency-caps/:id` | Remove a rule. Deletion takes effect within seconds and returns `204 No Content`.                                                                 |

Writes require an owner, admin, or developer role; reads only need a valid API key. Changes propagate within about 30 seconds — rule changes invalidate a short-lived rule cache, so a cap you just disabled stops gating within half a minute, not per-send.

## When a cap is hit: `FREQUENCY_CAP_EXCEEDED`

A direct API send that would exceed an active cap is rejected with HTTP 429 and never dispatched — it does not queue, does not retry, and does not burn a slot against any other cap. The error body pinpoints the rule that fired:

```json theme={null}
{
  "error": {
    "code": "FREQUENCY_CAP_EXCEEDED",
    "message": "Send blocked by a frequency-cap rule for this recipient and channel. Try again later or raise the cap in Settings → Frequency caps.",
    "details": {
      "channel": "sms",
      "window_seconds": 86400,
      "max_count": 3,
      "cap_id": "fc_9Qx2...",
      "retry_after_seconds": 4180
    }
  }
}
```

Read it as:

* **`details.cap_id`** — which rule blocked the send. `GET /api/v1/frequency-caps/:id` with this id to see (and adjust) the full rule.
* **`details.retry_after_seconds`** — how long until the oldest in-window send ages out and a slot re-opens. Treat it like a `Retry-After`: wait this many seconds before retrying this recipient, not a fixed backoff.
* **`details.channel` / `window_seconds` / `max_count`** — a copy of the rule's shape, so you don't need a second round-trip to log what fired.

Handle the 429 in your send loop: catch `code === "FREQUENCY_CAP_EXCEEDED"`, mark the recipient as deferred until `retry_after_seconds` elapses, and move on to the next recipient. Do **not** treat it as a delivery failure or a provider error — the carrier never saw the message, and re-POSTing immediately will 429 again.

Campaign sends behave differently by design: a capped campaign recipient reports back `status: "skipped", reason: "frequency_capped"` (with the same `frequency_cap_id` and `retry_after_seconds` fields) instead of failing the batch, so one capped contact never stops the rest of the audience from sending.

## Where the cap sits relative to opt-outs and suppression

Three independent controls answer three different questions, and they run in a fixed order inside the send pipeline:

| Order | Control                                            | Question it answers                                                         |
| ----- | -------------------------------------------------- | --------------------------------------------------------------------------- |
| 1     | [Opt-out](/api-reference/optouts)                  | "Has this contact told us to stop messaging them on this channel entirely?" |
| 2     | [Message suppression](/guides/message-suppression) | "Did this exact message body already reach this contact recently?"          |
| 3     | **Frequency cap**                                  | "Has this contact already received too many sends in this window?"          |

Ordering matters for your cap budget. An opted-out contact is blocked at step 1, so their sends never reach the cap. A send suppressed as a duplicate at step 2 is skipped **without consuming a cap slot** — the suppression hit means the cap counter doesn't move, which is exactly what you want: duplicates can't burn the allowance you reserved for fresh messages. Conversely, a capped send also consumes nothing: on the deny path, no slot is recorded against any cap, so a rejected send can't push a contact *over* the very limit that rejected it.

One nuance on the check itself: slot consumption is atomic with the check — two concurrent sends to the same contact can't both slip past a `max_count: 1` rule, and a send rejected by one cap never claims slots on the other caps it passed.

## Bypassing the cap for transactional sends

Tag time-sensitive sends — OTPs, receipts, appointment reminders — with `category: "transactional"` on the send request, and scope your caps to `applies_to_categories: ["marketing"]` (or similar) so those caps never fire on them. A cap with no `applies_to_categories` applies to every category, transactional included, so be deliberate about scoping caps you only intend for promotional traffic.

## Playbooks

### Marketing-only daily SMS cap

The most common configuration: 3 marketing SMS per contact per rolling day, transactional untouched.

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/frequency-caps/ \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "sms",
    "applies_to_categories": ["marketing", "promotional"],
    "max_count": 3,
    "window_seconds": 86400
  }'
```

The category list covers both labels your campaigns use; an OTP tagged `transactional` (or untagged) never counts.

### Global 24-hour hard ceiling

One rule that caps a contact at 5 messages per day **no matter which channel** — the anti-fatigue ceiling for orgs that send SMS + email + push in parallel:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/frequency-caps/ \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "scope": "global",
    "applies_to_categories": ["marketing"],
    "max_count": 5,
    "window_seconds": 86400
  }'
```

Note `channel` is omitted entirely — it comes back as `"*"` in the response. Because the counter aggregates across channels, a contact who got 3 marketing SMS and 2 marketing emails today is at their ceiling: the next marketing push notification is rejected with `FREQUENCY_CAP_EXCEEDED` even though the push channel itself has no rule.

### Per-category mixing: marketing capped, transactional separate

Two rules side by side, each clean:

```bash theme={null}
# Marketing: generous daily limit on the promo channel
curl -X POST https://api.orbit.devotel.io/api/v1/frequency-caps/ \
  -H "X-API-Key: $DV_API_KEY" -H "Content-Type: application/json" \
  -d '{"channel": "email", "applies_to_categories": ["marketing"], "max_count": 2, "window_seconds": 86400}'

# Transactional: a backstop that only fires if something is genuinely wrong
# (e.g. 20 OTPs/day to one contact usually means abuse of the OTP endpoint,
# not a deliverability preference)
curl -X POST https://api.orbit.devotel.io/api/v1/frequency-caps/ \
  -H "X-API-Key: $DV_API_KEY" -H "Content-Type: application/json" \
  -d '{"channel": "sms", "applies_to_categories": ["transactional"], "max_count": 20, "window_seconds": 86400}'
```

Each send evaluates only the rule(s) whose category list includes its own category — the two counters never interfere, and a busy marketing day can't push a receipt over the transactional backstop (or vice versa).

### Drip campaigns and flows

A drip campaign (see [campaign-end-to-end](/guides/campaign-end-to-end)) sends through the same pipeline as everything else, so caps apply with no campaign-side configuration — but the recipient experience is a skip, not an error:

* Each drip step evaluates the caps at send time, one recipient at a time. A capped recipient is reported as `status: "skipped", reason: "frequency_capped"` and the batch moves on; your campaign-level budget counters refund the skipped send, so capping a recipient never bills you.
* Because caps are rolling windows, the same recipient may succeed on the next drip step a day later — delays between steps are what lets their window reopen. If you see large `frequency_capped` skip counts on a drip, either spread the steps wider or loosen the cap, don't retry the step.
* Before launching, use the audience preview's advisory capped estimate (`frequency_capped` on the campaign preview response) to size how many sends the existing caps will hold back — it's a snapshot, but it catches "my org-wide global cap will eat half this campaign" before you send.

## See also

* [Frequency Caps API reference](/api-reference/frequency-caps) — endpoint list and full field reference
* [Message Suppression](/guides/message-suppression) — content-hash duplicate suppression
* [Opt-Outs API](/api-reference/optouts) — per-channel opt-out records
* [Campaign end-to-end](/guides/campaign-end-to-end) — how caps surface as `skipped` sends in campaigns and previews
* [Campaign A/B testing](/guides/campaign-ab-testing) — how experiment cohorts (variants, hold-outs) interact with the same per-contact cap gates
