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

# Rights requests

> A server-driven DSAR / ARCO form for access, deletion, portability, and more

```ts theme={null}
import { createRightsRequest } from "idesify/rights";
```

The widget fetches the request types and form fields that apply to your tenant and the subject's jurisdiction, renders them, and submits the request. It is light and framework-free — pure DOM plus a scoped stylesheet, with no camera and no engine.

## Pick a mode first

This single decision shapes the whole integration.

| | `anonymous` | `authenticated` |
| - | - | - |
| Where it runs | Your public privacy page | Inside your product, for a signed-in user |
| Identifies the subject by | What they type into the form | The session token you supply |
| You must provide | `tenantUuid` | `token` |
| CAPTCHA | Required — you supply the token | Never |
| Identity fields | The subject fills them in | Prefilled or omitted |

<Note>
  In authenticated mode the server binds the request to the session's subject and **ignores** any identity fields posted from the browser. A user cannot file a request as somebody else, even by editing the payload.
</Note>

## Anonymous — a public privacy page

The public surface is CAPTCHA-gated. The SDK bundles no third-party CAPTCHA script, which keeps your CSP tight, so you render the challenge and hand back a fresh token on demand.

```ts theme={null}
import { createRightsRequest } from "idesify/rights";

const controller = createRightsRequest({
  mode: "anonymous",
  container: document.getElementById("dsar")!,
  tenantUuid: "a1b2c3d4-e5f6-7890-abcd-ef1234567890", // your portal UUID
  language: navigator.language,
  // Called only when the server says a CAPTCHA is required.
  captchaProvider: () => turnstile.execute(siteKey),
  callbacks: {
    onReady:  (runtime) => console.log("offering", runtime.request_types.length, "types"),
    onSubmit: (result)  => console.log(result.outcome, result.tracking_token),
    onError:  ({ error, fatal }) => console.error(error, { fatal }),
  },
});
```

If the server requires a CAPTCHA and no `captchaProvider` was supplied, submission fails with an explicit error.

<Note>
  Your portal UUID is a public identifier — it appears in your Privacy Center URL and is safe to ship in frontend code. It is not a credential and grants no access on its own.
</Note>

## Authenticated — inside your product

Your API key never reaches the browser. Your backend exchanges it for a short-lived session token, exactly as with the verification flow.

```ts theme={null}
const controller = createRightsRequest({
  mode: "authenticated",
  container: document.getElementById("dsar")!,
  token,                                    // session token from your backend
  callbacks: {
    onSubmit: (result) => console.log(result.outcome),
    onExpire: async () => controller.resume(await mintNewToken()),
  },
});
```

No `tenantUuid` and no CAPTCHA. The subject's identity fields arrive prefilled, or are dropped entirely, because the session already supplies them.

## What the server decides, not you

The form renders **strictly** from configuration the server returns, so the same code serves every tenant and jurisdiction:

| Field | Meaning |
| - | - |
| `request_types` | Which rights the subject may exercise here. |
| `fields` | What the form collects. Jurisdiction-driven — some jurisdictions add a national-ID field with its own label and validation pattern; others do not. |
| `identity_gate` | Whether a CAPTCHA is needed, and whether some request types require an identity step-up. |
| `jurisdiction` | The resolved jurisdiction and, where the platform covers it, the statutory response deadline in days. |

<Warning>
  Never hard-code a field list. It will break the moment a new jurisdiction is enabled for your tenant.
</Warning>

<Warning>
  **Do not compute the deadline yourself.** `jurisdiction.deadline_days` is absent for jurisdictions with no encoded regulation, and the SDK renders deadline copy only when it is present. Deriving a date in JavaScript gets it wrong — "one month" is not 30 days, and month arithmetic overflows, so 31 January plus one month lands in March. Show what the server sends.
</Warning>

## The two outcomes

`onSubmit(result)` fires once the server accepts the request. There are two shapes, and you must handle both.

```ts theme={null}
type RightsSubmitOutcome = "submitted" | "biometrics_required";

interface RightsRequestResult {
  outcome: RightsSubmitOutcome;
  tracking_token?: string;    // opaque status token
  verification_url?: string;  // present when outcome is "biometrics_required"
  message: string;            // localized confirmation copy from the server
}
```

* **`submitted`** — accepted. Show `message`, and the `tracking_token` if present so the subject can check on it later.
* **`biometrics_required`** — accepted, but it cannot be actioned until the subject proves who they are. Send them to `verification_url`. The built-in form warns about this before submit, so the step-up is never a surprise.

The bundled terminal screen handles both. Override it with `slots.renderTerminal` if you would rather do it yourself.

## createRightsRequest(options)

| Option | Type | Notes |
| - | - | - |
| `mode` | `"anonymous" \| "authenticated"` | **Required.** |
| `container` | `HTMLElement` | **Required.** Mount target. |
| `tenantUuid` | `string` | **Anonymous only, required.** Your portal UUID. |
| `token` | `string` | **Authenticated only, required.** Session token from your backend. |
| `baseUrl` | `string` | Endpoint prefix. Defaults per mode. Override for another origin or a proxy path. |
| `language` | `string` | BCP-47. Sets `Accept-Language` and the SDK's own copy (`en`, `es`, `pt`; falls back to `en`). |
| `colorScheme` | `"light" \| "dark" \| "system"` | Default `system`. |
| `accent` | `string` | Brand accent, any CSS color. |
| `captchaProvider` | `() => Promise<string>` | **Anonymous only.** Resolves a fresh CAPTCHA token on demand. |
| `slots` | `RightsUISlots` | `labels` for per-string copy; `renderTerminal` to replace the result screen. |
| `callbacks` | `RightsCallbacks` | `onReady`, `onSubmit`, `onError`, `onExpire`. |
| `autoStart` | `boolean` | Fetch and mount immediately. Default `true`. |

### RightsRequestController

| Member | Description |
| - | - |
| `start()` | Fetch configuration and mount the form. Idempotent. |
| `resume(newToken)` | Authenticated only. Swap in a freshly minted token and refetch. |
| `destroy()` | Remove the UI, drop listeners, abort in-flight requests. |
| `state()` | One of `idle`, `loading`, `ready`, `submitting`, `complete`, `expired`, `error`. |
| `on(event, handler)` / `off(event, handler)` | Emitter mirror of the callbacks. `on` returns an unsubscribe function. |

## Errors

| Class | When |
| - | - |
| `RightsValidationError` | Field errors, available on `.errors` keyed by field. The form re-shows itself with the offending fields marked. Not fatal. |
| `RightsTokenExpiredError` | The session token expired in authenticated mode. `onExpire` also fires — re-mint and call `resume(newToken)`. |
| `RightsGatewayError` | Any other non-2xx. Carries `.status` and a best-effort `.body`. |

```ts theme={null}
import { RightsValidationError } from "idesify/rights";

callbacks: {
  onError: ({ error }) => {
    if (error instanceof RightsValidationError) {
      console.warn("fix these fields:", error.errors);
    }
  },
}
```

A validation error keeps the widget mounted and interactive.

## Privacy notes

* In anonymous mode the subject **asserts** their own identity. That is exactly why the identity gate and the biometric step-up exist — do not action a request on the strength of the form alone.
* `tracking_token` is opaque and contains no personal data. It is safe to show the subject and to store.
* Responses are enumeration-safe: a request for a subject that does not exist returns the same generic confirmation as one that does. Do not try to infer existence from the response.

## Embedding

Much lighter than the verification flow — no camera, no engine, no secure-context requirement. See [Embedding requirements](/sdk/embedding#rights-and-consent-widgets).
