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

# Sends blocked by consent gates — 403 consent_required

> One diagnostic sequence for a 403 whose details carry consent_required: true — tell the per-channel consent_required preflight apart from the org-wide unknown_marketing_policy marketing gate and a DSAR restriction, account for the 60-second settings cache, inspect the contact's ledger row, and take the matching recovery path.

# Sends blocked by consent gates

A send refuses with HTTP 403 and `consent_required: true` in the
response `details`. Three different controls can produce that shape,
and each has its own fix. This page walks the diagnostic sequence:
identify the knob that fired, verify the contact-level state it saw,
then take the matching recovery path.

<Note>
  All three controls are tenant-owned. Orbit enforces them; the
  posture decision — require affirmative opt-in, refuse unknown
  marketing consent, or honour a data-subject request — is always
  yours.
</Note>

***

## 1. Symptom

You submit a send (typically a marketing send) and receive:

```json theme={null}
{
  "error": {
    "code": "MESSAGING_CONSENT_REQUIRED",
    "message": "Recipient has no recorded affirmative opt-in (consent) for this channel. ...",
    "details": {
      "to": "+1312****",
      "channel": "sms",
      "consent_required": true,
      "consent_expired": false
    }
  }
}
```

The marketing gate answers with the same `details.consent_required:
true` flag but a `FORBIDDEN` code and the message "Contact has not
consented to marketing messages on this channel". Because both
envelopes carry `consent_required: true`, the flag alone does not tell
you which knob fired — the `code` does. Read the decision tree next
before changing any setting.

***

## 2. Decision tree — which knob fired

| Envelope | Control that fired | Owning doc |
| - | - | - |
| `403 MESSAGING_CONSENT_REQUIRED`, `details.consent_required: true` | The send-time preflight on `organizations.settings.consent_required` (per channel) | [Consent default policy](/compliance/consent-default-policy) |
| `403 FORBIDDEN`, "Contact has not consented to marketing messages on this channel", `details.consent_required: true` | The marketing gate on `unknown_marketing_policy: refuse` (org-wide) | [Consent default policy](/compliance/consent-default-policy) |
| A restriction named on the contact, while both checks above look satisfied | A DSAR restriction such as `limit_sensitive_pi` | [DSAR](/compliance/dsar) |

**Branch A — the per-channel preflight.** Your org enabled
`settings.consent_required` and the send's channel is in the gated
set (default `["sms", "mms"]`). The gate then demands a live,
unexpired `opted_in` row in the consent ledger for this contact and
channel; a missing row blocks with `consent_expired: false`, a lapsed
`valid_until` window blocks with `consent_expired: true`.
Transactional and OTP traffic is exempt by default
(`exempt_transactional`), as is the double-opt-in confirmation prompt
itself.

