Troubleshooting
Error codes returned from /siteverify and emitted by the widget, with the cause and the fix.
/siteverify error codes
The response shape:
{ "success": false, "error-codes": ["..."] }
| Code | Cause | Fix |
|---|---|---|
missing-input-secret |
Request body has no secret field, or it is empty |
Make sure the env var is set on the server, and the request body includes it |
invalid-input-secret |
secret value is malformed or unknown |
Verify the value starts with cpt_sec_ and matches what the dashboard shows. If you rotated the secret, the old one stops verifying immediately |
missing-input-response |
Request body has no response field |
Read the form field named caputchin-token and pass its value as response |
invalid-input-response |
Wrapped token is malformed (truncated, modified, or wrong shape) | Get a fresh token by re-submitting the form. Wrapped tokens are base64url-JSON — do not trim or re-encode them |
timeout-or-duplicate |
Token already redeemed or expired (10-minute TTL) | Each token verifies exactly once. If you saw this on a retry, you already have a verified verdict — return the cached result instead of re-calling /siteverify |
Widget client-side errors
The widget emits an error event with a code:
widget.addEventListener("error", (e) => console.log(e.detail.code));
| Code | Recoverable | Cause | Fix |
|---|---|---|---|
invalid-config |
no | Conflicting attributes — e.g. mode="manual" together with game or game-src |
See widget modes for which combinations are valid |
resolve-failed |
no | Marketplace game id could not be resolved at /games/:id/resolve |
Confirm the id is correct and currently indexed in the marketplace |
iframe-load-failed |
no | Sandboxed iframe failed to load the game script | Verify the game id is correct or your game-src URL is reachable. For marketplace games the SRI hash must still match — see game-distribution |
iframe-script-blocked |
no | CSP inside the iframe blocked the game script | Marketplace games include the canonical CSP; check that your customer-hosted game URL passes the iframe's script-src policy |
game-not-registered |
no | The loaded game script did not call register() at top level |
The author must call register() synchronously at module load. Confirm with the game author |
game-error-relayed |
no | The game's runtime threw an error and the widget forwarded it | The relayed error carries an originalCode in event.detail — share it with the game author |
postmessage-bad-origin |
yes | An unexpected postMessage arrived from outside the iframe | Almost always benign cross-frame noise; the widget keeps running |
cap-solve-failed |
yes | Cap's proof-of-work solve() failed (network or tab suspended mid-solve) |
The widget can re-issue. Encourage the user to retry; persistent failures point at a CSP or network issue with api.caputchin.com |
cap-redeem-failed |
yes | Cap's redeem step failed | Same as cap-solve-failed — retry |
form-not-found |
no | Widget could not locate its enclosing <form> to inject the caputchin-token field |
Wrap the widget inside the <form> you want it to protect. The widget walks up the DOM looking for an ancestor <form> |
Common integration mistakes
"Token is always null on the server"
The widget injects caputchin-token only after successful completion. If your form auto-submits before completion, the field is not there. Either use mode="form-submit" (widget gates submission until done) or wait for the complete event.
"I see two tokens injected"
You mounted two <caputchin-widget> elements in the same <form>. Each one injects a caputchin-token field on completion, and the second overwrites the first. Mount only one widget per form.
"Score is null even though the user played the game"
score is reported by the game, not the platform. The invisible default has no game, so score is null. Some games choose not to emit a score — that is also null. Either way, do not gate verification on score; it is client-claimed metadata. See principles.
"Verification works locally but fails in production"
The widget script must be reachable from the deployed page. Cloudflare and other CDN-fronted sites should allow cdn.jsdelivr.net. Your CSP must allow api.caputchin.com for the runtime calls.
"Hosted-verification webhook never fires"
Hosted verification is paid-tier only. Confirm the site is on the paid plan and the webhook is enabled in the dashboard. The platform retries failed deliveries with exponential backoff; check the dashboard delivery log.
Reporting a bug
If a verdict is wrong (verified humans rejected, or invalid tokens accepted), include:
- The verdict response body (success + error-codes)
- The wrapped token that was submitted (it is safe to share — it is single-use and already consumed)
- A timestamp accurate to the minute
- Whether the issue reproduces or is intermittent