Skip to main content

Documentation Index

Fetch the complete documentation index at: https://orbit-docs.devotel.io/llms.txt

Use this file to discover all available pages before exploring further.

Public consent form

Orbit can host a public landing page where recipients enter their email or phone, choose channels, and opt in or out of receiving messages. Submissions write to the same consent surfaces as carrier-side STOP / START keywords — consent_records, channel_preferences, and suppression_list — so the platform’s outbound send gate immediately honours the new state. The feature is off by default. Each tenant explicitly turns it on under Settings → Compliance → Public consent form. When disabled, the public URL returns 404.

When to use it

  • The footer of email / SMS campaigns where you’d otherwise embed a vendor unsubscribe link.
  • Sender bios (Instagram, LinkedIn) where recipients ask you to stop messaging.
  • Operator-driven opt-in capture, e.g. when transcribing paper-form consent.
  • Bulk re-permissioning campaigns after a privacy-policy change.
The page does not replace carrier-side STOP / START keywords or per-channel inline unsubscribe headers — it complements them.

Enable the form

  1. Sign in as an owner or admin.
  2. Go to Settings → Compliance → Public consent form.
  3. Toggle Enable public consent form on.
  4. Copy the public URL (under the toggle) — it has the form https://orbit.devotel.io/<locale>/consent/<tenantId>.
The URL becomes live the moment you save changes.

Optional configuration

All fields below are optional. Leave blank to use Orbit defaults. Branding
  • Business name — shown above the headline.
  • Primary color — 3- or 6-digit hex (e.g. #0ea5e9). Drives the submit button and success badge.
  • Logo URL — must be https:// (or http://). Rendered above the business name.
Page copy
  • Headline — replaces the default “Manage your preferences” (max 240 chars).
  • Body — short explanatory paragraph (max 2000 chars).
  • Footer — fine print, support contact, link to your privacy policy (max 1000 chars).
Allowed channels Restrict which channels appear on the form. When unchecked, the channel is hidden on the page and submissions for it are rejected with 422 INVALID_CHANNEL. Leave all 11 channels selected to accept the platform default (email, fax, instagram, line, messenger, push, rcs, sms, viber, voice, whatsapp).

Pre-fill the recipient identifier

Append ?contact=<email-or-phone> to the URL to pre-fill the form. The recipient can still edit the value before submitting.
https://orbit.devotel.io/en/consent/<tenantId>?contact=user@example.com
https://orbit.devotel.io/en/consent/<tenantId>?contact=%2B14155551212
Use this in campaign footers so the recipient lands one-tap away from saving their preferences.

Programmatic submissions

The form is a thin client over a public REST endpoint. Direct integrations (e.g. an embedded preference centre on your own marketing site) can POST against the same handler.
curl https://api.orbit.devotel.io/api/v1/public/consent \
  -H "Content-Type: application/json" \
  -d '{
    "tenantId": "<tenantId>",
    "contactIdentifier": "user@example.com",
    "channels": ["sms", "email"],
    "optIn": false,
    "source": "embedded_preference_centre"
  }'
Response:
{
  "data": {
    "status": "ok",
    "consentRecordIds": ["consent_...", "consent_..."]
  },
  "meta": {
    "request_id": "req_...",
    "timestamp": "2026-05-15T08:14:22Z"
  }
}

Body

FieldTypeNotes
tenantIdstringThe platform UUID, NOT the org id. Visible in the public URL.
contactIdentifierstringRFC-5322 email, E.164 phone (+14155551212), or raw WhatsApp ID / Messenger PSID.
channelsstring[]One or more of: email, fax, instagram, line, messenger, push, rcs, sms, viber, voice, whatsapp.
optInbooleantrue = subscribe, false = unsubscribe.
sourcestringOptional free-form tag (≤ 64 chars). Defaults to "public_form".

Errors

CodeWhen
VALIDATION_ERROR (422)Body failed Zod validation.
INVALID_TENANT (422)Tenant id is unknown OR the public form is disabled. Same shape either way — no existence-enumeration leak.
INVALID_CHANNEL (422)None of the submitted channels are in allowed_channels.

Read the tenant’s public config

For embedding teams that want to render their own UI against the same config the Orbit-hosted form uses:
curl https://api.orbit.devotel.io/api/v1/public/consent/<tenantId>/config
{
  "data": {
    "enabled": true,
    "branding": {
      "logo_url": "https://cdn.example.com/logo.png",
      "primary_color": "#0ea5e9",
      "business_name": "Acme Inc."
    },
    "copy": {
      "headline": "Manage your preferences",
      "body": "...",
      "footer": "..."
    },
    "allowed_channels": ["sms", "email", "whatsapp"]
  }
}
When the tenant has NOT enabled the form, the response carries enabled: false and empty branding / copy / channels. Render a 404 — that matches how the Orbit-hosted page reacts.

What the form writes

Every submission writes to the same three surfaces the carrier-side handleOptIn / handleOptOut routes touch:
  1. consent_records — one row per submitted channel with consent_state = 'opted_in' / 'opted_out'. The send-side gate at outbound time checks this column.
  2. contacts.channel_preferences JSONB — per-channel opted_out flag flipped, global_opt_out flipped on opt-out, and an entry appended to consent_history for audit continuity.
  3. suppression_list — opt-out inserts a channel='all' row that survives soft-delete. Opt-in sets revoked_at = NOW() on every active row (audit-friendly, NOT a DELETE).
A redis-backed STOP-fence cache key is also flipped so in-flight campaign batches converge within sub-RTT, and an entry lands on the tamper-evident audit log (action: consent.public_opt_in / consent.public_opt_out).

Limits and abuse defence

  • The POST + GET endpoints are rate-limited per source IP (RATE_LIMIT_PUBLIC_UNAUTH — 10 requests per minute).
  • There is no Clerk session or API key on this route.
  • Logs capture source IP, user-agent (truncated to 500 chars), and a contact-identifier kind classifier (email / phone / whatsapp_id).
  • Suppression and consent state are authoritative in Postgres; the STOP-fence cache is a sub-RTT convenience that fails open.

Disabling the form

Toggle Enable public consent form off. The public URL returns 404 from that moment, and the POST handler returns 422 INVALID_TENANT for that tenant — no in-flight submissions can land. Stored consent records remain — disabling only stops new submissions. Re-enabling restores the previous configuration block (branding, copy, allowed channels) so an accidental toggle doesn’t lose the customisation.