Skip to main content

Data Subject Access Requests (DSAR)

A Data Subject Access Request (also called a privacy request or consumer rights request) is the formal mechanism a person uses to exercise their rights over the personal data you hold about them — the right to access, delete, correct, port, or opt out of the sale of that data. Most privacy laws give you a hard deadline to respond (30 days under GDPR, 45 under CCPA/CPRA). Orbit gives you three intake paths and one fulfilment pipeline:
  • Operator-filed DSAR — your support or compliance team files a request on a customer’s behalf through the authenticated API or dashboard.
  • Public self-service portal — the data subject files their own request through a public, unauthenticated flow that proves their identity with a two-factor email + SMS OTP before anything is queued.
  • WhatsApp user data requests — when a WhatsApp user taps Request my data or Delete my data in a conversation with your business, Meta forwards the request to your WABA and Orbit enqueues it into your DSAR queue — see the WhatsApp section below.
This page describes Orbit’s platform controls. It is not legal advice. Your obligations — which laws apply, what you must disclose, and how long you have — depend on where your data subjects live and what data you process. Confirm with qualified counsel.
All endpoints below are rooted at https://api.orbit.devotel.io/api/v1/compliance.

Supported jurisdictions and deadlines

The applicable_jurisdiction on a request controls which statutory clock Orbit’s SLA tracker applies. Operators can reclassify a request after intake.

Request types

request_type describes what the subject is asking for. The full CCPA/CPRA verb set is available to operators; the public portal exposes a friendlier subset that maps onto it. For CCPA access requests you may also attach consumer_categories — the CCPA §1798.100(b) categories the subject is asking about: identifiers, customer_records, protected_classifications, commercial, biometric, internet_activity, geolocation, sensory, professional, education, inferences, sensitive_pi.

Restriction of processing (GDPR Art 18)

The verb set above intentionally has no Art 18 verb. GDPR Article 18 — the right to have processing of your data restricted rather than erased — is valid, for example when the subject disputes accuracy, challenges legality, or needs data kept pending a legal claim. Because restriction means “keep the data but pause processing” — it does not map to know, delete, correct, or portability, and there is no hidden request_type value for it. When an Art 18 claim arrives, file it under the nearest existing verb and annotate the legal basis in requester_statement:
  • correct — Art 18(1)(a) disputes accuracy; rectification is the remedy the subject usually wants anyway.
  • limit_sensitive_pi — the CCPA/CPRA analog: filing it with applicable_jurisdiction: "cpra" narrows processing in a lawful, tracked way until counsel resolves the claim.
  • Outside the DSAR pipeline — treat the claim as a freeze through your internal legal-hold process instead of queueing it here at all.
Whichever route you take, record the Art 18 basis in requester_statement so the audit chain and the GET /compliance/dsar queue explain why the request is open. A worked example — the subject disputes the accuracy of their call-activity records and asks you to stop acting on them while counsel reviews. File the accuracy half as correct (GDPR), and narrow processing in parallel as limit_sensitive_pi (the CCPA/CPRA analog) so the restriction is enforced rather than merely noted:
Deciding whether an Art 18 claim is valid — and which of the four Art 18(1) conditions applies — is a legal determination that stays with your counsel. Orbit gives you the request_type verb set, the requester_statement marker, and the SLA tracker; the judgement to accept, reject, or release the restriction is yours at the tenant level.

Operator-filed requests

Create a request

POST /compliance/dsar — requires an admin or owner API key. Provide at least one subject identifier (contact_id, subject_email, or subject_phone) plus the requester_email that should receive correspondence.
Returns 202 Accepted:
Note — applicable_jurisdiction only defaults to gdpr for rights that exist under GDPR. The opt_out_sale and limit_sensitive_pi request types are CCPA/CPRA-only and have no GDPR equivalent, so you must set applicable_jurisdiction to ccpa or cpra explicitly for them. Omitting it (or leaving the gdpr default) is rejected with 422 VALIDATION_ERROR.

Status lifecycle

A request moves through: received → processing → completed with terminal branches failed, expired, and cancelled. The verification sub-state is tracked independently: pending → verified (worker proceeds) or rejected (worker halts). GDPR/admin-filed rows default to not_required.

Verify or reject identity

Higher-assurance requests (delete, opt-out, limit-sensitive) require an operator decision before fulfilment proceeds:
decision is verified or rejected; notes is optional (≤ 2048 chars). Returns the new verification_status and verified_at.

Cancel a request

POST /compliance/dsar/{id}/cancel withdraws an in-flight request (GDPR Art 7(3)). Only works while the request is received or processing; a terminal request returns 409 Conflict.

List and read requests

  • GET /compliance/dsar — paginated list. Query: page (≥ 1), page_size (≤ 100, default 25), and an optional status filter.
  • GET /compliance/dsar/{id} — fetch one request. The response includes the signed export_url (and its export_expires_at) once an access/portability export has been produced, plus tables_exported describing the per-table row counts.

Erasure requests

