Skip to main content

Preference center: tenant controls and contract

The preference center is a hosted public page where a contact manages their own channel opt-ins, subscription topics, message frequency, and (optionally) files a data-deletion request. This page is the posture and contract reference: the knobs your organization owns, the artifact each contact decision writes, and how the public surface fails. For step-by-step configuration, branding, and link placement, use the walkthrough at Preference center: the public opt-in/opt-out page.
The preference center is a tenant-owned control: you choose the channels, topics, and branding, and your organization holds the consent evidence. Orbit hosts the page and records the artifacts; it does not set your posture. This page is not legal advice.

1. Where it sits in the control map

The preference center is the contact-written half of your consent posture. Four surfaces work together: A decision made on the hosted page is not a UI flag: it lands in the same consent ledger and suppression entries that a STOP keyword or an operator action writes. The contract is defined by two route surfaces:
  • Admin/API configuration (apps/api/src/routes/compliance/preference-center.routes.ts): POST /preference-center, GET /preference-center, and POST /preference-center/link.
  • Public, unauthenticated (apps/api/src/routes/compliance/preference-center-public.ts): GET /preferences/:token and PUT /preferences/:token, gated by a signed token instead of a login.
All endpoints are rooted at https://api.orbit.devotel.io/api/v1/compliance.

2. The toggle surface

Configure with POST /preference-center and read back with GET /preference-center. Every field is validated server-side; a rejected POST returns 422 with per-field issues. Fields you omit take the server-side defaults below, so a minimal config is safe, and optional fields left unset simply do not render. Topics. Each topic id is a stable slug ([a-z0-9][a-z0-9_-]{0,63}), unique across the array (duplicates are a 422); name is the page label; description is optional context shown beside the toggle; defaultOptIn (default false) decides how a contact with no recorded choice is treated; archived removes the topic from the page while its history stays in the consent ledger. Links. A minted link carries an HMAC-SHA256 token (v1.<payload>.<signature>) scoped to exactly one contact in exactly one organization, valid for 30 days.

3. What each action writes

Every contact decision on the page writes the same artifacts the STOP-keyword path writes on the inbound webhooks, so a form opt-out blocks sends exactly as a keyword STOP does: The operator-facing read surface for the same consent ledger is Consent management (apps/api/src/routes/compliance/consent-management.controller.ts), and the suppression writes mirror the inbound STOP-keyword handler, so both entry points block sends the same way. The hosted page also shows the contact their own history, newest first, drawn from that same ledger.
Channel wins over topic. A channel-level opt-out silences every message on that channel; a topic toggle only narrows consent within the channels the contact still accepts. Re-opt-in is symmetric only when every channel in the request is set to opted_in.

4. Regional anchoring

The preference center is the mechanism; the obligations are yours, per jurisdiction.
  • US CAN-SPAM. A clear unsubscribe mechanism that keeps working for at least 30 days after the message was sent, with requests honored within 10 business days. The 30-day token TTL plus the durable suppression entry cover the mechanism; the honor window is your process. See CAN-SPAM.
  • Canada CASL. Unsubscribe requests handled without delay and no later than 10 business days after receipt. See CASL.
  • GDPR. Withdrawal of consent must be as easy as granting it (Art 7(3)), information must be transparent (Arts 12–14), and consent must be demonstrable (Art 5(2)). The append-only consent ledger and the contact’s self-serve history are the record-keeping half. See GDPR posture.
Your duty vs Orbit’s. You publish the link in footers and templates, choose the channels, topics, and honor windows, and meet the response deadlines. Orbit hosts the page, records the consent, suppression, STOP-fence, and audit artifacts, and keeps them readable from your dashboard and this API.

5. Failure modes

Error responses carry the code, a human-readable message, and a request id. The invalid- and expired-token responses do not disclose which contact or organization a token referred to.

6. RBAC posture

  • Changing the config (POST /preference-center) and minting links (POST /preference-center/link): organization owner or admin roles only.
  • Reading the config (GET /preference-center): any authenticated member of the organization.
  • The public page (GET/PUT /preferences/:token): unauthenticated by design. The signed token is the authorization: one contact, one organization, 30 days. Email and phone come back masked, so the page never echoes the raw identifiers it was minted for.
Rate limits: 60/min on config reads, 10/min on admin writes, 30/min on public reads, and 10/min on public writes.

Worked example

Configure the center with topics and a custom opt-out label, then mint a per-contact link:
Expected: 201 with the saved config echoed. Then mint the link:
The response returns link: the hosted page URL with ?token=v1… appended. The page itself reads and writes through GET/PUT /preferences/:token. Placement guidance (email footer, SMS fallback) lives in the walkthrough; how the resulting suppression entries interact with imports and keyword STOPs lives in Opt-Out & Suppression Lists.