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. |
| data-despoof-redirect | Sends the visitor to this address once the check passes, with the code added as ?despoof_code=. |
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.
Callback and redirect are two different things
This trips people up, so it is worth being blunt about it.
| Callback URL | Redirect | |
|---|---|---|
| What happens | The widget POSTs the code to it in the background. The visitor stays where they are. | The visitor's browser goes there, with the code in the query string. |
| Where it can point | Your own site, same origin as the page the widget is on. Anywhere else and the browser blocks it, unless that server sends the right CORS headers. | Anywhere at all, including a different domain. |
| Use it when | You want your backend to know, without moving the visitor. | You are gating entry to somewhere and want to send them through. |
If you set a callback URL on another domain you will see this in the console, and the browser will refuse the request:
Access to fetch at 'https://other-site.com/' from origin 'https://your-site.com' has been blocked by CORS policy
That means you wanted the redirect, not the callback. You can set both: the callback fires first, then the visitor is sent on.
Either way the code still has to be confirmed from a server, as below. A code sitting in a query string proves nothing on its own.
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. |
The distance prompt reports as either move_closer or move_back in the checks list, depending on which way the visitor had room to move. | |
| 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. |
| out_of_time | The whole test was not finished inside the window you set. The clock starts at the first prompt and nothing the browser sends can reset it. |
| timeout | The check ran past the absolute ceiling. |
| 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.