> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Data Subject Access Requests (DSAR)

> How to receive, verify, and fulfil GDPR / CCPA / CPRA / LGPD / PDPA / DPDP data-subject requests on Orbit — both operator-filed and the public OTP-verified self-service portal.

# 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 two 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.

<Warning>
  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.
</Warning>

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.

| Jurisdiction              | Code     | Response SLA |
| ------------------------- | -------- | ------------ |
| EU / EEA GDPR             | `gdpr`   | 30 days      |
| California CCPA           | `ccpa`   | 45 days      |
| California CPRA           | `cpra`   | 45 days      |
| Brazil LGPD               | `lgpd`   | 15 days      |
| Singapore / Thailand PDPA | `pdpa`   | 30 days      |
| Canada PIPEDA             | `pipeda` | 30 days      |
| India DPDP                | `dpdp`   | 30 days      |

## 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.

| Operator `request_type` | Meaning                                                       | Public portal verb |
| ----------------------- | ------------------------------------------------------------- | ------------------ |
| `know`                  | Access — disclose the data held (GDPR Art 15, CCPA §1798.110) | `access`           |
| `delete`                | Erasure (GDPR Art 17, CCPA §1798.105)                         | `delete`           |
| `correct`               | Rectification (GDPR Art 16, CPRA §1798.106)                   | —                  |
| `portability`           | Machine-readable export (GDPR Art 20)                         | `portability`      |
| `opt_out_sale`          | Opt out of sale/share (CCPA §1798.120)                        | `opt_out`          |
| `limit_sensitive_pi`    | Limit use of sensitive PI (CPRA §1798.121)                    | —                  |
| `non_discrimination`    | Non-discrimination right (CCPA §1798.125)                     | —                  |

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`.

***

## 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.

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/compliance/dsar \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "subject_email": "jordan@example.com",
    "requester_email": "jordan@example.com",
    "applicable_jurisdiction": "gdpr",
    "request_type": "know",
    "verification_method": "email_link"
  }'
```

Returns `202 Accepted`:

```json theme={null}
{
  "id": "dsar_8x2k…",
  "status": "received",
  "applicable_jurisdiction": "gdpr",
  "request_type": "know",
  "verification_status": "pending",
  "message": "Request received and queued for verification."
}
```

| Field                     | Type      | Notes                                                                                                                                      |
| ------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `contact_id`              | string    | Optional. Links the request to a known contact.                                                                                            |
| `subject_email`           | email     | One of email / phone / contact\_id is required.                                                                                            |
| `subject_phone`           | string    | E.164.                                                                                                                                     |
| `requester_email`         | email     | **Required.** Where status updates are sent.                                                                                               |
| `applicable_jurisdiction` | enum      | Default `gdpr`. Must be set explicitly to `ccpa` or `cpra` when `request_type` is `opt_out_sale` or `limit_sensitive_pi` (see note below). |
| `request_type`            | enum      | Default `know`.                                                                                                                            |
| `consumer_categories`     | string\[] | CCPA categories (access only).                                                                                                             |
| `verification_method`     | enum      | `email_link`, `email_phone`, `document`, `manual_review`.                                                                                  |
| `requester_statement`     | string    | Free text, ≤ 4096 chars.                                                                                                                   |
| `authorized_agent`        | object    | `{ agent_name, agent_email, permission_document_id? }` when an agent files on the subject's behalf.                                        |

> **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:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/compliance/dsar/dsar_8x2k…/verification \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{ "decision": "verified", "notes": "Matched gov-ID upload." }'
```

`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`.

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/compliance/dsar/dsar_8x2k…/cancel \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{ "reason": "Duplicate of dsar_7a1f…" }'
```

### 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.

### SLA dashboard

`GET /compliance/dsar/sla` returns a combined export + erasure SLA
snapshot so you never miss a statutory deadline:

```json theme={null}
{
  "items": [
    {
      "id": "dsar_8x2k…",
      "kind": "export",
      "status": "processing",
      "days_elapsed": 22,
      "days_remaining": 8,
      "severity": "amber",
      "sla_deadline_at": "2026-07-01T00:00:00.000Z",
      "approaching": true,
      "breach": false,
      "escalation_due": false
    }
  ],
  "alerts": {
    "breached": 0,
    "approaching": 1,
    "escalation_due": 0,
    "worst_severity": "amber",
    "has_alert": true
  },
  "sla_days": 30
}
```

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.

<Note>
  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.
</Note>

### Flow overview

<Steps>
  <Step title="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.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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`.
  </Step>

  <Step title="Verify phone">
    `POST /compliance/public/dsar/verify-phone` with `claim_id` and the
    6-digit `code`. Returns state `phone_verified` and next step
    `submit`.
  </Step>

  <Step title="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.
  </Step>
</Steps>

### 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.

| Variable                        | Used for                       | Default / fallback                                                        |
| ------------------------------- | ------------------------------ | ------------------------------------------------------------------------- |
| `DEVOTEL_DSAR_PROOF_FROM_EMAIL` | From-address on the email OTP. | `privacy@orbit.devotel.io`. Delivery also needs `DEVOTEL_RESEND_API_KEY`. |
| `DEVOTEL_DSAR_PROOF_SMS_FROM`   | E.164 sender on the SMS OTP.   | Falls back to `DEVOTEL_PLATFORM_DEFAULT_FROM`.                            |

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

| Control                | Limit                                                       |
| ---------------------- | ----------------------------------------------------------- |
| Cloudflare Turnstile   | Required on `begin` in production (fail-closed).            |
| Per-IP `begin`         | 3 per hour.                                                 |
| Per-email cooldown     | 1 per 60 s.                                                 |
| Per-phone SMS cooldown | 1 per 60 s.                                                 |
| Fastify per-IP gate    | 30 requests/min per IP, applied independently per endpoint. |
| OTP TTL / attempts     | 10 minutes, max 3 attempts per code.                        |
| Claim TTL              | 30 minutes end-to-end.                                      |

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.

| Variable                                 | When         | Description                                                                                                                     |
| ---------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------- |
| `DEVOTEL_TURNSTILE_SECRET_KEY`           | API (server) | Cloudflare Turnstile secret. The `begin` endpoint verifies the submitted `turnstile_token` against Cloudflare when this is set. |
| `NEXT_PUBLIC_DEVOTEL_TURNSTILE_SITE_KEY` | Web (client) | Public Turnstile site key the portal renders the widget with.                                                                   |

<Warning>
  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.
</Warning>

***

## Hosting the portal link

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.

***

## Related references

* [Consent Management](/compliance/consent-management) — record and
  look up the consent state a DSAR may ask you to honour.
* [Opt-Out & Suppression Lists](/compliance/opt-out-suppression) —
  how `delete` / `opt_out` outcomes flow into suppression.
* [Call Recording Consent](/compliance/recording-consent) — handling
  recordings referenced by an access request.
* [API Reference → Compliance](/api-reference/compliance) — full
  request/response schemas (regenerated from the live API).
