API reference
All bodies are JSON, at most 64 KiB. Unknown fields are rejected (400 INVALID_BODY). Every response carries an X-Request-ID header, repeated in error bodies
Headers and credentials
| Header | Where | Notes |
|---|---|---|
Authorization: Bearer dck_… | Backend | Backend credential with the required scope; never in a browser |
X-DigiCaptcha-Site-Key | Browser | Public site key; not a secret |
X-DigiCaptcha-Session-Token | Browser | The session's completion_token, for challenge, redeem and complete |
Idempotency-Key | Everywhere | Required for intent/session creation, complete and siteverify; 16 to 128 characters from A-Z a-z 0-9 . _ : - |
Origin | Browser | Sent by the browser; must be one of the site's allowed origins |
X-CSRF-Token | Panel | For admin API changes, together with the digicaptcha_session cookie |
Public endpoints
/v1/intentsBearer · intents:createsite_id- Site ID; must be the credential's site
action- Operation name, pattern
^[a-zA-Z0-9_.:-]{1,64}$ request_hash- Pattern
^sha256:[a-f0-9]{64}$ binding_version- Only
"jcs-v1" expires_in_seconds- 30 to 600; default 300
Response 201: {intent_id, action, expires_at}
/v1/sessionsSite key · Originintent_id- Required; an intent of the same site
locale- Optional
client_capabilities- Optional; any JSON
Response 201: {session_id, intent_id, completion_token, expires_at, policy_version, challenge} The challenge payload and renderer depend on the selected engine; Cap configuration is not a universal contract for other engines Keep the session token private; an eligible replay using the same idempotency key can return the original issuance response
/v1/sessions/{id}/cap/challengeSite key · Session tokenBody {}. Returns Cap's response unchanged, with Cap's status code. Not a success
/v1/sessions/{id}/cap/redeemSite key · Session tokenBody {token, solutions} from the Cap solver. Returns Cap's response with a token. No receipt has been issued yet
The example below is for Cap Each engine has its own provider_evidence contract A timeout after upstream verify is indeterminate; never infer success or switch the issued session to another engine
/v1/sessions/{id}/completeSite key · Session tokenBody {"provider_evidence": {"token": "…"}} with the redeem token. Captchas.ir verifies it with Cap using the site secret. Response 200: {receipt, receipt_expires_at, intent_id, session_id}. Repeating with the same Idempotency-Key returns the same receipt without calling Cap again
The same request and Idempotency-Key can replay the original response within the same recovery epoch A fresh key cannot consume the receipt again Database restore invalidates artifacts from the earlier epoch
/v1/siteverifyBearer · receipts:verifyBody {receipt, intent_id, action, request_hash}. Response 200: {success: true, verification_id, intent_id, site_id, action, request_hash, verified_at}. A consumed receipt gets 409 ALREADY_CONSUMED, another site's receipt 403 BINDING_MISMATCH, an expired one 410 SESSION_EXPIRED
/v1/stats?days=30Bearer · dcs_…Read-only stats for one site, for an outside dashboard or script. Issue the token on the site page under “Protection and stats access”; it is shown once and each site has one active token. Response 200: {site_id, name, days, totals, series, countries}. A revoked or wrong token gets 401
Other endpoints
| Path | Notes |
|---|---|
/v1/sessions… | Preflight for registered origins; Access-Control-Max-Age: 60 |
GET /livez | Process is alive |
GET /readyz | Database reachable; stage is m1-sqlite or m1-postgres |
GET /v1/capabilities | Catalog of eight engines with implemented and readiness |
GET /widget/cap.js | Captchas.ir widget script |
GET /widget/vendor/{name} | cap.min.js, cap_wasm_bg.wasm and hashwx.wasm |
GET /playground/ | Full in-browser flow test; its calls need an admin session |
Admin API
The panel uses these routes. Authentication is the digicaptcha_session cookie, and every change needs an X-CSRF-Token header equal to the digicaptcha_csrf cookie
| Path | Purpose |
|---|---|
POST /admin/v1/session/login | Sign in with {username, password} |
POST /admin/v1/session/mfa | Complete the second factor with {method, code} |
POST /admin/v1/session/mfa/email | Send an email code during sign-in |
POST /admin/v1/session/bootstrap | First sign-in with {token} |
GET /admin/v1/session | Current session |
POST /admin/v1/session/logout | Sign out |
GET /admin/v1/monitor | Monitor page data |
GET /admin/v1/security/mfa | Second-factor state; …/mfa/totp and …/mfa/email routes enable and change it |
GET /admin/v1/sites | List sites with cursor and page_size |
POST /admin/v1/sites | New site with {name, environment, allowed_origins} and an Idempotency-Key |
GET /admin/v1/sites/{id} | Site detail with keys, credentials and instances |
POST /admin/v1/sites/{id}/public-keys | New site key |
POST /admin/v1/sites/{id}/credentials | Backend credential with {scopes} |
POST /admin/v1/credentials/{id}/rotate | Rotate a credential |
POST /admin/v1/sites/{id}/cap-instances | Connect Cap with {name, endpoint, admin_key, hashwx_difficulty} |
POST /admin/v1/cap-instances/{id}/rotate-secret | Rotate the site secret in Cap |
GET /admin/v1/providers | Engine catalog |
PUT /admin/v1/public-keys/{id} | Label a public key with {label} |
POST /admin/v1/public-keys/{id}/revoke | Revoke one public key |
PUT /admin/v1/credentials/{id} | Label and scopes of a server key with {label, scopes} |
POST /admin/v1/credentials/{id}/revoke | Revoke one server key |
DELETE /admin/v1/sites/{id} | Delete a site with {confirm_name}, which must equal the site name |
POST /admin/v1/security/mfa/recovery | Ten new recovery codes with {password}, each usable once |
PUT /admin/v1/sites/{id} | Edit {name, environment, allowed_origins, status, revision}; status is active or disabled and a stale revision answers 409 |
GET /admin/v1/stats/overview?days=30 | Daily series, today and totals for every site, 1 to 366 days |
GET /admin/v1/stats/sites | Today per site |
GET /admin/v1/sites/{id}/stats?days=30 | Daily series for one site |
POST /admin/v1/session/password | Change the password with {current_password, new_password}; other sessions are signed out |
GET /admin/v1/version | Build revision, Go version, schema and Cap version |
GET /healthz | Same answer as /readyz |
GET /admin/v1/events/traffic | Traffic events filtered by site, kind, country, ip, with cursor and limit. Kinds: session.issued, cap.solved, cap.failed, cap.indeterminate, verify.ok, verify.rejected, blocked.rate |
GET /admin/v1/events/audit | Audit trail with a prefix such as cap. and cursor |
GET /admin/v1/stats/countries?days=30 | Counts by country, optional site |
GET /admin/v1/stats/hourly?hours=24 | Hourly series, optional site |
GET /admin/v1/stats/addresses | Busiest addresses, as stored by the IP mode |
GET PUT /admin/v1/settings | {ip_mode, retention_days, login_captcha_site}; ip_mode is masked, full or off, retention 1 to 90 days. The sign-in captcha site needs an active public key and Cap instance, otherwise 409 |
GET POST /admin/v1/mcp-tokens | List and create MCP tokens with {name}, at most twenty active; DELETE /admin/v1/mcp-tokens/{id} revokes one |
PUT /admin/v1/sites/{id}/settings | {rate_limit_per_min} from 0 to 100,000; zero means no cap |
POST DELETE /admin/v1/sites/{id}/stats-token | Issue or revoke the site stats token |
PUT /admin/v1/cap-instances/{id}/config | {hashwx_difficulty, obfuscation_level}; difficulty 50,000 to 5,000,000 and obfuscation 1 to 10. Applied on Cap first, then recorded |
GET /admin/v1/session/login-captcha | Whether the sign-in captcha is on. The /login-captcha frame opens the sign-in intent with POST on the same path, and the receipt goes in the captcha field of the sign-in body |
Errors
Every error has the same shape:
{
"error": {
"code": "ALREADY_CONSUMED",
"message": "This receipt was already consumed.",
"request_id": "5f0c2a9e8b7d4c3e1a2b3c4d5e6f7a8b",
"retryable": false
}
}
| HTTP | code | Meaning |
|---|---|---|
| 400 | INVALID_BODY · INVALID_REQUEST | Bad JSON, unknown field or failed validation, including a missing Idempotency-Key |
| 401 | UNAUTHENTICATED | Credential, key or session missing or invalid |
| 403 | FORBIDDEN | Missing scope, disallowed origin or wrong CSRF token |
| 403 | BINDING_MISMATCH | Receipt or intent belongs to another site, action or installation |
| 404 | NOT_FOUND | Not found in this tenant |
| 409 | ALREADY_CONSUMED | The receipt was already consumed |
| 409 | IDEMPOTENCY_CONFLICT | Same key with a different body |
| 409 | OPERATION_IN_PROGRESS | The same operation is still running; retryable: true |
| 409 | CONFLICT | The resource is not in a state that accepts this operation |
| 410 | SESSION_EXPIRED | Intent, session or receipt has expired |
| 413 | BODY_TOO_LARGE | Body over 64 KiB |
| 400 | CAPTCHA_REQUIRED | The panel sign-in captcha is on and its receipt is missing or was not accepted |
| 422 | INVALID_PROOF | Cap rejected the token, or it belongs to another Cap site |
| 502 | UPSTREAM_REJECTED | Cap rejected an admin request |
| 503 | UPSTREAM_UNAVAILABLE | Cap unreachable for challenge or redeem; retryable: true |
| 503 | UPSTREAM_RESULT_UNKNOWN | Verification outcome unknown; no receipt issued, do not retry with the same proof |
| 503 | NO_ELIGIBLE_PROVIDER | No eligible active engine is available for the site |
| 503 | STORAGE_UNAVAILABLE | Database unavailable; retryable: true |
| 429 | RATE_LIMITED | Too many sign-in or browser session calls from one address, or the site's per-IP session cap is full; wait for Retry-After seconds |
| 503 | MAIL_NOT_CONFIGURED | SMTP is not configured |
| 503 | ADAPTERS_NOT_IMPLEMENTED | Foundation mode, no database |
| 500 | INTERNAL_ERROR | Internal error; the operation failed closed |
Idempotency
- Generate one random key per logical operation and send the same key when retrying that operation
- The same key with the same body returns the stored response. That is not a second consumption and creates no new success
- The same key with a different body gets
409 IDEMPOTENCY_CONFLICT - If the earlier operation is still running you get
409 OPERATION_IN_PROGRESS - After
UPSTREAM_RESULT_UNKNOWNdo not retry blindly. Start a new intent and session
Engine protocol routes
This inventory follows the registered routes in this version A route does not prove configuration, entitlement or a successful engine test in your environment The public key, Origin and issued session token must match the same site and pinned engine
| Method | Route |
|---|---|
| POST | /v1/sessions/{id}/cap/challenge |
| POST | /v1/sessions/{id}/cap/redeem |
| POST | /v1/sessions/{id}/wicketkeeper/challenge |
| GET | /v1/sessions/{id}/fcaptcha/api/pow/challenge |
| POST | /v1/sessions/{id}/fcaptcha/api/verify |
| POST | /v1/sessions/{id}/gocaptcha/data |
| POST | /v1/sessions/{id}/gocaptcha/check |
| POST | /v1/sessions/{id}/digitalcaptcha/challenge |
| POST | /v1/sessions/{id}/digitalcaptcha/check |
| POST | /v1/sessions/{id}/captchapro/data |
| POST | /v1/sessions/{id}/captchapro/verify |
| POST | /v1/sessions/{id}/iconcaptcha |
| POST | /v1/sessions/{id}/mosparo/api/v1/frontend/{action} |
| POST | /v1/sessions/{id}/mcaptcha/config |
| POST | /v1/sessions/{id}/mcaptcha/verify |
| POST | /v1/sessions/{id}/mcaptcha/frame |
| POST | /v1/sessions/{id}/altcha/challenge |
| POST | /v1/sessions/{id}/altcha/verify |
Download the API contract
Registered JSON API routes are extracted from this source version and fixture requests and responses are checked against the contract Provider passthrough payloads can remain open schemas HTML, demos and static assets are outside this contract