Skip to content

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:

To 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 token
data = req.body['procaptcha-response']
// send a POST application/json request to the API endpoint
response = POST('https://api.prosopo.io/siteverify', {
token: data.token,
secret: 'your_secret_key',
})

Or, as a CURL command:

Terminal window
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

Terminal window
curl -vv

in 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
}

Professional 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"
}

Professional 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 pass example.com as the value for the email field.

const 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
}
<?php
function 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
}
?>
import 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 False
import 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 response
JSONObject jsonResponse = new JSONObject(response.toString());
return jsonResponse.optBoolean("verified", false); // Default to false if not found
}
}

We have a JavaScript implementation of the Procaptcha verification package available on npm.

JavaScript / TypeScript Verification

Section titled JavaScript / TypeScript Verification

The @prosopo/server package is available on NPM and can be installed using:

Terminal window
npm install @prosopo/server

To 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 frontend
const payload = JSON.parse(event.body)
// parse the procaptcha response token
const procaptchaResponse = payload[ApiParams.procaptchaResponse]
// initialise the `ProsopoServer` class
const prosopoServer = new ProsopoServer(config)
// check if the captcha response is verified
if (await prosopoServer.isVerified(procaptchaResponse)) {
// perform CAPTCHA protected action
}

There is an example TypeScript server NodeJS Server Side Example that you run locally.

All /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).

A 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 failures

A 200 response does not mean the user passed the challenge. Always inspect the verified field.

ScenarioBodyNotes
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 errors

These indicate a problem with the request itself. Retrying without changes will not succeed.

HTTPerror.keyTriggerWhat to fix
400API.MISSING_BODYRequest body is empty or required fields are missing.Send a POST with a JSON body containing at least secret and token.
400API.PARSE_ERRORBody fails schema validation.Check field names and types: token (string), secret (string), and optional ip, email, timeouts.
400API.INVALID_SITE_KEYThe 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.
400API.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 errors

These indicate a transient or upstream problem. Retrying with backoff is appropriate.

HTTPerror.keyTrigger
500GENERAL.MISSING_SECRET_KEYThe secret field is present but invalid as a keypair.
forwardedvariesThe verification provider rejected the request with a ProsopoApiError. The HTTP code and error.key are propagated from the underlying error.
forwardedAPI.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.

When the verification provider returns an error, the original error.key is forwarded. The keys you are most likely to see in production:

error.keyMeaning
API.SITE_KEY_NOT_REGISTEREDThe site key is well-formed but not registered with the provider. Re-check the site key in the customer portal.
API.PROVIDER_VERIFY_FAILEDThe provider reached a verification decision of “failed” for reasons other than a bad request (e.g. internal state mismatch).
API.UNKNOWN_ERRORThe 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.

  1. Check verified first. A 200 with verified: false is the normal “challenge failed” path, not an exception.
  2. Retry only on 5xx. 4xx responses indicate a configuration or integration error and will not succeed on retry.
  3. Log error.key, not error.message. Messages are localised and may change wording; keys are stable identifiers.
  4. 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.