# Authentication

Programs authenticate with an organization API key. The dashboard uses a session token for the signed-in user, and AI assistants that connect over MCP can sign in with OAuth instead of holding a key.

| Credential | Who uses it | Sent as | Access |
|---|---|---|---|
| API key (`ck_live_…`) | Your code, the SDKs, MCP clients that can send headers | `X-API-Key` or `Authorization: Bearer` | One organization, limited to the key's scopes |
| Session token (JWT) | The dashboard | `Authorization: Bearer` | Every organization the user belongs to, no scope limits |
| OAuth access token | MCP clients such as ChatGPT and claude.ai | `Authorization: Bearer` | One organization, MCP endpoint only |

## API keys

An API key belongs to exactly one organization. It looks like `ck_live_` followed by 32 lowercase letters and digits. Only a SHA-256 hash of the key is stored, so the full key is shown once, when it is created; afterwards the dashboard shows its first 12 characters (for example `ck_live_7hq2…`) and when it was last used.

### Sending the key

Send the key in either header. The SDKs use `X-API-Key`.

| Header | Example |
|---|---|
| `X-API-Key` | `X-API-Key: ck_live_…` |
| `Authorization` | `Authorization: Bearer ck_live_…` |

A `Bearer` value is treated as an API key only when it starts with `ck_`; anything else is treated as a session token. If both headers are present, `X-API-Key` is used.

**cURL**

```bash
curl https://xdive.dev/api/whoami \
  -H "X-API-Key: $XDIVE_API_KEY"
```

**Python**

```python
from xdive import XDive

xdive = XDive(api_key="ck_live_…")  # or set XDIVE_API_KEY
```

**JavaScript**

```ts
import { XDive } from "xdive";

const xdive = new XDive({ apiKey: "ck_live_…" }); // or set XDIVE_API_KEY
```

A key used under another organization's path (`/orgs/{id}/…` with a different `{id}`) is rejected with `401 invalid API key`.

### Creating and revoking keys

