View .md

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.

CredentialWho uses itSent asAccess
API key (ck_live_…)Your code, the SDKs, MCP clients that can send headersX-API-Key or Authorization: BearerOne organization, limited to the key's scopes
Session token (JWT)The dashboardAuthorization: BearerEvery organization the user belongs to, no scope limits
OAuth access tokenMCP clients such as ChatGPT and claude.aiAuthorization: BearerOne 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.

HeaderExample
X-API-KeyX-API-Key: ck_live_…
AuthorizationAuthorization: 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.

bash
curl https://xdive.dev/api/whoami \
  -H "X-API-Key: $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; an API key cannot create, list or revoke keys.

POST/orgs/{id}/api-keys
FieldTypeDefaultDescription
namestring"Untitled key"A label shown in the dashboard.
scopesstring[]["crawl", "scrape"]What the key may do; see Scopes.
expires_in_daysinteger0Days until the key stops working, from 1 to 3650. 0 means it never expires. The dashboard suggests 90.
ip_allowliststring[][]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.

ScopeAllows
scrapeGET and POST /orgs/{id}/scrape (including scrapes with browser actions or screenshots), POST /orgs/{id}/parse
crawlPOST /orgs/{id}/crawl, POST /orgs/{id}/map
scrape or crawlPOST /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
searchPOST /orgs/{id}/search, POST /orgs/{id}/research
browserInteractive browser sessions and saved browser profiles under /orgs/{id}/browser/
connectorsData 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/whoami
bash
curl https://xdive.dev/api/whoami \
  -H "X-API-Key: $XDIVE_API_KEY"

With an API key:

json
{
  "auth": "api_key",
  "keyId": "key_5b1e9c7a20d34f8e86a1c0d7e3f92b64",
  "orgId": "org_8f2c41d09a7b4e6f93d0c5a1b2e47f18",
  "plan": "free",
  "scopes": ["crawl", "scrape", "search"],
  "suspended": false
}
FieldTypeDescription
authstringapi_key
keyIdstringThe key's id (key_…).
orgIdstringThe organization the key belongs to (org_…).
scopesstring[]The key's scopes.
planstringThe organization's plan: free, pro or enterprise.
suspendedbooleanWhether 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": "[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.

POST/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:

errorMeaningWhat to do
authorization_pendingNot approved yet.Keep polling at interval.
slow_downYou polled too fast.Add 5 seconds to your interval, then keep polling.
access_deniedThe person clicked Deny, or the account or organization lost access.Stop.
expired_token10 minutes passed.Start again from step 1.
invalid_grantUnknown 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.