Docs / Server verify / Node
Verify Playtcha from Node
This page is the server-side contract for Node 18+ apps. Your server receives a client-submitted result token, posts it to /v1/verify with your secret key, and only then decides whether to continue the request.
Minimal example
This is the shortest correct shape: send { secret, token }, parse the response, and reject the request unless success is true.
Production-ready example
In production, add a timeout, preserve the upstream error envelope in logs, and remember that degraded still means the verification passed.
Response shape that matters
On 2xx, the body has success: true as a real wire field plus the optional fields below. On 4xx/5xx, the body is the error envelope { error: { code, message, request_id, hint?, host? } } only — there is no success: false field on the wire.
| Field | Meaning |
|---|---|
success | Whether the user is verified. Present and true on 2xx. Not present on errors. |
challenge_ts | ISO 8601 timestamp for the verified row. |
hostname | The origin domain bound to the verification. |
degraded + degraded_reason | Usage-band signal only. Treat the request as verified; log the reason. |
action / score | Optional, echoed from the challenge if present. |
error.code | Present on non-2xx failures inside the error envelope. |
Common gotchas
- Do not call /v1/verify from the browser. It belongs on the server because it requires your secret key.
- Fail open on 5xx and network errors; fail closed on 4xx. A Playtcha outage should not break your form. Opt into PLAYTCHA_FAIL_CLOSED=1 for high-fraud paths.
- Treat degraded=true as success. It means the user is verified and your project has crossed a monthly usage boundary; it is not a block signal.
- 409 already_redeemed is single-use-by-design. In production it means replay attack, 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 error envelope is { error: { code, message, request_id, ... } }. Any success: false in helper code is a local discriminator.
- Log upstream error.code, request_id, and hint server-side, but do not echo upstream details straight back to the browser.
- Use a timeout. Verification should be a small dependency, not an unbounded request in your auth flow.
Relevant failure codes
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_json / invalid_body | Bad request body. Bug on your side. |
| 401 | token_invalid | Bad signature or malformed token. Often wrong env (live vs test) or tamper. |
| 401 | bad_secret | The secret did not match the project. Config error. |
| 404 | unknown_project | Project no longer exists or was deleted. |
| 409 | already_redeemed | Security signal. Single-use by design — a 409 in production means replay, stuck retry, or duplicate POST. Log with request_id at warn level. |
| 410 | token_expired | Result token TTL elapsed (5 minutes from issue). |
| 429 | rate_limited_ip | Per-IP throttle hit. Response includes a Retry-After header — honor it; do not retry tighter than 1s. |
| 500 | db_error | Playtcha-side. Fail open. |
Token shapes and TTLs are documented in token lifecycle.
Where to go next
If you are integrating with App Router forms, pair this with the Next.js guide. If you are moving off Google, read the reCAPTCHA migration guide. For the deeper security contract, read token lifecycle. If Node is not your stack, use the Python, PHP, Go, or Ruby references.