Overview Docs API keys Swiftaw
📘 Integration guide · Lifecheck v1.2

Add Lifecheck to your site.

Lifecheck is a drop-in human check. The widget runs in a sandboxed frame served from Swiftaw, hands the browser a one-time token on success, and you confirm that token on your server before trusting the request. Two lines to install, one call to verify.

Quickstart

The whole flow, top to bottom:

<!-- 1 · load Lifecheck once (in your <head> or before </body>) -->
<script src="https://swiftaw.com/lifecheck/lifecheck.js" async defer></script>

<!-- 2 · put the widget inside the form you want to protect -->
<form method="POST" action="/signup">
  <input name="email" type="email">

  <div class="lifecheck" data-sitekey="YOUR_SITE_KEY"></div>

  <button type="submit">Create account</button>
</form>

Lifecheck auto-renders the .lifecheck element, sizes its own frame, and injects a hidden <input name="lifecheck-token"> so the token rides along with a normal form POST. Then verify it on your server. That's the entire integration.

Just want to see it? The overview page embeds the real widget as a live demo, the same code you're pasting here.

Site & secret keys

Lifecheck uses a key pair, one public and one private:

KeyWhere it livesWhat it does
Site keyIn your HTML, on the widgetPublic. Renders the widget and scopes it to your registered domain(s).
Secret keyOn your server onlyPrivate. Signs the call that verifies a token. Never ship it to the browser.

Register your domain with Swiftaw to receive a pair. Keys look like lc_site_xxx and lc_secret_xxx.

Treat the secret key like a password. If it leaks, rotate it. Anyone who has it can forge verifications.

1 Add the widget

Load the script once per page, then place a container. You can configure it entirely with data- attributes:

<div class="lifecheck"
     data-sitekey="YOUR_SITE_KEY"
     data-callback="onLifecheckPass"
     data-expired-callback="onLifecheckExpired"></div>

See every attribute in the data-attribute reference. Prefer to render manually, say inside a modal that opens later? Skip the auto-render and call the API yourself:

<div id="check"></div>

<script>
  Lifecheck.render("check", {
    sitekey: "YOUR_SITE_KEY",
    callback: function(token){ console.log("passed:", token); }
  });
</script>
Data & consent. Lifecheck records how visitors interact with the check and its mini-games (including robotic or suspicious signals) to run, improve and train Swiftaw's systems and AI. The widget shows this consent inline and links our Terms, Privacy and Products policies — no extra notice is required on your side.

2 Read the token

On a successful check, Lifecheck gives you the token three ways. Use whichever fits:

  • Hidden field. A <input name="lifecheck-token"> is added inside the container, so a plain form POST already carries it.
  • Callback. Your data-callback function is invoked as callback(token, widget).
  • DOM event. The container emits a bubbling lifecheck:verified event with detail.token.

Or pull it on demand:

const token = Lifecheck.getResponse();   // "" until verified

// e.g. gate an AJAX submit
form.addEventListener("submit", async (e) => {
  const t = Lifecheck.getResponse();
  if (!t) { e.preventDefault(); alert("Please complete the check."); }
});
A token is single-use and expires ~2 minutes after it's minted. Call Lifecheck.reset() to hand the user a fresh challenge (e.g. after a failed submit).

3 Verify on your server

The client token proves nothing on its own. Always confirm it from your backend (never browser JS, or you'll leak your secret and hit CORS). Verification is a single call to Lifecheck's function endpoint with your secret key and the token:

POST https://mwszvynzzugbowdngzab.supabase.co/rest/v1/rpc/lifecheck_verify_token Content-Type: application/json
cURL
Node.js
PHP
curl -X POST \
  "https://mwszvynzzugbowdngzab.supabase.co/rest/v1/rpc/lifecheck_verify_token" \
  -H "apikey: LIFECHECK_PUBLIC_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "p_secret": "YOUR_SECRET_KEY", "p_token": "THE_TOKEN_FROM_THE_BROWSER" }'
// server-side only — keep YOUR_SECRET_KEY out of the browser
const res = await fetch(
  "https://mwszvynzzugbowdngzab.supabase.co/rest/v1/rpc/lifecheck_verify_token", {
  method: "POST",
  headers: {
    "apikey": "LIFECHECK_PUBLIC_KEY",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    p_secret: process.env.LIFECHECK_SECRET,
    p_token: req.body["lifecheck-token"]
  })
});
const data = await res.json();
if (!data.success) return res.status(400).send("Failed Lifecheck");
$r = file_get_contents(
  "https://mwszvynzzugbowdngzab.supabase.co/rest/v1/rpc/lifecheck_verify_token", false,
  stream_context_create(["http" => [
    "method"  => "POST",
    "header"  => "apikey: LIFECHECK_PUBLIC_KEY\r\nContent-Type: application/json",
    "content" => json_encode([
      "p_secret" => $SECRET,
      "p_token"  => $_POST["lifecheck-token"]
    ])
  ]]));
$ok = json_decode($r)->success;

Response

{
  "success":      true,
  "score":        0.9,
  "passed":       "signals",         // "signals" | "challenge"
  "challenge_ts": "2026-07-31T18:04:11Z",
  "hostname":     "yoursite.com",
  "v":            "1.2",
  "error-codes":  []
}
Gate your action on success === true. apikey is the public Lifecheck key (safe to ship); p_secret is your private secret and must stay server-side. Tokens are single-use and expire ~2 minutes after issue.
Keys are checked live, so managing them takes effect right away — remove a key and it stops working immediately, no redeploy needed.

