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 https://xdive.dev/api/whoami \
-H "X-API-Key: $XDIVE_API_KEY"from xdive import XDive
xdive = XDive(api_key="ck_live_…") # or set XDIVE_API_KEYimport { XDive } from "xdive";
const xdive = new XDive({ apiKey: "ck_live_…" }); // or set XDIVE_API_KEYA 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; an API key cannot create, list or revoke 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. |
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:
{
"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.
curl https://xdive.dev/api/whoami \
-H "X-API-Key: $XDIVE_API_KEY"from xdive import XDive
who = XDive().whoami()
print(who["orgId"], who["scopes"])import { XDive } from "xdive";
const who = await new XDive().whoami();
console.log(who.orgId, who.scopes);With an API key:
{
"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:
{
"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>.
curl -X POST https://xdive.dev/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "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).
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 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). 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 and defaults to scrape crawl search.
curl -X POST https://xdive.dev/api/oauth/device_authorization \
-d client_name="My agent" \
-d scope="scrape crawl search"{
"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:
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:
{
"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.