Integrate Proof-of-Life
PoL Gate follows the same pattern as reCAPTCHA, hCaptcha and Turnstile: a widget on the page, a token in the form, one server call to check it. Step-up, recovery and activity verification build on the same token and the same check.
pk_...) and secret keys (sk_...) are issued to early-access partners. Request access. Detection thresholds are still being calibrated on real devices, so field names are stable but default thresholds may change before general availability.Quick start
- Get a site key and a secret key, and tell us the hostnames the widget will run on.
- Add the widget script and element to your form. The widget adds a hidden field called
pol-token. - On your server, send that token and your secret key to
POST https://api.heliusnetwork.app/v1/siteverifyand accept the request only whensuccessistrue.
<!-- in the page --> <script src="https://cdn.heliusnetwork.app/pol/v1/gate.js" async defer></script> <form method="post" action="/signup"> <input name="email" type="email"> <div class="pol-gate" data-sitekey="pk_your_site_key"></div> <button>Create account</button> </form>
The first time, people complete a 9-second motion check on their phone (three moves, then a screen-lock confirmation). On a computer the widget shows a QR code. After that, their Verified Human Pass is reused for 30 days, usually with one screen-lock confirmation. Proof-of-Life uses no camera and no biometrics.
The two-line CAPTCHA swap
Most sites change two lines in the page, and one URL and one field name on the server.
<!-- before (reCAPTCHA v2) --> <script src="https://www.google.com/recaptcha/api.js" async defer></script> <div class="g-recaptcha" data-sitekey="..."></div> <!-- after (PoL Gate) --> <script src="https://cdn.heliusnetwork.app/pol/v1/gate.js" async defer></script> <div class="pol-gate" data-sitekey="pk_..."></div>
| reCAPTCHA | PoL Gate | |
|---|---|---|
| Form field | g-recaptcha-response | pol-token |
| Verify URL | google.com/recaptcha/api/siteverify | api.heliusnetwork.app/v1/siteverify |
| Request | secret, response, remoteip | The same |
| Success field | success | success, plus human, unique, present |
| Token reuse | Single use, about 2 minutes | Single use, 120 seconds |
hCaptcha (class="h-captcha") and Turnstile (class="cf-turnstile") swap the same way. data-callback, data-expired-callback and data-error-callback work as before, and the JavaScript API mirrors grecaptcha: PoL.render(el, opts), PoL.execute(sitekey, { action }), PoL.getResponse(id) and PoL.reset(id).
reCAPTCHA v3 returns a score for how human traffic looks. Proof-of-Life instead reports whether its Liveness Engine accepted a live, hand-held motion check, so replace score >= 0.5 with success === true. Require unique === true only where duplicates cost you money, such as sign-up bonuses, rewards and votes: a brand-new device starts below the default threshold and becomes unique with history or a co-verification.
siteverify: request and response
POST https://api.heliusnetwork.app/v1/siteverify, form-encoded or JSON, from your server only. Never put your secret key in a page or an app.
| Field | Required | Meaning |
|---|---|---|
secret | Yes | Your secret key |
response | Yes | The token from pol-token or PoL.execute |
remoteip | No | Accepted for compatibility, not stored |
expected_action | No | Fail with action-mismatch unless the token's action matches |
expected_account_hint_hash | No | Fail with account-mismatch unless the step-up was started for this account |
expected_subject | No | Fail with subject-mismatch unless the token's pairwise subject equals the one you stored at sign-up |
allow_new_device | No | Recovery only: accept a recovery by a person other than the one bound to the account, and move the binding to them |
Response
{
"success": true, "human": true, "unique": true, "present": true,
"level": "gate", "action": "signup", "hostname": "shop.example",
"challenge_ts": "2026-10-22T09:00:00.000Z", "pass_age_s": 42,
"subject": "pairwise-id-for-your-site",
"liveness_confidence": 0.95, "uniqueness_confidence": 0.8,
"uniqueness_level": "history", "error-codes": []
}
| Field | Meaning |
|---|---|
success | The token is valid, unused, unexpired, for your site, and every expected_* check passed |
human, present | The Liveness Engine accepted the motion check as a live person holding a phone (passed at 0.7 or more). It is a strong signal, not a guarantee |
unique | uniqueness_confidence is at or above your site's threshold (default 0.6) |
level | gate, stepup, recovery, liveness or activity |
liveness_confidence | 0 to 1 from the Liveness Engine at the last full check (passes at 0.7 or more) |
uniqueness_confidence | 0 to 1 from the Uniqueness Engine. A confidence score, not a guarantee |
uniqueness_level | Strongest evidence so far: device (passkey-based device anchor only), history (several days or places) or vouched (co-verified) |
subject | Pseudonymous ID, stable for the same person on your site and different on every other site |
account_binding | Step-up and recovery only: first, match or rebound |
activity | Activity tokens only: { type, km, dur_s, cf } |
Every response token is single use. Gate, step-up, recovery and liveness tokens expire after 120 seconds and activity tokens after 10 minutes. A second check of the same token returns timeout-or-duplicate. Tokens are signed, and the public keys are published at https://api.heliusnetwork.app/.well-known/jwks.json for offline verification. Anchoring a hash on Solana is optional.
Step-up with account binding
A step-up shows a live person completed the check for one exact action. After the first trusted step-up binds the account, later step-ups also check it is the same person.
- Your server starts the step-up with the action and a hash of your internal user ID. Never send an email address or user ID in clear.
- The browser runs the check with the returned
client_token. - Your server checks the token with the same action and account hash.
// 1. your server POST https://api.heliusnetwork.app/v1/stepup/start { "secret": "sk_...", "action": "transfer", "account_hint_hash": "<base64url HMAC-SHA256 of your user id>", "level": "stepup" } // returns { "session_id", "client_token", "expires_in" } // 2. the browser const token = await PoL.execute('pk_your_site_key', { action: 'transfer', stepup: client_token }); // 3. your server POST https://api.heliusnetwork.app/v1/siteverify { "secret": "sk_...", "response": token, "expected_action": "transfer", "expected_account_hint_hash": "<same hash>" }
- First. The first completed step-up or recovery for an account binds the account to that person:
account_binding: "first". Run it at a moment you trust, such as right after sign-up or a strong login. - Match. Later step-ups by the same person return
match. Another person fails the check withaccount-mismatchand no token is issued. - Rebound. A recovery by another person (for example the owner on a new phone with a new passkey) fails at siteverify with
account-mismatchunless you passallow_new_device: trueafter your own recovery checks. It then returnsreboundand moves the binding.
For the strongest guarantee, store the pairwise subject from the user's sign-up token and pass it as expected_subject on every step-up. Start errors: invalid-action, invalid-account-hint-hash, invalid-level, 403 plan-does-not-include-level and 402 plan-limit-reached. Step-up is included from the Business plan, and recovery on Enterprise.
Activity verification
The Activity Verification API checks a recorded walk, run, ride or wheelchair session. An app calls POST /v1/activity/start with its site key, records GPS points and accelerometer samples, encrypts them into an envelope with the returned key, and sends it to POST /v1/activity/verify. Your server can also call verify directly with your secret key.
// envelope contents { "activity": "walking | running | cycling | wheelchair", "points": [{ "lat", "lng", "t", "acc" }, ...], "accel": [{ "t", "x", "y", "z" }, ...], "device": { "emulator": false, "rooted": false, "devMode": false, "mockLocation": false, "vpn": false } } // response { "verified": true, "confidence": 1, "hard": [], "soft": [], "summary": { "activity": "running", "distanceKm": 5.1, "durationS": 1800, "points": 181 }, "token": "..." }
| Layer | Hard failure | Soft (lowers confidence) |
|---|---|---|
| 1 Speed | A segment faster than the activity's limit: walking 9, running 25, cycling 45, wheelchair 20 km/h | |
| 2 Teleport | More than 200 km in under 10 minutes | |
| 3 Sensor fusion | GPS moved but the accelerometer shows a still phone | |
| 4 Route entropy | Bearing entropy under 0.2, a ruler-straight replay | |
| 5 Time consistency | Distance impossible for the duration, or timestamps out of order | |
| 6 Device integrity | Emulator, mock location, rooted, developer mode, VPN, poor GPS accuracy |
verified needs no hard failure and a confidence of 0.5 or more. Activity tokens can be checked at siteverify for 10 minutes. Raw GPS and motion data is deleted within 60 seconds of the check.
Error codes
| Code | Where | Meaning |
|---|---|---|
missing-input-secret | siteverify | No secret key was sent |
invalid-input-secret | siteverify | The secret key is wrong or revoked |
missing-input-response | siteverify | No token was sent |
invalid-input-response | siteverify | The token is malformed, not signed by Helius or for another site |
timeout-or-duplicate | siteverify | The token expired or was already checked |
action-mismatch | siteverify | The token's action is not expected_action |
account-mismatch | siteverify, ceremony | The step-up or recovery was for another account, or by another person |
subject-mismatch | siteverify | The token's subject is not expected_subject |
bad-request | siteverify | The request could not be parsed |
rate-limited | any route (429) | Too many requests; retry with backoff |
invalid-sitekey | any route (400) | The site key is unknown |
hostname-not-allowed | any route (403) | The page's hostname is not registered for the site key |
plan-limit-reached | any route (402) | The monthly ceremony allowance is used up |
firewall-blocked | any route (429) | Blocked by the Cognitive Firewall after anomalous traffic |
Integration guides
Migrating from reCAPTCHA, hCaptcha or Turnstile
Page, server and JavaScript changes, and a rollout plan.
Node.js and Express
Gate on sign-up and a step-up before a transfer.
Python (Flask and Django)
A shared siteverify helper and both frameworks.
PHP
siteverify with cURL.
Okta, Auth0 and Microsoft Entra
Step-up and recovery patterns for identity providers.
Mobile apps and activity
Native passes, activity sessions and co-verification.
Questions or a pilot? Write to hashim@heliusnetwork.com. Data handling is described in the Proof-of-Life privacy notice.