No backend? Verify in the browser safely

A direct browser call to the verify RPC fails CORS on purpose — it stops you shipping your secret to the client. For a fully static site, deploy the lifecheck-verify Edge Function (in supabase/functions/): the browser sends only the public site key + token, and the function looks up the secret server-side and returns a CORS-enabled verdict. Your secret never leaves Supabase. Try it live on the token verifier.

// deploy once:  supabase functions deploy lifecheck-verify --no-verify-jwt
const r = await fetch(
  "https://mwszvynzzugbowdngzab.supabase.co/functions/v1/lifecheck-verify", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ sitekey: "lc_site_...", token: Lifecheck.getResponse() })
});
const verdict = await r.json();   // { success:true, passed:"challenge", ... }
Browser verification trades a little strictness for convenience: it takes the public key, so it's best for low-risk gates. For anything sensitive, verify server-side with the secret as shown above.

Data-attribute reference

AttributeDescription
class="lifecheck"requiredMarks the element for auto-render. (Or data-lifecheck.)
data-sitekeyrequiredYour public site key.
data-callbackoptionalName of a global function called as fn(token, widget) on pass. Dotted paths like app.onPass work.
data-expired-callbackoptionalGlobal function called when a token expires.
data-response-fieldoptionalRenames the hidden input (default lifecheck-token).

JavaScript API

Everything hangs off the global Lifecheck object.

MethodReturnsDescription
Lifecheck.render(el, opts?)idRenders a widget into el (element or id). opts: sitekey, callback, expired-callback. Returns a numeric widget id.
Lifecheck.getResponse(ref?)stringCurrent token, or "" if not yet verified. ref = id, element, or container id; defaults to the first widget.
Lifecheck.reset(ref?)voidClears the token and loads a fresh challenge.
Lifecheck.versionstringLoader version, e.g. "1.2".

Verify endpoint

POST /rest/v1/rpc/lifecheck_verify_token (JSON body) request parameters:

ParamDescription
p_secretrequiredYour secret key (server-side only).
p_tokenrequiredThe token returned by the widget in the browser.

Send the public Lifecheck key in the apikey header. The endpoint accepts cross-origin requests, but call it from your backend so p_secret never reaches a browser.

Response fields:

FieldTypeMeaning
successbooleanWhether the token is valid and unused. Gate on this.
scorenumber0.0–1.0 confidence the visitor is human.
passedstring"signals" (passed on behaviour) or "challenge" (solved a task).
challenge_tsstringISO-8601 timestamp of the check.
hostnamestringDomain the check was solved on.
vstringLifecheck version ("1.2").
error-codesarrayPresent when success is false. See below.

Error codes

CodeMeaning
missing-input-secretThe p_secret parameter was not sent.
invalid-input-secretThe secret key is unknown (or its key was deleted).
missing-input-tokenThe p_token parameter was not sent.
invalid-input-tokenNo such token for this key.
timeout-or-duplicateThe token has expired or was already verified once.

Frame events (advanced)

Under the hood the sandboxed widget talks to the loader over postMessage. You normally never touch this (the loader turns it into the callbacks above), but for custom hosts, messages are shaped:

{ source: "swiftaw-lifecheck", v: "1.2", event: "verified", token: "LC1.2_…" }
{ source: "swiftaw-lifecheck", v: "1.2", event: "resize",   height: 74 }
{ source: "swiftaw-lifecheck", v: "1.2", event: "ready" }
Always validate event.origin against Lifecheck's origin before trusting a message. The bundled loader does this for you.

Anti-theft model

People ask how they can integrate Lifecheck without being able to just copy it. Here's the honest answer:

  • The detection logic and the five challenges live in embed.html, served only from swiftaw.com and loaded inside a sandboxed <iframe>. Integrators embed a URL, not a re-hostable copy of the checks.
  • Tokens are minted server-side. When a visitor passes, the widget asks Swiftaw to issue a token, which only happens if your site key still exists and the host is on its allow-list. A copied widget with no valid key gets nothing back.
  • Live revocation. Delete a key and token issuance stops that instant, and the secret-side verify can no longer find the key. Pages already open can't keep verifying — no reload required.
  • The token is single-use and useless until your secret key verifies it. A stolen widget shell can't produce tokens that pass your server check.
Like any browser check, a determined bot can still drive the widget itself — the honest guarantee is the pairing of live key validation and server-side secret verification (step 3), not client-side friction. Never skip step 3.

Changelog

v1.2current
  • New mini-games. Pick the life form, spot the odd one out, and count the life forms — drawn with Twemoji so they look the same everywhere.
  • Refreshed widget. The LifeCheck wordmark, a wider card, and smoother motion.
  • Interaction insights that help the check keep getting better.
  • Hardened verification and behind-the-scenes tuning.
v1.1previous
  • Per-site API keys, server-issued tokens, and a verify endpoint.
  • Self-sizing frame via resize messages, so the box never clips.
  • Hidden-field auto-injection for zero-JS form submits.
  • lifecheck:verified DOM event + dotted-path callbacks.
v1.0initial
  • Cursor-behaviour detection (distance, jitter, reaction time) with touch fallback.
  • Themed challenges + the “I'm not a robot” checkbox widget.

Ready to wire it up? Jump back to the quickstart or see the overview.