Server-side verification
By adding the client side code, you were able to render a Procaptcha widget that identified if users were real people or automated bots. When the captcha succeeded, the Procaptcha script inserted unique data into your form data, which is then sent to your server for verification. The are currently two options for verifying the user’s response server side:
API Verification
Section titled API VerificationTo verify that the token is indeed real and valid, you must now verify it at the API endpoint:
https://api.prosopo.io/siteverify
EU-only and US-only endpoints are also available if you prefer your data to be processed in a specific region:
The endpoint expects a POST request with the procaptcha-response token. You must also pass your secret key, which you can obtain by logging in to our customer portal.
A simple test will look like this, where the contents in data is the procaptcha-response token, after being
parsed:
// pseudocode// get the contents of the procaptcha-response tokendata = req.body['procaptcha-response']
// send a POST application/json request to the API endpointresponse = POST('https://api.prosopo.io/siteverify', { token: data.token, secret: 'your_secret_key',})Or, as a CURL command:
curl --location 'https://api.prosopo.io/siteverify' \--header 'Content-Type: application/json' \--data '{"secret":"your secret key copied from within the customer portal","token":"PROCAPTCHA-RESPONSE"}''Note that the endpoint expects the application/json Content-Type. You can see exactly what is sent using
curl -vvin the example above. The response will be a JSON object with a verified key, which will be true if the token is valid and false if it is not.
{ "verified": true,}If you are a Premium Tier customer then you will also receive a risk score associated with the request. The closer the score is to 1, the more likely it is that the request is from a bot.
{ "verified": true, "score": 0.1}Optional Fields
Section titled Optional FieldsIP Address
Section titled IP AddressProfessional and Enterprise Tier can pass the user’s IP address to the verification endpoint, which will allow Prosopo to perform additional checks
on the request. This is optional, but recommended for better accuracy. To do this, include the ip field in your POST data:
{ "secret": "your_secret_key", "token": "PROCAPTCHA-RESPONSE", "ip": "USER_IP_ADDRESS"}Email address
Section titled Email addressProfessional and Enterprise Tier can pass email to the verification endpoint, which will allow Prosopo to filter spam and temporary email addresses. This is optional, but recommended if you are receiving a lot of spam from temporary email domains. To do this, include the email field in your POST data:
{ "secret": "your_secret_key", "token": "PROCAPTCHA-RESPONSE", "email": "USER_EMAIL_DOMAIN"}Note: You do not need to include the full email address, just the domain is sufficient for Prosopo to perform the necessary checks. For example, if the user’s email is
[email protected]then you can passexample.comas the value for the
Verification Code Examples
Section titled Verification Code ExamplesJavaScript
Section titled JavaScriptconst fetch = require('node-fetch');
async function verifyToken(token) {const response = await fetch('https://api.prosopo.io/siteverify', {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({secret: 'your_secret_key', token}),});return response.json().verified || false; // Return verified field, default to false}<?phpfunction verifyToken($token) { $url = 'https://api.prosopo.io/siteverify'; $data = json_encode(["secret" => "your_secret_key", "token" => $token]); $options = [ 'http' => [ 'header' => "Content-Type: application/json\r\n", 'method' => 'POST', 'content' => $data, ], ]; $context = stream_context_create($options); $result = file_get_contents($url, false, $context); $response = json_decode($result, true);
return $response["verified"] ?? false; // Return verified field, default to false}?>Python
Section titled Pythonimport requests
def verify_token(token):url = "https://api.prosopo.io/siteverify"data = {"secret": "your_secret_key", "token": token}response = requests.post(url, json=data)return response.json().get("verified", False) # Return verified field, default to Falseimport java.io.*;import java.net.*;import org.json.JSONObject;
public class ProcaptchaVerification {public static boolean verifyToken(String token) throws Exception {URL url = new URL("https://api.prosopo.io/siteverify");HttpURLConnection con = (HttpURLConnection) url.openConnection();con.setRequestMethod("POST");con.setRequestProperty("Content-Type", "application/json");con.setDoOutput(true);
String jsonInputString = "{\"secret\":\"your_secret_key\", \"token\":\"" + token + "\"}";try (OutputStream os = con.getOutputStream()) {byte[] input = jsonInputString.getBytes("utf-8");os.write(input, 0, input.length);}
BufferedReader br = new BufferedReader(new InputStreamReader(con.getInputStream(), "utf-8"));StringBuilder response = new StringBuilder();String responseLine;while ((responseLine = br.readLine()) != null) {response.append(responseLine.trim());}
// Parse JSON responseJSONObject jsonResponse = new JSONObject(response.toString());return jsonResponse.optBoolean("verified", false); // Default to false if not found}}Verification Package
Section titled Verification PackageWe have a JavaScript implementation of the Procaptcha verification package available on npm.
JavaScript / TypeScript Verification
Section titled JavaScript / TypeScript VerificationThe @prosopo/server package is available on NPM and can be installed using:
npm install @prosopo/serverTo verify a user’s response using JavaScript / TypeScript, simpy import the verify function from @prosopo/server and pass it
the procaptcha-response POST data. Types can be imported from @prosopo/types.
import {ProsopoServer} from '@prosopo/server'import {ApiParams} from '@prosopo/types'
...// parse the body received from the frontendconst payload = JSON.parse(event.body)
// parse the procaptcha response tokenconst procaptchaResponse = payload[ApiParams.procaptchaResponse]
// initialise the `ProsopoServer` classconst prosopoServer = new ProsopoServer(config)
// check if the captcha response is verifiedif (await prosopoServer.isVerified(procaptchaResponse)) { // perform CAPTCHA protected action}There is an example TypeScript server NodeJS Server Side Example that you run locally.
Error Responses
Section titled Error ResponsesAll /siteverify responses use a uniform JSON envelope. For programmatic handling, branch on error.key (stable, machine-readable) rather than error.message (human-readable and localised).
Response envelope
Section titled Response envelopeA successful response:
{ "status": "ok", "verified": true, "score": 0.1, "reason": "optional"}An error response:
{ "error": { "code": 400, "key": "API.INVALID_SITE_KEY", "message": "Invalid site key", "data": {} }}HTTP 200 — Success and soft failures
Section titled HTTP 200 — Success and soft failuresA 200 response does not mean the user passed the challenge. Always inspect the verified field.
| Scenario | Body | Notes |
|---|---|---|
| Token verified | { "status": "ok", "verified": true, "score": ..., "reason"?: ... } | Normal happy path. score is included for Premium-tier customers. |
| Token cannot be decoded | { "status": "ok", "verified": false, "score": 0 } | The token is malformed or forged. Treat as a failed challenge — do not retry. |
| Provider verification timed out (5s) | { "status": "ok", "verified": true, "score": 0 } | Fail-open: if our verification provider is temporarily unreachable, the request is allowed through so legitimate users are not blocked. A cluster of these responses indicates a provider incident. |
HTTP 4xx — Client / integration errors
Section titled HTTP 4xx — Client / integration errorsThese indicate a problem with the request itself. Retrying without changes will not succeed.
| HTTP | error.key | Trigger | What to fix |
|---|---|---|---|
| 400 | API.MISSING_BODY | Request body is empty or required fields are missing. | Send a POST with a JSON body containing at least secret and token. |
| 400 | API.PARSE_ERROR | Body fails schema validation. | Check field names and types: token (string), secret (string), and optional ip, email, timeouts. |
| 400 | API.INVALID_SITE_KEY | The secret does not correspond to the site key embedded in the token. | Confirm the secret key for the site matches the one rendering the widget. A common cause is using a secret from a different site. |
| 400 | API.UNKNOWN_ERROR (or a more specific key) | The verification provider returned an error block for this token. | Inspect error.key and error.message for the specific provider-side reason. See Forwarded provider errors below. |
HTTP 5xx — Server / upstream errors
Section titled HTTP 5xx — Server / upstream errorsThese indicate a transient or upstream problem. Retrying with backoff is appropriate.
| HTTP | error.key | Trigger |
|---|---|---|
| 500 | GENERAL.MISSING_SECRET_KEY | The secret field is present but invalid as a keypair. |
| forwarded | varies | The verification provider rejected the request with a ProsopoApiError. The HTTP code and error.key are propagated from the underlying error. |
| forwarded | API.UNKNOWN (default) | Any uncaught exception inside the handler. Wrapped responses default to HTTP 400 with key: "API.UNKNOWN" when the underlying error is not a ProsopoApiError. |
Forwarded provider errors
Section titled Forwarded provider errorsWhen the verification provider returns an error, the original error.key is forwarded. The keys you are most likely to see in production:
error.key | Meaning |
|---|---|
API.SITE_KEY_NOT_REGISTERED | The site key is well-formed but not registered with the provider. Re-check the site key in the customer portal. |
API.PROVIDER_VERIFY_FAILED | The provider reached a verification decision of “failed” for reasons other than a bad request (e.g. internal state mismatch). |
API.UNKNOWN_ERROR | The provider returned an error block without a recognised key. Inspect error.message and contact support if it persists. |
Other API.* and GENERAL.* keys may appear depending on provider state.
Recommended client handling
Section titled Recommended client handling- Check
verifiedfirst. A200withverified: falseis the normal “challenge failed” path, not an exception. - Retry only on 5xx. 4xx responses indicate a configuration or integration error and will not succeed on retry.
- Log
error.key, noterror.message. Messages are localised and may change wording; keys are stable identifiers. - Treat timeout fail-open as a signal. Repeated
200 { "verified": true, "score": 0 }responses likely indicate a provider incident. Alert on this rather than counting them as successful verifications.