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

# Authentication

> API keys, scopes, expiry and rotation — and what happens when a key leaks.

Every request carries an API key as a bearer token:

```bash theme={null}
curl https://app.dotportion.com/api/v1/account \
  -H "Authorization: Bearer sk_live_..."
```

<Warning>
  Send the key in the header, never in a query string. URLs end up in server logs, browser
  history and `Referer` headers — a key in a URL is a key you have to assume is public.
</Warning>

## Key format

| Prefix     | Environment                                                         |
| ---------- | ------------------------------------------------------------------- |
| `sk_live_` | Your real data. Invitations send email and consume plan quota.      |
| `sk_test_` | [Test mode](/test-mode). Fixed sample data; writes persist nothing. |

Keys are 256-bit random strings. We store only a SHA-256 hash, so **a key is shown exactly
once, at creation** — if you lose it, revoke it and mint another. There is no "show again".

## Scopes

Grant the narrowest scope that does the job. Scopes are **hierarchical**: `admin` satisfies
`write`, which satisfies `read`.

| Scope   | Grants                                                                                                          |
| ------- | --------------------------------------------------------------------------------------------------------------- |
| `read`  | List and fetch challenges, assessments, invitations, submissions, reports, members, account.                    |
| `write` | Everything `read` can do, plus create, update and delete assessments, and invite, resend and revoke candidates. |
| `admin` | Everything `write` can do. Reserved for future administrative endpoints.                                        |

If a key lacks the scope an endpoint needs, you get `403 INSUFFICIENT_SCOPE`. The message
names the scope **required** and never the scopes granted — a stolen read key shouldn't
double as a map of what else to try.

<Tip>
  Use a separate key per integration. Rate limits are per key, so one noisy job can't
  starve another, and you can revoke one without disturbing the rest.
</Tip>

## Expiry and rotation

A key can be created with an expiry of 30, 90 or 365 days, or none. **An expired key
behaves exactly like a revoked one** — `401 INVALID_TOKEN`, no grace period.

To rotate without downtime:

<Steps>
  <Step title="Create the replacement">
    Same scopes, new key.
  </Step>

  <Step title="Deploy it">
    Roll it out everywhere the old key was used.
  </Step>

  <Step title="Confirm the old key is idle">
    The dashboard shows **Last used** per key, updated within a minute or so.
  </Step>

  <Step title="Revoke the old key">
    Effective on its next request.
  </Step>
</Steps>

An organization can hold up to 20 active keys. That's a blast-radius limit, not a
licensing one: rotate rather than accumulate.

## When a key leaks

Revoke it in **Settings → API keys**. Revocation takes effect on the next request — there
is no cache to wait out.

What a leaked key can and cannot do:

* It is bound to **one organization**. It cannot read another organization's data, and
  cannot be widened.
* It carries **only its own scopes**. A `read` key cannot create or delete anything.
* It **cannot** change billing, invite team members, alter branding, or read candidate
  workspace archives beyond the signed URLs the API already returns.
* It **can** read candidate names, emails and hiring reports within your organization —
  treat a `read` key as personal data access, not merely metadata.

## Brute force

Repeated failed authentication from one IP is throttled. Once tripped, responses stop
distinguishing a wrong key from a valid one, so failures carry no information. Successful
requests never count toward it, so a busy integration — including several behind one
NAT — is unaffected.
