Skip to main content

Consent Management & Receipts

Before you message a contact on a regulated channel you generally need a lawful basis — most often consent. Orbit’s consent API is the system of record for who opted in or out, on which channel, when, and under what legal basis. Every write fans out to the surfaces your sends are gated against, so recording consent here is what actually unblocks (or blocks) a message. All endpoints below are rooted at https://api.orbit.devotel.io/api/v1/compliance.
Recording consent in Orbit creates an auditable trail, but it does not by itself make a send lawful. You remain responsible for obtaining valid consent and for the content you send. This page is not legal advice.

Channels and states

Consent is tracked per channel. The supported channel set is: email, fax, instagram, line, messenger, push, rcs, sms, viber, voice, whatsapp. A (contact, channel) pair resolves to one of three states:
POST /compliance/consent records an opt-in or opt-out across one or more channels in a single call. Identify the contact by contact_id or by identifier (an email, E.164 phone, or WhatsApp ID — Orbit resolves the type automatically).
Returns 201 Created:
Supplying both valid_until and expires_in_days is ambiguous and rejected with 422 VALIDATION_ERROR. When you set a window, the 201 response echoes the resolved valid_until (the absolute expiry instant); it is null for a non-expiring grant or an opt-out. Re-recording an opt-in with a fresh window extends validity — the original granted_at is preserved, but the expiry is updated. What a write does. Each recorded channel updates four synchronized surfaces: the consent_records audit table, the contact’s channel_preferences mirror (the read-side fast path your sends check), the suppression_list (on opt-out), and a short-lived Redis STOP-fence so in-flight campaign batches honour the change within ~10 minutes.
Writes are partial-safe: if one channel fails, the others still apply. Compare consent_record_ids.length against the number of channels you requested to detect a partial write. Re-recording an opt-in for a channel that is already opted-in refreshes the metadata/proof but keeps the original granted_at.

GET /compliance/consent/lookup returns the current state for one (contact, channel) pair — use it as a pre-send gate.
A state of unknown means no record exists for the pair — your application decides whether that implies consent (some transactional flows) or blocks the send (most marketing flows). The last three fields report time-bounded consent and are always present:
GET /compliance/consent/expiring sweeps the tenant for opt-ins whose validity window has lapsed or is about to — the input to a re-permission (re-confirmation) campaign. Only grants that carry a valid_until are returned; non-expiring consent never appears.
Query parameters:
Items are ordered oldest-expiring first. Each carries status (expired or expiring) so you can split “must re-confirm now” from “warn before the window closes”. Re-confirmation is an ordinary POST /compliance/consent opt-in — optionally with a fresh valid_until or expires_in_days.
Treat next_cursor as opaque and round-trip it verbatim; a null value means the final page. An invalid or stale cursor is treated as a fresh first page rather than an error.

GET /compliance/consent/history returns the full, paginated audit trail for a contact — every grant and revocation, most recent first. Query parameters: contact_id or identifier (one required), an optional channel filter, limit (≤ 100, default 50), and an opaque cursor.
Treat next_cursor as opaque — round-trip it verbatim to fetch the next page. An invalid or stale cursor is treated as a fresh first page rather than an error.

India’s Digital Personal Data Protection Act (DPDP) introduces the concept of a Consent Manager — an accountable, registered intermediary that mints cryptographically signed consent receipts on behalf of a data principal. Orbit can register the managers your users go through and verify the receipts they issue. POST /compliance/consent/managers (admin/owner) registers a manager and stores its public key (an ECDSA P-256 SPKI PEM) used to verify every receipt it signs.
  • GET /compliance/consent/managers lists registered managers (active first).
  • PUT /compliance/consent/managers/{id} updates or deactivates one (partial update; all fields optional).

Store a signed receipt

POST /compliance/consent/receipts verifies a manager-signed receipt and persists it as consent. The signature (ECDSA P-256 / SHA-256, IEEE-P1363, base64url) is checked against the registered manager’s public key over a JCS-inspired, sorted-key JSON canonicalization of the payload before anything is stored. This canonicalization sorts object keys ascending by UTF-16 code unit and drops insignificant whitespace, but it is not a full RFC 8785 implementation — in particular it does not apply JCS’s mandated number serialization rules. Sign receipts with the same sorted-key form Orbit uses rather than assuming a spec-complete RFC 8785 verifier will produce a matching hash.
Returns 201 with { "id": …, "receipt_id": …, "verified": true }. A bad signature, or an unregistered/inactive manager, returns 422 CONSENT_RECEIPT_INVALID — the detail notes the payload may have been tampered with or the manager may have rotated keys.

Re-verify a stored receipt

POST /compliance/consent/receipts/{id}/verify re-checks a previously stored receipt against the manager’s current key — use it during an audit to confirm a receipt still validates and whether its manager remains active. {id} accepts either the consent-record id or the receipt_id.
Consent receipts require the tenant consent_managers migration. On tenants that predate it, the read paths degrade gracefully: the manager list returns an empty list and the re-verify endpoint returns 404. Minting a receipt is fail-closed, so POST /compliance/consent/receipts returns 422 CONSENT_RECEIPT_INVALID on pre-migration tenants rather than degrading — run the migration before issuing receipts.