GDPR Art 17 erasures are tracked as their own resource so you can audit and intervene before data is destroyed:
  • GET /compliance/dsar/erasure-requests — list. Query: status (pending, cancelled, executing, executed, failed) and limit (≤ 500).
  • POST /compliance/dsar/erasure-requests/{id}/cancel — cancel a pending erasure before it executes. Optional reason (≤ 500 chars). Returns 409 if it is already executing or done.

Proof-of-deletion certificate

Once an erasure request lands executed, you can issue a signed proof-of-deletion certificate — the receipt a data subject or supervisory authority can hold as evidence the request was fulfilled. The certificate is built deterministically from the immutable executed request row and its audit-chain anchor, self-signed with a detached HMAC-SHA256 tag. It is not persisted — it is derived on demand, so re-issuing for the same request and issued_at timestamp always produces a byte-identical, re-verifiable artifact. For the full operational sequence around this artifact — trigger and finalize, reading and verifying the digest, presenting it as Art. 17 evidence, and handling erasures that did not complete cleanly — see the Proof-of-Deletion Certificate Runbook.

Issue the certificate

GET /compliance/dsar/erasure-requests/{id}/certificate — owner or admin API key. Only executed erasures can be certified; anything earlier returns 409 Conflict.

Re-verify a certificate

To confirm the artifact has not been altered:
  1. Canonicalise the certificate object with RFC 8785 (JCS) — the builder uses the same canonical form other Orbit signed artifacts use: object keys sorted ascending, no insignificant whitespace.
  2. Compute HMAC-SHA256 with the platform signing secret over the string orbit.dsar.erasure-certificate.v1.<canonical-json>.
  3. Base64url-encode and compare against signature.value with a timing-safe equality check (e.g. crypto.timingSafeEqual in Node).
The domain-separated context string (orbit.dsar.erasure-certificate.v1) is mixed into every tag, so a certificate signature can never collide with any other platform-signed surface reusing the same secret. A mismatched or malformed tag verifies false; it never fails open.

What the certificate is — and is not

The certificate is a tenant-issued artifact: it is the signed receipt you hand to a data subject or supervisory authority to evidence that an erasure executed. Whether that evidence discharges a legal obligation stays with your counsel, consistent with Orbit’s tenant-owned compliance posture. The platform signs integrity of the artifact; the regulatory judgement is yours.

Propagation fold

When you run downstream erasure propagation (see CDP erasure propagation), the per-destination outcomes are folded into the certificate’s destination_propagation section — total, dispatched, failed, and skipped counts plus one entry per destination. Destinations are sorted by name and deduplicated (latest outcome wins) so re-issue stays byte-stable. An empty section is signed as total: 0 — an honest, verifiable “no external destinations were notified,” not silence.

SLA dashboard

GET /compliance/dsar/sla returns a combined export + erasure SLA snapshot so you never miss a statutory deadline:
Severity tiers scale proportionally with each jurisdiction’s SLA window — the day thresholds are anchored to the GDPR 30-day case and multiplied by the slaDays / 30 ratio, so a request always turns amber and red at the same fraction of its own deadline. escalation_due flips 5 days before the statutory deadline (slaDays − 5). For GDPR (sla_days: 30): green (< 20 days elapsed), amber (20–25), red (26–30), red + breach (> 30); escalation_due at day 25. For CCPA/CPRA (sla_days: 45) the same ratios give green (< 30), amber (30–38), red (39–45), red + breach (> 45); escalation_due at day 40. Always read the tier boundaries against the sla_days returned for that request, not the fixed 20/25/30 numbers.

Public self-service portal

The public flow lets a data subject file a request without an account. Identity is proven with a two-factor OTP — an email code and an SMS code — before any request is queued. The endpoints live under /compliance/public/dsar and are unauthenticated, but are defended by Cloudflare Turnstile, per-IP and per-identifier rate limits, and a privacy-preserving response shape that never reveals whether an email/phone pair matches a real contact.
SMS verification codes are delivered through the Devotel softswitch (the platform’s sole outbound SMS path). They are platform OTPs, not tenant-billable traffic, and carry no delivery-receipt persistence.

Flow overview

1

Begin

POST /compliance/public/dsar/begin with email, phone (E.164), request_type (access | delete | portability | opt_out), and a Cloudflare turnstile_token (required in production). Returns an opaque claim_id, email_sent: true, and expires_in: 600. An email OTP is sent immediately.
2

Verify email

POST /compliance/public/dsar/verify-email with claim_id and the 6-digit code. Returns state email_verified and next step phone_send. Codes expire after 10 minutes; max 3 attempts. POST …/resend-email (with claim_id + email) issues a new code, subject to a 60-second cooldown.
3

Send phone code

POST /compliance/public/dsar/send-phone with claim_id and the phone that matches the one given at begin. Sends an SMS OTP (expires_in: 600). A 60-second cooldown applies between sends; a too-soon retry returns 429 with Retry-After.
4

Verify phone

POST /compliance/public/dsar/verify-phone with claim_id and the 6-digit code. Returns state phone_verified and next step submit.
5

Submit

