DeSpoof
Integration guide

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 2. Paste the tag 3. Handle the result in the browser 4. Confirm it on your server A worked example Reasons and errors Permissions policy What is stored

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

AttributeWhat it does
data-despoof="verify"Turns the element into a trigger. The click is held until the check finishes.
data-despoof-refYour 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-callbackOverrides 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

ReasonWhat happened
camera_deniedThey refused camera permission, or the browser blocked it.
no_cameraThe device has no camera.
action_not_seenThe prompt was not followed clearly enough for the strictness you set.
face_not_heldTheir face left the frame during the prompt.
no_natural_movementThe camera view was too still to be a live one. A held-up photo lands here.
too_fast / too_slowThe response did not fit a human reaction window.
low_confidenceEvery prompt was followed, but not well enough overall.
timeoutThe whole check ran past 90 seconds.
closedThey shut the frame.
cooldown / ip_hourly_limit / ip_daily_limitYour own throttle turned them away. These never count against your plan.
origin_not_allowedThe tag ran on a website that is not on your approved list.
quota_exceededThe monthly allowance is used up.

Errors from the verify call

ErrorWhat to do
bad_secretThe secret key is wrong or was replaced. Update it on your server.
unknown_codeThe code does not belong to this website.
code_already_usedCodes work once. Do not retry the same one.
code_expiredConfirm the code as soon as it reaches your server.
not_passedThe 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.