Frequency Caps API
Define rolling-window limits on how often a single contact can be messaged. Enforcement happens insidemessages.service.sendMessage against Redis sorted sets — sends that would exceed the cap return a FREQUENCY_CAP_EXCEEDED error and don’t dispatch.
Base path: /api/v1/frequency-caps
Authentication: API key (X-API-Key) or session JWT.
Using the SDKs
client.request() escape hatch above. See the Python SDK.
Returns the typed ApiResponse envelope. See the SDK index at SDK quickstart.
Example — cap marketing SMS at 3 per day per contact
category: "transactional" tag on the send request.
Recipes — cookbook-style
Recipe 1: consent-gated outbound SMS
Plan a marketing blast that respects a per-contact daily cap. This walks the full hand-shake: create the cap, confirm the opt-in state, send withcategory: "marketing", then handle the FREQUENCY_CAP_EXCEEDED shape the pipeline returns when the recipient has already received all their slots.
Step 1 — Create a channel-scoped marketing SMS cap (3 per day)
GET /api/v1/contacts/{id}/channel-state returns every channel the contact can be reached on plus suppression flags; the sms.opted_out flag inside channel_preferences gates send eligibility.
category="marketing" so the cap fires for the marketing traffic you intended it to
429 FREQUENCY_CAP_EXCEEDED. Your app should back off and retry only after retry_after_seconds (uses the oldest entry’s expiry to compute the next window).
channel_preferences.sms.opted_out === true (or the email is in the suppression list you pass to step 2), the send is rejected at the compliance gate before the frequency-cap check runs.
Recipe 2: transactional bypass
When you scope the cap to["marketing"] you’re promising that OTPs and receipts ride free. Send a transactional message tagged category="transactional" and it skips the marketing cap entirely — it never claims a cap slot, it never gets blocked when a marketing recipient is over their limit.
duplicate_content body inside the suppression window is skipped, and an email listed in email_suppressions blocks the email channel outright. Tagging category="transactional" shifts opt-in/off the cap’s cross-category exemption only.
A transactional send cannot bypass a cap set with
applies_to_categories omitted (unspecified → every category, default included). To surface that gate-on, tag the send explicitly with category="transactional" and exclude transactional from applies_to_categories. Double-opt-in confirmation prompts ride their own out-of-band exemption — see the FE Automation → double-opt-in handshake.Recipe 3: bulk on-call import — mass opt-out file, then verify
A support-evidence export or a paid-suppression vendor file arrives and 50 000 contacts need to be marked asopted_out on sms. Single POST /optouts calls would take hours. This recipe:
- loads the file through
POST /api/v1/contacts/optouts/bulk(up to 500 rows per call) - then verifies each row’s state with
GET /api/v1/contacts/{id}/channel-stateso a retry loop is idempotent
status is succeeded (newly opted out), skipped (already opted out), or failed so the wizard can surface per-row errors without rolling back the whole batch.
GET /api/v1/contacts/{id}/channel-state is the single source of truth across caps, opt-outs, and suppression lists, so an idempotent re-check reads sms.opted_out: true.
POST /api/v1/contacts/imports (results are polled via GET /api/v1/contacts/imports/{id} and POST /api/v1/contacts/imports/preview for a dry-run) — see Imports API.
Recipe errors
See also
- Messaging API → categories
- Compliance → opt-out — opt-out is enforced regardless of caps
- Message Suppression API — content-hash dedupe in parallel with caps
- Compliance → opt-out & suppression