Docs / Server verify / PHP
Verify Playtcha from PHP
This page is the server-side contract for PHP apps. Your backend receives the client-submitted result token, posts it to /v1/verify with your secret key, and only then continues the request.
Minimal example
The minimal correct contract is still { secret, token } over JSON with a bounded timeout.
Production-ready example
Log the upstream error envelope server-side, and remember that degraded still means the user verified successfully.
Common gotchas
- Do not send your secret key to the browser or render it into templates.
- Fail open on 5xx and network errors; fail closed on 4xx. Opt into PLAYTCHA_FAIL_CLOSED=1 for high-fraud paths.
- Treat degraded=true as success and log it as a quota-state event, not a failed human check.
- 409 already_redeemed is single-use-by-design. In production it means replay, stuck retry, or duplicate POST. Log with request_id at warn level — do not retry.
- 429 rate_limited_ip means per-IP throttle. Honor the Retry-After header; do not retry tighter than 1s.
- The wire format has no success: false field on errors. The envelope is { error: { code, message, request_id, ... } }.
- Return a generic verification error to the browser; keep the upstream error envelope in server logs.
Relevant failure codes
| Status | Code | Meaning |
|---|---|---|
| 401 | token_invalid | Bad signature or claims. Often wrong env (live vs test). |
| 401 | bad_secret | Secret does not match the project. Config error. |
| 404 | unknown_project | Project deleted or never existed. |
| 409 | already_redeemed | Security signal. Single-use by design. Log with request_id at warn level. |
| 410 | token_expired | Result token TTL elapsed (5 minutes). |
| 429 | rate_limited_ip | Per-IP throttle. Response has Retry-After header. |
| 500 | db_error | Playtcha-side. Fail open. |
Where to go next
Pair this with token lifecycle for the token model, or domain whitelist if your issue happens before verify on the public widget endpoints.