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

# Authentication and session model

> The three Orbit credential classes — dv_-prefixed API keys, dashboard session tokens, and the SCIM provisioning token — and the resolution rule that decides which one a request carries.

# Authentication and session model

Orbit recognizes three credential classes: `dv_`-prefixed API keys, dashboard
session tokens, and the SCIM provisioning token. Each request carries one of
them, and a single resolution rule decides the class from the headers you
send. This page defines the vocabulary, states the rule, and tells you which
credential each audience (server, dashboard, identity provider) should send.
For route-by-route rejection codes, see the
[authentication troubleshooting page](/troubleshooting/auth-and-api-keys).

## Section 1 — The credential vocabulary

Three credential classes authenticate to Orbit. They look different on the
wire, authorize different surfaces, and get managed in different places.

**API keys (`dv_` prefix).** Long-lived credentials you create in the
dashboard under **Settings → API Keys** and use for programmatic calls. Four
forms exist:

| Form          | Class       | Allowed scopes                  |
| ------------- | ----------- | ------------------------------- |
| `dv_live_sk_` | live secret | Write and administrative scopes |
| `dv_test_sk_` | test secret | Write scopes in test mode       |
| `dv_live_pk_` | live public | Read-only scopes only           |
| `dv_test_pk_` | test public | Read-only scopes in test mode   |

A public key (`pk`) is meant for browser or mobile bundles, so the platform
rejects write and administrative scopes on it at creation with a `422`. For
anything that sends or modifies data — `messages:write`, `contacts:write`,
`admin`, the `*` wildcard — use a secret key (`sk`).

**Dashboard session token (JWT).** A signed JSON Web Token issued when a
dashboard user signs in — email/password, or SAML single sign-on where
enabled. Session tokens carry a user's identity and the org memberships they
belong to; they authenticate the *person*, not an application. You recognize
one by the `eyJ` prefix that every JWT starts with.

**SCIM provisioning token.** A dedicated Bearer credential that
authenticates exactly one surface: the `/scim/v2/{orgSlug}` endpoints your
identity provider calls to create and remove dashboard users. It is neither
an API key nor a session token, and it is valid on no other route.

## Section 2 — How resolution is decided

The server determines which credential class a request carries with one
precedence rule:

1. A non-empty `X-API-Key` header is checked **first**.
2. Only if `X-API-Key` is absent does the server look at
   `Authorization: Bearer`, and only when the token carries a `dv_` prefix is
   it treated as an API key.
3. Any other Bearer token — the `eyJ…` JWT shape — is treated as a dashboard
   session token.

Because a JWT always starts with `eyJ` and every API key starts with `dv_`,
the two forms never collide on the `Authorization` header. This is the
fallback rule stated on the [authentication page](/authentication), and it
exists so browser environments where the `X-API-Key` custom header is blocked
by CORS can still send an API key as a Bearer token.

The practical consequence: sending both an `X-API-Key` header and a session
`Bearer` is unambiguous — the API key wins. A malformed request mixes the two
only when the client generated it wrong; the resolution rule still makes the
outcome deterministic.

## Section 3 — Session token lifecycle

A dashboard session token moves through four states:

1. **Issued at login.** Email/password sign-in, or the SAML callback from
   your identity provider when SSO is configured, both mint a signed JWT.
2. **Used until expiry.** The dashboard sends it on every API request as an
   `Authorization: Bearer eyJ…` header. Session tokens have bounded lifetimes;
   an expired token gets a `401`.
3. **Rotated.** Re-authentication or a token refresh issues a replacement
   JWT; the old one expires on its own schedule.
4. **Terminated on sign-out.** Signing out invalidates the token; the next
   request with it fails.

This lifecycle is why the SSO pages assume a session credential exists:
[SAML single sign-on](/guides/saml-sso-enrollment) and
[SCIM provisioning](/compliance/scim-provisioning) are dashboard-side
concerns. SAML mints session tokens at login; SCIM manages the users who hold
them. Neither touches API keys — server-to-server integrations keep using
`dv_` keys while users come and go through the IdP.

## Section 4 — The SCIM provisioning token

Generate the token under **Settings → SCIM** in the dashboard, or with
`POST /api/v1/settings/scim/generate-token`. The full value is **shown once**
at creation — copy it into your IdP immediately; if you lose it, generate a
new one. The endpoint is a dashboard-session-authorized call, so minting or
rotating the token requires a signed-in administrator.

Your IdP then sends the token as a Bearer credential against the SCIM
resource endpoints (`/scim/v2/{orgSlug}/Users`, `/Groups`), which return
`application/scim+json`. The step-by-step IdP walkthroughs for Okta, Entra
ID, and generic clients are in the [SCIM 2.0 provisioning guide](/guides/scim-provisioning)
(and the tenant-owned posture view in [compliance](/compliance/scim-provisioning)).

Because the value is shown once, treat rotation as "generate a new token,
update the IdP, discard the old" — a one-step operation, but one you must
plan for when you do not retain the original.

## Section 5 — Where tenant identity comes from

The credential is also how the platform learns which tenant a request belongs
to. Every org is a row in the shared `organizations` catalog; each API key is
issued against exactly one org, and the server resolves the key back to that
org on every request. A dashboard session token likewise carries its org
bound at login and selected from the user's memberships. Client-supplied
claims of tenant identity (`tenant_id` fields in a request body) are never
trusted. The full resolution chain — catalog lookup, `tenant_<tenant_id>`
schema routing, and the subaccount sharing rules — is covered in
[Tenant isolation](/concepts/tenant-isolation).

## Section 6 — What to branch on

Send the credential class that matches the caller's relationship to the
platform:

| Audience / surface                                   | Credential to send                                |
| ---------------------------------------------------- | ------------------------------------------------- |
| Server-to-server integration (your backend)          | Secret API key (`dv_live_sk_`) in `X-API-Key`     |
| Browser or mobile client                             | Public API key (`dv_live_pk_`), scoped to reads   |
| Dashboard user (browser app, CLI acting as a person) | Session token (`Authorization: Bearer eyJ…`)      |
| Identity provider provisioning users                 | SCIM token (`Authorization: Bearer <scim-token>`) |
| Test-mode development of any of the above            | The `dv_test_` variant of the key                 |

When you build on an SDK, set the same choice: the SDK sends either an API
key or a session token; the resolution rule above applies untouched.

Two rules of thumb close out most mistakes:

* **A 401 means the credential did not resolve.** A truncated key, a
  `dv_` value sent as `Authorization: Bearer` without the full prefix, or a
  session token whose expiry has passed all fail here.
* **A 422 from SSO or SCIM usually means you sent a session surface an API
  key** (or vice versa). Match the credential class to the table above, then
  re-check the scopes page for `sk` vs `pk` if the route still rejects.