**Branch B — the org-wide marketing gate.** The send is categorised
as marketing and `unknown_marketing_policy` is `refuse`: a contact
with no recorded marketing consent for the channel is refused
outright. Under `allow_with_logging` the same send goes out and the
allow decision is logged instead (see
[Audit trail](#7-audit-trail)). This gate looks only at marketing
traffic; the Branch A preflight applies to any non-exempt send on a
gated channel. Both can be armed at once, and when they are, the
preflight runs on every gated send while the marketing gate adds a
second refusal on marketing sends — which is exactly why one toggle
change can appear to "fix" nothing.

**Branch C — a DSAR restriction.** A data-subject request placed a
restriction such as `limit_sensitive_pi` on the contact. While the
request is open, marketing use of the contact's data is paused and
sends that depend on it stop — regardless of both org knobs. The
restriction lives on the contact record, not in org settings, so no
settings toggle lifts it; the request itself must be resolved per
[DSAR](/compliance/dsar).

If you cannot tell A from B by eye, compare `error.code`:
`MESSAGING_CONSENT_REQUIRED` is the preflight, `FORBIDDEN` with the
marketing-consent message is the marketing gate. The full code index
is on
[Compliance send-gate error codes](/compliance/troubleshooting-compliance-error-codes).

***

## 3. The 60-second settings cache

Org settings are read through an in-process cache on each API
replica, refreshed roughly every 60 seconds. Two consequences when
you are mid-diagnosis:

1. **A toggle you just changed is not yet live everywhere.** You flip
   `consent_required` off in the dashboard and the retry still 403s —
   the replica that answers the retry may still hold the previous
   posture. Wait a minute and retry before you conclude the toggle
   did not work.
2. **Two replicas can disagree for up to a minute.** If the same send
   succeeds on one retry and fails on the next, you are inside the
   propagation window, not facing two different gates. Let the cache
   turn over, then re-test once.

The same window applies in both directions: lifting a knob stops
blocking within about a minute; tightening one starts blocking
within about a minute. If a refusal persists past a full minute with
the knob verifiably off, continue to
[Contact-level checks](#4-contact-level-checks) — the gate is now
seeing contact state, not stale settings.

***

## 4. Contact-level checks

Once the settings read is settled, reproduce the exact state the
gate saw for this recipient.

**Channel preferences on the contact:**

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/contacts/{contact_id}" \
  -H "X-API-Key: dv_live_sk_..."
```

Read `channel_preferences` for the send's channel. An `opted_out`
entry (or `global_opt_out: true`) is a suppression signal — see
[Suppression always wins](#6-suppression-always-wins). Its absence is
not an opt-in: the preflight requires a positive ledger row.

**The consent ledger:**

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/compliance/consent?contact_id={contact_id}" \
  -H "X-API-Key: dv_live_sk_..."
```

* No row for the channel → the gate found the `no-row` ledger state
  and Branch A blocks with `consent_expired: false`.
* A row whose validity window has lapsed → Branch A blocks with
  `consent_expired: true`; the grant needs re-confirmation, not a
  settings change.
* A live `opted_in` row for the channel → Branch A passes; a
  remaining 403 with `consent_required: true` is Branch B (marketing
  consent on a marketing send) or Branch C (a restriction on the
  contact, visible on the contact read above).

***

## 5. Recovery paths per knob

**Branch A — fill the ledger.** Record the affirmative opt-in before
re-sending: capture it through the Consent API, or run the managed
double-opt-in handshake from the dashboard (Audience → Send double
opt-in), which delivers the confirmation prompt and records the
YES. The prompt itself is exempt from the gate, so it goes out even
while the contact is un-consented. If the grant had lapsed
(`consent_expired: true`), the same handshake re-confirms it.

**Branch B — decide the policy deliberately.** `refuse` is the strict
posture; if some of that traffic is legitimate, either record
marketing consent per contact (same tooling as Branch A) or move the
org to `allow_with_logging`, which lets marketing sends proceed and
writes an allow row you can review (see
[Audit trail](#7-audit-trail)). Both are set on the compliance
settings surface described in
[Consent default policy](/compliance/consent-default-policy).

**Branch A, lifted instead of filled.** If the requirement itself no
fits your posture, narrow `settings.consent_required.channels` away
from the affected channel, or disable the requirement. The change
propagates within the cache window from
[Section 3](#3-the-60-second-settings-cache). Lift it only for
traffic you are sure does not need affirmative opt-in — the gate
exists for regimes (GDPR Art. 6/7, LGPD Art. 8, PDPA s.14) where sending
without a recorded grant is the violation.

**Branch C — resolve the request.** Restrictions lift when the DSAR
is processed per [DSAR](/compliance/dsar); no send-side or
settings-side workaround exists by design.

***

## 6. Suppression always wins

A suppression entry blocks the send regardless of consent state. A
contact with a live `opted_in` ledger row and an `opted_out`
channel preference (or `global_opt_out`) still does not receive the
send — opt-in does not override a withdrawal. The consent model and
the suppression model are separate ledgers with separate semantics;
[Consent vs suppression](/compliance/consent-vs-suppression-model)
draws the line.

Check the suppression side before re-sending:

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/compliance/suppression-list" \
  -H "X-API-Key: dv_live_sk_..."
```

If the recipient is listed, the send stays blocked even after you
fill the consent ledger or relax a policy knob. Remove the entry
only with a documented reason; a STOP or opt-out recorded by the
recipient is theirs to withdraw.

***

## 7. Audit trail

Every gate decision leaves a row you can rebuild the timeline from:

* **Blocked sends** (Branch A and Branch B refusals) write an audit
  row naming the channel and the reason, so a compliance review can
  see what was refused and why.
* **`allow_with_logging`** writes the per-contact allow row when the
  marketing gate lets a send proceed — that row is the record your
  reviewer asks for when the policy is set to log rather than
  refuse.
* **The ledger itself** stays queryable at
  `GET /api/v1/compliance/consent`, independent of the audit log.

Read the rows in the dashboard under Settings → Audit log, filtered
by the contact or the send. If a refusal persists with the knob off
past the cache window, the ledger read clean, and no suppression
entry, open a ticket carrying the `error.code`, the HTTP status, the
`meta.request_id` from the envelope, and the contact id — the same
packet the
[error-code index](/compliance/troubleshooting-compliance-error-codes)
asks for.

***

## Cross-references

* [Consent default policy](/compliance/consent-default-policy) — the `consent_required` and `unknown_marketing_policy` knobs, their values and defaults.
* [Consent vs suppression](/compliance/consent-vs-suppression-model) — why an opt-in never overrides an opt-out.
* [DSAR](/compliance/dsar) — restrictions such as `limit_sensitive_pi` and how a request resolves.
* [Compliance send-gate error codes](/compliance/troubleshooting-compliance-error-codes) — the full pre-send code index.
* [Consent receipt invalid](/troubleshooting/consent-receipt-invalid) — when a recorded receipt fails verification.


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