POST /compliance/public/dsar/submit with claim_id. Persists an audit row and — only if the verified email + phone match a contact in your tenant — queues a real DSAR (pre-marked verification_status: verified, since the OTP already proved identity). Returns a reference_id (e.g. dsar_pub_…) and a queued boolean.

Configure the identity-proof senders

The two OTPs are sent from platform-level senders you configure once in your API environment. Set them before publishing the portal — an unset SMS sender with no fallback makes the phone step fail closed. If DEVOTEL_DSAR_PROOF_SMS_FROM and DEVOTEL_PLATFORM_DEFAULT_FROM are both unset, the send-phone step fails closed with a 503 — the portal returns a “temporarily unavailable” message and the failure is emitted under the dsar.proof.sms_send_failed metric so it surfaces in your dashboards rather than silently skipping the second factor. Likewise, the email step returns 503 when DEVOTEL_RESEND_API_KEY is unset. Configure both senders before you link the portal publicly.

Abuse defenses

The response shape is identical whether or not the identifiers match a real contact — the portal never confirms or denies that someone is in your database. When Redis is unavailable the rate-limit gates fail open to preserve availability.

Enabling Turnstile protection

The Turnstile gate is configured with two environment variables.
The gate is fail-open when DEVOTEL_TURNSTILE_SECRET_KEY is unset: begin accepts requests without a token and logs a single warning. Set the secret in production, or the portal is unprotected by Turnstile even though every other abuse defense above still applies. Generate both keys in the Cloudflare dashboard (Turnstile → Add site) and set them on the API and web deployments respectively.

Publish the public portal under your privacy policy as the “Submit a privacy request” link. Because the flow self-verifies via OTP, requests that arrive through it are already identity-proven — they land in your operator queue ready to fulfil, and appear in GET /compliance/dsar alongside operator-filed requests.

Inbound DSAR triggers from WhatsApp (Meta user data requests)

Beyond the two paths you file through directly, a third intake path requires nothing from you to switch on. WhatsApp gives every user a Request my data and a Delete my data action inside a conversation with a business. When one of your WhatsApp contacts taps either (the GDPR Article 15 and 17 rights, exercised against you as the data controller), Meta forwards the request to the webhook your WhatsApp Business Account already delivers to Orbit, and Orbit routes it into your DSAR queue so the usual fulfilment pipeline can run.

How Orbit routes these requests

  • Orbit identifies your organisation by the WABA the webhook arrived on — shared WABAs resolve to every organisation attached to them.
  • If the user’s phone number matches a contact in your tenant, that contact’s dsar_status is tagged and the Meta payload is attached to the contact record.
  • A dsar_requests row is queued in status received, with requester_statement noting the request came in from WhatsApp along with Meta’s original request_id where present.
  • From there nothing is special-cased: scheduler-dsar-export and scheduler-gdpr-erasure pick the row up exactly as they do operator-filed and portal requests, including downstream erasure propagation and the proof-of-deletion certificate.

How Meta verbs map to request_type

data_request asks you to hand over what you hold, so it queues as an export; data_delete asks you to erase, so it queues as an erasure.

What you see in your DSAR queue

A WhatsApp-originated row appears in GET /compliance/dsar and the dashboard queue alongside operator-filed and portal requests, with:
  • request_type set by the mapping above.
  • subject_phone set to the user’s WhatsApp number in E.164 — the one identifier Meta supplies.
  • requester_statement describing the WhatsApp origin, including the WABA the request arrived on and Meta’s request_id.
  • verification_status: verified. Because Meta signs these attestations on your behalf upstream, Orbit does not gate them behind a second identity check; it is a meta_dpo_webhook-originated row, and the queue groups it as such. Art 12(6) still leaves it to you to decide when the identity is sufficient — record your basis if you accept the Meta attestation as verification.
The contact-side view complements this: the affected contact carries dsar_status of export_requested or deletion_requested, plus the Meta metadata the webhook delivered — request_id, expires_at, and signed_request_present (whether Meta signed the request blob).

Duplicate deliveries enqueue once

Meta may deliver the same request_id more than once. Before Orbit inserts a queue row, it checks whether a row for that organisation already carries the same request_id in requester_statement, and skips the insert on a match — a redelivered webhook never piles a second row into your queue. The original row keeps the first requested_at, and with it the SLA clock.

You still own fulfilment

Orbit routes the upstream trigger into your queue; everything from verification onward is your call, exactly as with the other intake paths:
  • The Article 12 clock starts when Meta delivers the request to Orbit, not when your team first reviews the queue row. Meta also sends its own expires_at in the payload, visible on the contact — budget your review time inside both.
  • Verify that the matched contact really is the data subject before you export or erase, and reject the request through the normal verification flow (POST /compliance/dsar/{id}/verification) if it is not.
  • The 30-day GDPR window applies; the SLA dashboard escalation tiers track these rows like any other gdpr request.
Every intake path feeds the same queue and the same fulfilment pipeline: the operator-filed requests workflow above, the self-service portal guide, and the GDPR posture guide all describe the controls these rows flow through once queued.