Create keys in the dashboard under **API Keys** → **Issue new key**. Keys can also be managed over the API with a [session token](#dashboard-sessions); an API key cannot create, list or revoke keys.

`POST https://xdive.dev/api/orgs/{id}/api-keys`

| Field | Type | Default | Description |
|---|---|---|---|
| `name` | string | `"Untitled key"` | A label shown in the dashboard. |
| `scopes` | string[] | `["crawl", "scrape"]` | What the key may do; see [Scopes](#scopes). |
| `expires_in_days` | integer | `0` | Days until the key stops working, from 1 to 3650. `0` means it never expires. The dashboard suggests 90. |
| `ip_allowlist` | string[] | `[]` | Up to 32 IP addresses or CIDR ranges the key may be used from. Empty allows any address. |

The response (`201 Created`) is the only one that includes `secret`:

```json
{
  "id": "key_5b1e9c7a20d34f8e86a1c0d7e3f92b64",
  "orgId": "org_8f2c41d09a7b4e6f93d0c5a1b2e47f18",
  "name": "Production server",
  "prefix": "ck_live_7hq2",
  "secret": "ck_live_7hq2…",
  "scopes": ["crawl", "scrape", "search"],
  "createdAt": "2026-10-15T08:30:00Z",
  "lastUsedAt": null,
  "expiresAt": "2027-01-13T08:30:00Z",
  "ipAllowlist": ["203.0.113.10", "198.51.100.0/24"]
}
```

`GET /orgs/{id}/api-keys` lists the organization's keys without secrets. `DELETE /orgs/{id}/api-keys/{keyId}` revokes a key and returns `204`; the key stops working immediately on every server.

### Scopes

Each key carries a list of scopes. A request outside them fails with `403` and a message such as `this API key lacks the 'search' scope`.

| Scope | Allows |
|---|---|
| `scrape` | `GET` and `POST /orgs/{id}/scrape` (including scrapes with browser actions or screenshots), `POST /orgs/{id}/parse` |
| `crawl` | `POST /orgs/{id}/crawl`, `POST /orgs/{id}/map` |
| `scrape` or `crawl` | `POST /orgs/{id}/batch/scrape`, `GET` and `DELETE /orgs/{id}/crawls/{crawlId}`, `GET /orgs/{id}/crawls/{crawlId}/errors`, `POST /orgs/{id}/extract`, `POST /orgs/{id}/llmstxt` |
| `search` | `POST /orgs/{id}/search`, `POST /orgs/{id}/research` |
| `browser` | Interactive browser sessions and saved browser profiles under `/orgs/{id}/browser/` |
| `connectors` | Data connectors under `/orgs/{id}/connectors` |

A new key may carry only these five scopes; any other value is rejected with `400`, which lists the valid ones. Batch scrape results are read through the crawl endpoints, which is why those accept `scrape` as well as `crawl`.

Organization management is not available to API keys at all: API keys, usage, plan and billing, schedules and monitors, history, AI settings, the audit log, crawl export and connected apps all require a session token.

### Expiry and IP allowlists

An expired key, or a key used from an address outside its allowlist, is rejected with `401 invalid API key`, the same answer as for an unknown key. The client address is the connection's address, or the `X-Forwarded-For` address when the request arrives through a proxy the server trusts.

### Suspended organizations

Keys of a suspended organization are rejected with `403 organization is suspended`. `GET /whoami` still answers and reports `"suspended": true`.

## Find the organization

`GET /whoami` identifies the caller. It accepts an API key or a session token, and the SDKs use it to discover the organization id from the key.

`GET https://xdive.dev/api/whoami`

**cURL**

```bash
curl https://xdive.dev/api/whoami \
  -H "X-API-Key: $XDIVE_API_KEY"
```

**Python**

```python
from xdive import XDive

who = XDive().whoami()
print(who["orgId"], who["scopes"])
```

**JavaScript**

```ts
import { XDive } from "xdive";

const who = await new XDive().whoami();
console.log(who.orgId, who.scopes);
```

With an API key:

```json
{
  "auth": "api_key",
  "keyId": "key_5b1e9c7a20d34f8e86a1c0d7e3f92b64",
  "orgId": "org_8f2c41d09a7b4e6f93d0c5a1b2e47f18",
  "plan": "free",
  "scopes": ["crawl", "scrape", "search"],
  "suspended": false
}
```

| Field | Type | Description |
|---|---|---|
| `auth` | string | `api_key` |
| `keyId` | string | The key's id (`key_…`). |
| `orgId` | string | The organization the key belongs to (`org_…`). |
| `scopes` | string[] | The key's scopes. |
| `plan` | string | The organization's plan: `free`, `pro` or `enterprise`. |
| `suspended` | boolean | Whether the organization is suspended. |

With a session token, `auth` is `user` and the response lists the user's organizations:

```json
{
  "auth": "user",
  "orgs": [
    {
      "id": "org_8f2c41d09a7b4e6f93d0c5a1b2e47f18",
      "name": "Acme",
      "plan": "free",
      "role": "owner",
      "createdAt": "2026-10-01T09:12:44Z"
    }
  ],
  "userId": "usr_0c7d52e1a94b4f3e8d6a2b19f50c3e7a"
}
```

An invalid key returns `401 invalid API key`; a missing or invalid session returns `401 unauthorized`.

## Dashboard sessions

Signing in (`POST /auth/login` with `email` and `password`) returns a session `token`: a JWT that is valid for 7 days and is sent as `Authorization: Bearer <token>`.

```bash
curl -X POST https://xdive.dev/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email": "you@example.com", "password": "…"}'
```

A session acts as the user. It can use every organization the user is a member of and is not limited by scopes, which is why it is meant for the dashboard and the management endpoints rather than for programs. The account's status is checked on every request: if an administrator has not approved the account yet, or has rejected it, the session receives `403` with `ACCOUNT_PENDING_APPROVAL` or `ACCOUNT_REJECTED` (see [Errors and limits](https://xdive.dev/docs/errors.md#error-codes)).

## OAuth for AI assistants

MCP clients that cannot hold an API key, such as ChatGPT and claude.ai connectors, sign in with OAuth 2.1. The client discovers the server from `/.well-known/oauth-protected-resource` and `/.well-known/oauth-authorization-server`, registers itself, and sends you to a consent screen where you choose the organization. The flow uses the authorization code grant with PKCE (`S256` only).

- Access tokens last 1 hour; refresh tokens last 30 days and are rotated on use.
- Tokens are bound to the MCP endpoint (`/api/mcp`). The REST API rejects them, so they cannot be used for direct API calls.
- A grant can scrape, crawl and search for the organization you chose.
- Review and revoke connected clients under **API Keys** → **Connected apps**.

ChatGPT, Claude, Claude Code, Codex, Cursor, VS Code, Gemini CLI and Windsurf all sign in this way. Clients and scripts that cannot run a browser sign-in can send an API key in a header instead. See [MCP](https://xdive.dev/docs/mcp.md) for setup.

## Device sign-in for CLIs and agents

A command-line tool or AI agent can get an API key with one approval in the browser, using the OAuth 2.0 Device Authorization Grant ([RFC 8628](https://www.rfc-editor.org/rfc/rfc8628)). The agent shows a short code, you open `/device`, check the code, choose the organization and click **Allow**, and the agent receives a normal organization API key. Nothing has to be copied or pasted.

The result is an API key rather than an OAuth access token on purpose: it works with every REST endpoint and with MCP, carries scopes like any other key, never needs refreshing, and is listed under **API Keys** (as `Device: <client name> <date>`), where you can revoke it. The server advertises the endpoint as `device_authorization_endpoint` in `/.well-known/oauth-authorization-server`.

**1. Ask for a code.** `client_id` is optional; without it the default public client `cli` is used, and `client_name` sets the name you will see (it is shown as unverified). `scope` is a space-separated list of [key scopes](#scopes) and defaults to `scrape crawl search`.

`POST https://xdive.dev/api/oauth/device_authorization`

```bash
curl -X POST https://xdive.dev/api/oauth/device_authorization \
  -d client_name="My agent" \
  -d scope="scrape crawl search"
```

```json
{
  "device_code": "dc_Qm9vdGgtY29kZS1leGFtcGxlLW5vdC1yZWFs",
  "user_code": "WDJB-MJHT",
  "verification_uri": "https://xdive.dev/device",
  "verification_uri_complete": "https://xdive.dev/device?user_code=WDJB-MJHT",
  "expires_in": 600,
  "interval": 5,
  "scope": "scrape crawl search"
}
```

**2. Send the person to the page.** Show `user_code` and `verification_uri`, or open `verification_uri_complete`, which fills in the code. They sign in if needed, compare the code, pick an organization and approve or deny. The code works for 10 minutes and only once; typing it in lower case or without the dash is fine.

**3. Poll for the key.** Every `interval` seconds, until you get a key or a final error:

```bash
curl -X POST https://xdive.dev/api/oauth/token \
  -d grant_type=urn:ietf:params:oauth:grant-type:device_code \
  -d device_code="$DEVICE_CODE"
```

If you passed a `client_id` in step 1, send the same one here. Once approved, the response carries the key in `access_token`. It is returned exactly once:

```json
{
  "access_token": "ck_live_…",
  "token_type": "Bearer",
  "scope": "scrape crawl search",
  "org_id": "org_8f2c41d09a7b4e6f93d0c5a1b2e47f18",
  "key_id": "key_5b1e9c7a20d34f8e86a1c0d7e3f92b64",
  "key_name": "Device: My agent 2026-10-17"
}
```

Until then the endpoint answers `400` with one of these `error` values:

| `error` | Meaning | What to do |
|---|---|---|
| `authorization_pending` | Not approved yet. | Keep polling at `interval`. |
| `slow_down` | You polled too fast. | Add 5 seconds to your interval, then keep polling. |
| `access_denied` | The person clicked **Deny**, or the account or organization lost access. | Stop. |
| `expired_token` | 10 minutes passed. | Start again from step 1. |
| `invalid_grant` | Unknown code, another client's code, or a key was already issued for it. | Stop. |

A suspended organization, or an account waiting for approval or rejected, gets no key (`403`). Requests for codes are rate-limited per IP address, and the `/device` page locks a user out for 15 minutes after 10 wrong codes.

Use the key like any other: `Authorization: Bearer ck_live_…` against `/api/orgs/{org_id}/…`.

## Keeping keys secret

- Use keys from server-side code only. Never put a key in browser or mobile code, or commit it to a repository. Browser requests from origins the server does not allow are refused with `CORS_ORIGIN_DENIED`.
- Load keys from the environment (`XDIVE_API_KEY`) or a secret manager.
- Create one key per application and environment with only the scopes it needs, an expiry, and an IP allowlist where your servers have fixed addresses.
- Rotate by issuing a new key, deploying it, then revoking the old one.
- If a key leaks, revoke it in **API Keys**. Revocation takes effect immediately.

---

Source: https://xdive.dev/docs/authentication · All docs for agents: https://xdive.dev/llms.txt
