From nothing to a working check in about ten minutes.
There is no SDK and no API to learn. You paste a script, mark a button, and make one call from your server to find out what happened.
1. Approve your website
In the dashboard, open your website and add its address under Approved websites. The widget refuses to start anywhere else, which is what stops someone lifting your tag and spending your monthly allowance.
Write one host per line. Start a line with *. to include subdomains.
example.com *.example.com staging.example.com
2. Paste the tag
One script before the closing body tag, and one attribute on the button that starts the sensitive action.
<!-- before </body> -->
<script src="https://widget.despoof.com/widget_v1.js" data-app="dsp_live_..."></script>
<button data-despoof="verify" data-despoof-ref="signup">Continue</button>
Button attributes
| Attribute | What it does |
|---|---|
| data-despoof="verify" | Turns the element into a trigger. The click is held until the check finishes. |
| data-despoof-ref | Your own tag for this action, for example signup. It comes back when you confirm the code. |
| data-despoof-then="submit" | Submits the form the button sits in once the check passes, with the code added as despoof_code. |
| data-despoof-then="click" | Re-fires the original click once the check passes, so your own handler runs as normal. |
| data-despoof-callback | Overrides the callback URL for this button only. |
Starting a check from your own code
DeSpoof.verify({ reference: 'password_reset' }).then(function (res) {
if (res.ok) sendToServer(res.code);
});
Call DeSpoof.bind() again after you render new buttons on the page.
3. Handle the result in the browser
These events are for your interface only. Nothing here should decide whether the action is allowed.
DeSpoof.on('verified', function (e) {
// e.code, e.reference. The code is short lived, send it to your server.
});
DeSpoof.on('failed', function (e) {
showMessage(e.reason);
});
DeSpoof.on('cancelled', function () { /* they closed the frame */ });
DeSpoof.on('completed', function (e) { /* your callback URL answered */ });
4. Confirm it on your server
This is the only result worth trusting. A code works once, expires after two minutes by default, and is tied to the website that asked for it.
POST https://widget.despoof.com/verify.html
Authorization: Bearer <secret key>
Content-Type: application/json
{ "code": "dsc_..." }
A pass comes back like this:
{
"success": true,
"app": "dsp_live_8x2kq4m",
"reference": "signup",
"score": 0.94,
"strictness": "normal",
"checks": ["turn_left", "blink"],
"verified_at": "2026-08-07T04:19:52+00:00",
"duration_ms": 5140,
"origin": "https://example.com",
"ip": "203.0.113.24",
"browser": "Chrome 141",
"os": "macOS 15.3",
"device": "desktop"
}
Anything else comes back with success: false and an error. Treat every one of them as a fail.
A worked example
Your page
<form method="post" action="/signup">
<input name="email" type="email" required>
<button data-despoof="verify" data-despoof-ref="signup" data-despoof-then="submit">
Create my account
</button>
</form>
<script src="https://widget.despoof.com/widget_v1.js" data-app="dsp_live_..."></script>
Your server, in PHP
$code = $_POST['despoof_code'] ?? '';
$ch = curl_init('https://widget.despoof.com/verify.html');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DESPOOF_SECRET'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['code' => $code]),
]);
$check = json_decode(curl_exec($ch), true);
if (empty($check['success'])) {
// No person confirmed. Hold it, retry it, or refuse it.
exit('Please complete the check.');
}
createAccount($_POST['email'], [
'liveness_score' => $check['score'],
'checked_at' => $check['verified_at'],
'checked_ip' => $check['ip'],
]);
Your server, in Node
const r = await fetch('https://widget.despoof.com/verify.html', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.DESPOOF_SECRET}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ code })
});
const check = await r.json();
if (!check.success) return res.status(400).json({ error: check.error });
Reasons and errors
Why a check did not pass
| Reason | What happened |
|---|---|
| camera_denied | They refused camera permission, or the browser blocked it. |
| no_camera | The device has no camera. |
| action_not_seen | The prompt was not followed clearly enough for the strictness you set. |
| face_not_held | Their face left the frame during the prompt. |
| no_natural_movement | The camera view was too still to be a live one. A held-up photo lands here. |
| too_fast / too_slow | The response did not fit a human reaction window. |
| low_confidence | Every prompt was followed, but not well enough overall. |
| timeout | The whole check ran past 90 seconds. |
| closed | They shut the frame. |
| cooldown / ip_hourly_limit / ip_daily_limit | Your own throttle turned them away. These never count against your plan. |
| origin_not_allowed | The tag ran on a website that is not on your approved list. |
| quota_exceeded | The monthly allowance is used up. |
Errors from the verify call
| Error | What to do |
|---|---|
| bad_secret | The secret key is wrong or was replaced. Update it on your server. |
| unknown_code | The code does not belong to this website. |
| code_already_used | Codes work once. Do not retry the same one. |
| code_expired | Confirm the code as soon as it reaches your server. |
| not_passed | The check ran but did not pass. The reason field says why. |
Permissions policy
The camera opens inside a frame on our domain, so your own pages never ask for camera permission. If your site sends a Permissions-Policy header, the browser will not pass the camera into that frame unless you say so. This is the most common reason a first install does nothing.
Permissions-Policy: camera=(self "https://widget.despoof.com")
If you do not send that header at all, nothing needs changing.
What is stored
No image, no video, no face map, no thumbnail. At any point, including in your dashboard.
What is kept against each attempt is the time, the connection it came from, the browser and operating system reported by that browser, which prompts were asked, how long each took, and whether it passed. That is what you get back from the verify call, and it is all there is.
You are the one asking your visitors to use their camera, so your privacy policy needs a line about it, and the consent wording shown before the camera opens is yours to edit in the dashboard.