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:
StateMeaning
opted_inConsent granted and not revoked.
opted_outConsent revoked, or an explicit opt-out recorded.
unknownNo consent record exists for the pair — your send gate decides the default.

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).
curl -X POST https://api.orbit.devotel.io/api/v1/compliance/consent \
  -H "Authorization: Bearer $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "identifier": "jordan@example.com",
    "channels": ["email", "sms"],
    "opt_in": true,
    "source": "web_form",
    "consent_type": "marketing",
    "lawful_basis": "consent",
    "purpose": "Weekly product newsletter and order updates",
    "consent_text_version": "tos-2026-04",
    "consent_proof_url": "https://example.com/proofs/abc123.png"
  }'
Returns 201 Created:
{
  "contact_id": "cnt_9f…",
  "consent_record_ids": ["cr_a1…", "cr_b2…"],
  "channels": ["email", "sms"],
  "state": "opted_in"
}
FieldTypeNotes
contact_id / identifierstringProvide one of the two.
channelsstring[]One or more of the channel set; deduplicated and sorted.
opt_inbooleanRequired. true = opt-in, false = opt-out.
sourcestringHow consent was captured (e.g. web_form, import, double_opt_in). Default consent_api.
consent_typestringPurpose category, e.g. marketing, transactional. Default messaging.
lawful_basisenumGDPR Art 6 basis: consent, contract, legal_obligation, vital_interests, public_task, legitimate_interests.
purposestringFree text describing the use (≤ 500).
consent_text_versionstringVersion of the notice the subject agreed to.
consent_proof_urlstring (url)Link to a screenshot or signed document.
metadataobjectArbitrary custom key/values.
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.
curl "https://api.orbit.devotel.io/api/v1/compliance/consent/lookup?identifier=jordan@example.com&channel=sms" \
  -H "Authorization: Bearer $ORBIT_API_KEY"
{
  "contact_id": "cnt_9f…",
  "channel": "sms",
  "state": "opted_in",
  "source": "web_form",
  "granted_at": "2026-04-02T10:11:00.000Z",
  "revoked_at": null,
  "lawful_basis": "consent",
  "purpose": "Weekly product newsletter and order updates",
  "consent_text_version": "tos-2026-04",
  "consent_proof_url": "https://example.com/proofs/abc123.png"
}
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).
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.
{
  "contact_id": "cnt_9f…",
  "channel": null,
  "items": [
    {
      "id": "cr_b2…",
      "channel": "sms",
      "consent_state": "opted_in",
      "granted": true,
      "source": "web_form",
      "granted_at": "2026-04-02T10:11:00.000Z",
      "revoked_at": null,
      "lawful_basis": "consent",
      "created_at": "2026-04-02T10:11:00.000Z"
    }
  ],
  "next_cursor": "eyJ0…"
}
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.
curl -X POST https://api.orbit.devotel.io/api/v1/compliance/consent/managers \
  -H "Authorization: Bearer $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme Consent Manager",
    "manager_id": "acme-cm-001",
    "manager_url": "https://cm.acme.example",
    "public_key": "-----BEGIN PUBLIC KEY-----\n…\n-----END PUBLIC KEY-----",
    "country_code": "IN"
  }'
  • 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 the RFC 8785 (JCS) canonicalized payload before anything is stored.
curl -X POST https://api.orbit.devotel.io/api/v1/compliance/consent/receipts \
  -H "Authorization: Bearer $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "contact_id": "cnt_9f…",
    "consent_manager_id": "acme-cm-001",
    "channel": "sms",
    "consent_type": "marketing",
    "receipt": {
      "receipt_id": "rcpt_77…",
      "issued_at": "2026-05-01T09:00:00.000Z",
      "purpose": "Promotional SMS",
      "fiduciary_id": "fid_acme",
      "signature": "MEUCIQ…",
      "payload": { "…": "…" }
    }
  }'
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 manager/receipt endpoints degrade gracefully (empty list / 404) rather than erroring.