API reference
Updated
The browser script calls the challenge endpoint. Your server calls verify and stats with the site's secret key. Scripts call sites with an account key. The whole API is in /openapi.json (OpenAPI 3.1).
GET /api/v1/challenge?pk=pk_live_…
Returns a challenge for the site. CORS permits every origin, or only the origins on the site's list when one is set. A rate limit applies per IP address (120 a minute) and per site (6,000 a minute). The answer is Altcha-compatible JSON:
curl "https://bot403.com/api/v1/challenge?pk=pk_live_YOUR_PUBLIC_KEY"
{ "algorithm": "SHA-256", "challenge": "8b4f…", "maxnumber": 100000,
"salt": "a1b2c3?d=medium&expires=1789999999&o=shop.example.com&pk=pk_live_…&",
"signature": "0c3d…", "expires": 1789999999, "difficulty": "medium",
"honeypot": "_website" }
Errors: 404 unknown public key, 403 origin not on the site's list (only when a list is set), 429 rate limit (with Retry-After).
POST /api/v1/verify
Header Authorization: Bearer sk_live_…. Body: JSON {"solution": "…"} or form fields (_bot403, the honeypot, anything else is ignored). Form posts may carry the secret key in a secret field when you cannot set headers. Answers are on the verify page. The token is at most 4 KB; the body at most 64 KB.
curl -X POST https://bot403.com/api/v1/verify \
-H "Authorization: Bearer sk_live_YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"solution": "<the _bot403 field from the form>"}'
GET /api/v1/stats
Header Authorization: Bearer sk_live_…. Returns the site's totals for the last 30 days, one row per day, and the quota of the account:
curl https://bot403.com/api/v1/stats \
-H "Authorization: Bearer sk_live_YOUR_SECRET_KEY"
{ "site": "pk_live_…", "window_days": 30,
"totals": { "challenges": 4210, "solved": 4102, "verify_ok": 4090, "verify_fail": 12, "rate_limited": 0,
"fail": { "expired": 3, "replay": 0, "invalid": 9, "wrong_site": 0, "quota": 0, "honeypot": 0 } },
"days": [ { "day": "2026-09-16", "challenges": 140, "verify_ok": 136, "verify_fail": 1 } ],
"quota": { "plan": "pro", "used": 4090, "limit": 500000, "cap": -1, "capped": false, "resets_at": "2026-10-01T00:00:00Z" } }
GET /api/v1/sites and POST /api/v1/sites
Header Authorization: Bearer sk_live_… with an account API key (Settings → API keys, scope sites:read or sites:write) or an OAuth token. GET lists your sites: id, name, public key, secret prefix, origins, difficulty. Difficulty is low, medium (the default) or high; on Free it is always medium, and the old value auto is accepted as medium. POST with {"name": "Bakery orders"} creates a site and answers 201 with the whole site plus secret_key, once. At the plan's site limit (2 on Free, 10 on Starter, no limit on Pro) the answer is 402 with error: quota_exceeded.
curl https://bot403.com/api/v1/sites \
-H "Authorization: Bearer sk_live_YOUR_ACCOUNT_KEY"
Account endpoints of the dashboard
The dashboard has no private backend. It calls /api/v1 with the session cookie and the X-CSRF-Token header from GET /api/v1/me: /me/keys, /me/apps, /me/sessions, POST /me/logout, and POST /billing/checkout and POST /billing/portal, which answer {"url": "…"} for the Stripe page to open. Sign-out, billing and key changes do not accept an API key.
Status codes
401: the key is missing or wrong.402: the site limit is reached (sites, Free and Starter), or verify in a capped month above the limit. A month is capped only after two calendar months over the plan in a row; otherwise going over never changes the answer. Verify answers{"ok": false, "reason": "quota"}and does not burn the token. Never fail open on a 402.429: above the per-site verify rate (100 a minute on Free, 1,000 on Starter, 3,000 on Pro) or the challenge limits.Retry-Aftersays when to try again.
Keys
Each site has one public key and one secret key. Rotate the secret key on the site's settings page; the old key answers 401 at once. Delete a site and both keys stop. Account keys are separate: they list and create sites and open the MCP server, and you revoke them in Settings.