API Auth and Error Model
Understand how authentication works across the 10x API, what errors you can expect, and how to handle them.
Auth modes
The API uses four authentication modes. The mode you need depends on the endpoint you are calling.
| Auth mode | How to pass it | Route families | Who uses it |
|---|---|---|---|
JWT | Authorization: Bearer <jwt-token> | /v2/handles/*, /v2/account/domains/*, /v2/billing/* | Creators, operators, and owners managing their resources via the dashboard or scripts |
PAT | Authorization: Bearer patv1_<tokenId>.<secret> | /v2/public/handles/{handle}/* | Automation scripts, CI/CD pipelines, external integrations |
VISITOR_COOKIE | Automatic (browser cookie) | /v2/public/pages/*, /v2/visitor/* | Visitors accessing paid pages, verifying purchases, managing entitlements |
NONE | No auth required | Selected /v2/public/* endpoints | Public runtime: conversions, chain signals, context resolution |
When to use JWT vs. PAT
Note
- Use JWT for interactive sessions and control-plane operations (creating handles, managing collaborators, configuring domains).
- Use PAT for automated operations on a specific handle (link management, analytics queries, webhook subscriptions).
- PATs are scoped to a single handle. JWTs can access all handles the user owns or collaborates on.
Need concrete route-by-route guidance? See JWT vs PAT: When to Use Each.
JWT vs PAT in practice
| Question | JWT | PAT |
|---|---|---|
Can it call control-plane routes like /v2/handles/*? | Yes | No (unless an endpoint explicitly supports PAT) |
| Is it ideal for unattended CI/cron jobs? | Not ideal (session token) | Yes (scoped automation credential) |
| Can it create/revoke PATs? | Yes (OPERATOR+) | No |
| Is it bound to one handle? | No | Yes |
| Can it be scope-limited per integration? | Role-based only | Yes, scope-based |
Concrete examples
JWT: control-plane call
curl -sS "https://api.10x.in/v2/handles" \
-H "Authorization: Bearer ${JWT_TOKEN}"PAT: handle automation call
curl -sS "https://api.10x.in/v2/public/handles/${HANDLE}/links" \
-H "Authorization: Bearer ${PAT_TOKEN}"JWT required: create PAT
curl -sS -X POST "https://api.10x.in/v2/handles/${HANDLE}/tokens" \
-H "Authorization: Bearer ${JWT_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"name":"ci-worker","scopes":["links.read","links.write"],"expiresInDays":30}'JWT refresh behavior
POST /v2/auth/refresh uses the refresh cookie established at login. For browser clients, send requests with credentials: "include".
For CLI workflows, use a cookie jar:
curl -sS -c cookies.txt -X POST "https://api.10x.in/v2/auth/login" \
-H "content-type: application/json" \
-d '{"email":"you@example.com","password":"your-password"}'
curl -sS -b cookies.txt -c cookies.txt -X POST "https://api.10x.in/v2/auth/refresh" \
-H "content-type: application/json"
Obtaining a JWT
Request
curl -sS -X POST "https://api.10x.in/v2/auth/login" \
-H "Content-Type: application/json" \
-d '{"email": "you@example.com", "password": "your-password"}'Body
{
"email": "you@example.com",
"password": "your-password"
}Returns an access token. Include it in subsequent requests:
Authorization: Bearer <access-token>
JWTs expire after a set period. When you receive a 401 token_expired error, log in again to get a fresh token.
Obtaining a PAT
See API Tokens and Automations for creating and managing PATs.
Error model
Every error response follows a consistent structure:
{
"error": {
"code": "insufficient_scope",
"message": "Token does not have the required scope: analytics.*",
"statusCode": 403
}
}
Error categories
| HTTP status | Error codes | Meaning | Should you retry? |
|---|---|---|---|
| 400 | invalid_*, validation_error | Request is malformed or missing required fields | No — fix the request |
| 401 | missing_bearer_token, invalid_token, token_expired | Authentication failed | No — fix your credentials or log in again |
| 403 | insufficient_scope, insufficient_role, feature_locked | Authorized but not permitted | No — upgrade scope, role, or plan |
| 404 | not_found | Resource does not exist | No — check the ID or slug |
| 409 | conflict | Resource already exists or state conflict | No — check current state |
| 429 | rate_limited | Too many requests | Yes — back off and retry |
| 5xx | Various | Server error | Yes — retry with exponential backoff |
Auth error quick reference
| Error | Cause | Fix |
|---|---|---|
401 missing_bearer_token | No Authorization header | Add the header with your JWT or PAT |
401 invalid_token | Token is malformed or the secret is wrong | Check the token format and value |
401 token_expired | JWT has expired | Log in again to get a fresh token |
403 insufficient_scope | PAT does not have the required scope | Create a new PAT with the needed scope |
403 insufficient_role | User does not have the required role on this handle | Ask the handle owner to upgrade your role |
403 feature_locked | Feature requires a higher plan tier | Upgrade your plan (see Plans, Quotas, and Feature Gates) |
Retry policy
Follow these rules for reliable integrations:
| Scenario | Action |
|---|---|
429 (rate limited) | Wait for the time indicated in the Retry-After header, then retry |
500, 502, 503, 504 | Retry with exponential backoff: 1s, 2s, 4s, 8s, 16s (max 5 attempts) |
400, 401, 403, 404, 409 | Do not retry — these are deterministic errors that will not resolve on their own |
Rate limits
The API enforces per-handle rate limits. Exact limits depend on your plan tier:
| Plan | Requests per minute |
|---|---|
| Free | 60 |
| Pro | 300 |
| Business | 1000 |
See Plans, Quotas, and Feature Gates for the full plan comparison.
Common integration patterns
Before creating a resource, check if it exists:
curl -sS "https://api.10x.in/v2/handles/{handle}/links/{slug}" \
-H "Authorization: Bearer <access-token>"
If you receive 404, proceed with creation. If you receive 200, decide whether to update or skip.
Many endpoints support upsert semantics. These create if the resource does not exist and update if it does, avoiding the need for a check-then-act flow.
curl -sS -X PUT "https://api.10x.in/v2/handles/{handle}/links/{slug}" \
-H "Authorization: Bearer <access-token>" \
-H "Content-Type: application/json" \
-d '{"destinationUrl": "https://example.com/product", "title": "Product Page"}'For conversion submissions and other critical writes, use an idempotencyKey:
Request
curl -sS -X POST "https://api.10x.in/v2/public/conversions" \
-H "Content-Type: application/json" \
-d '{"ctx": "...", "eventType": "purchase", "value": 49.99, "idempotencyKey": "order-12345"}'Body
{
"ctx": "...",
"eventType": "purchase",
"value": 49.99,
"idempotencyKey": "order-12345"
}If you submit the same key twice, the second request returns 200 with deduped: true instead of creating a duplicate.
Related
JWT vs PAT
Decision guide with route boundaries and examples.
API Tokens
Create and manage PATs
Endpoint Coverage Map
Full endpoint reference
Error Codes
Common codes and handling guidance
Plans and Quotas
Rate limits and plan comparison
Troubleshooting
Debug common issues