API Reference

Everything you need to
verify humans.

One endpoint. Behavioral signals. A confidence score. No puzzle in front of your users. See what it does not catch.

API Operationalv1Try live demo →
https://humaverify.com
⚡ 2 lines to integrate
Step 1 — Add the snippet to your HTML (enriches behavioral signals)
<script src="https://humaverify.com/huma.js"></script>
Step 2 — Call verify at any critical moment (signup, login, checkout)
const result = await Huma.verify('user_123', 'huma_live_...');
if (result.human && result.confidence > 0.8) { // ✅ allow }

The snippet silently collects behavioral signals (mouse, keyboard, scroll) and sends them with each verify call for a richer score.

All requests must include your API key in the Authorization header as a Bearer token.

Authorization: Bearer huma_live_••••••••••••••••

Get your key from your dashboard →

POST/api/v1/verifyVerify a user

Analyzes a user's behavioral signals and returns a human confidence score. Call at critical moments — signups, logins, form submissions, high-value actions.

Request body

ParameterTypeRequiredDescription
userIdstringYesYour internal user identifier. Stored with the verification record so it appears in your dashboard and so repeat activity can be scored. Send a pseudonymous id, never an email or a name.
sessionDataobjectIn practiceThe behavioral signals collected by huma.js in the browser. This is the entire input: without it there is nothing to score and the call cannot tell you anything about the visitor. Optional in the signature only so a call can still be made when the browser sent nothing. Field reference below.

Response fields

FieldTypeDescription
humanbooleanTrue when confidence is at or above your account's threshold, returned below.
confidencenumberScore from 0.0 to 1.0. Higher = more human-like behavior.
thresholdnumberThe cutoff this verdict was compared against. Returned on every call so you never have to guess which number decided it.
tokenstringUnique verification token. Store for audit purposes.
pii_storedbooleanAlways false. useHUMA never stores personal information.
agentobjectPro & Enterprise. AI-agent detection: { is_agent, agent_confidence (0–1), signals[] }. Parallel to the human score — present only when behavioral signals are sent. Omitted on lower plans.
warningsstring[]Present only when something is wrong with your payload, such as a field name we do not recognise. Read it in development. A visitor scored from a field we never read is not a bot, it is a wiring mistake, and this is where you find out.

Example

REQUEST
curl -X POST https://humaverify.com/api/v1/verify \
  -H "Authorization: Bearer huma_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "user_abc123",
    "sessionData": { /* the object from Huma.debug().features */ }
  }'
RESPONSE 200 OK
{
  "human": true,
  "confidence": 0.92,
  "threshold": 0.5,
  "token": "h_verified_Kd3mP8luYMC2Mi1x",
  "pii_stored": false,
  "agent": {                       // Pro & Enterprise
    "is_agent": false,
    "agent_confidence": 0,
    "signals": []
  }
}

Session data

These are the fields sessionData is read from. You do not build this object yourself: huma.js produces it in the browser and you forward it untouched. The list is here because you should be able to see exactly what leaves your users' browsers, and because an integration that rebuilds the object by hand needs to know the names.

IN THE BROWSER
<script src="https://humaverify.com/huma.js"></script>
<script>
  // Send this to your own backend, unchanged
  const sessionData = window.Huma.debug().features;
</script>

Fields

FieldTypeWhat it is
time_on_page_msnumberMilliseconds since the collector started.
mouse_sample_countnumberPointer positions sampled.
mouse_speed_cvnumberVariation in pointer speed.
mouse_direction_changesnumberTimes the pointer changed direction.
key_countnumberIntervals between keystrokes, not keystrokes. Four characters gives three.
key_interval_cvnumberVariation in typing rhythm.
scroll_countnumberScroll events.
scroll_interval_cvnumberVariation in scroll timing.
click_countnumberClicks.
click_interval_cvnumberVariation in click timing.
tab_switchesnumberTimes the page lost or regained focus.
tap_countnumberTouch taps. Mobile.
touch_move_countnumberTouch drags. Mobile.
agentobjectAutomation tells. Forward this nested object untouched. An integration that keeps only the numbers drops it, and automation detection silently stops working. It is the single most common way to break this.

The collector sends a few additional touch measurements alongside these. They are accepted and ignored, so forwarding the whole object is always safe. Send a field name we do not recognise and the response carries a warnings array saying so, rather than quietly scoring the visitor as though the data were never there.

What is not in here

No cookies, no local storage, no canvas or WebGL fingerprint, no user-agent profiling, and nothing you typed. Pointer coordinates and keystroke timings are reduced to the summary numbers above inside the browser itself, and the raw traces are discarded there. You can confirm all of it in devtools, which is the point of publishing the list.

On Pro & Enterprise, every verification returns an agent block alongside the human score. It flags AI agents driving a real browser — Playwright, Puppeteer, Selenium, and extension / computer-use agents — even when the session looks human. It only appears when behavioral signals are sent.

One response, a three-way decision: check human first, then agent.is_agent. The action is always yours.

const { human, confidence, agent } = await res.json();

if (!human) {
  // Not human — block or challenge (bot / automated abuse)
  return reject();
}

if (agent?.is_agent) {
  // A real browser driven by an AI agent (Pro & Enterprise).
  // Your call: allow the automation you want, or block / step-up the rest.
  return challenge();
}

// Trusted human — let them through
return allow();

Detection is high-confidence, not absolute — a perfectly throttled agent that mimics human input is an arms race. Use agent_confidence to tune how aggressively you act.

POST/api/v1/sessionHeartbeat — continuous monitoring

Monitor an active user session for bot behavior changes and account takeovers. Call every 30 seconds after a successful verify(). Returns a live score and flags anomalies when behavior drops significantly from the user's verified baseline.

Use Huma.startSession() in your frontend — it handles the 30s interval automatically. Use this endpoint directly only for custom server-side monitoring.

Request body

ParameterTypeRequiredDescription
userIdstringYesSame userId used in verify().
sessionTokenstringYesThe token returned by verify() — used to fetch the behavioral baseline.
sessionDataobjectNoBehavioral signals from the current window (same shape as verify sessionData).

Response fields

FieldTypeDescription
scorenumber 0–100Live session score for this window.
confidencenumber 0–1Confidence as a decimal.
baselinenumber 0–100Score from the original verify — used as comparison reference.
deltanumberScore drop from baseline. Negative = degraded behavior.
anomalybooleanTrue when this window departs far enough from the session baseline, or is low enough on its own, to be worth acting on.
actionstringallow | flag | block. Branch on this rather than on score: a window where the visitor simply read without touching anything carries no information, and thresholding it yourself would treat a reader as a bot.
reasonstringok | quiet_window | behaviour_changed | no_human_signals | automation. Enough to branch on and to log.

Frontend usage

// After a successful verify():
const result = await Huma.verify('user_123', 'huma_live_...');

if (result.human) {
  Huma.startSession('user_123', 'huma_live_...', result.token, {
    intervalMs: 30000, // every 30 seconds
    onAnomaly: function(r) {
      if (r.action === 'block') {
        window.location.href = '/logout'; // account takeover detected
      }
    }
  });
}

In a React app

// Call your own backend, which holds the key and forwards to /api/v1/session.
useEffect(() => {
  const id = setInterval(async () => {
    const r = await fetch("/api/huma/heartbeat", {
      method: "POST",
      body: JSON.stringify({
        userId,
        sessionToken: verifyResult.token,
        sessionData: window.Huma.debug().features,
      }),
    }).then((r) => r.json());

    if (r.action === "block") router.push("/logout");
  }, 30000);
  return () => clearInterval(id);
}, [userId]);

Receive real-time HTTP POST notifications when a bot is detected or a human is verified. Signatures use HMAC-SHA256 — the same pattern as Stripe. Max 5 webhooks per API key.

GET/api/v1/webhooksList your webhooks
Returns all registered webhook endpoints for the authenticated API key.
POST/api/v1/webhooksRegister a webhook
ParameterTypeRequiredDescription
urlstringYesYour HTTPS endpoint. Must start with https://
eventsstring[]NoEvents to receive: bot.detected, human.verified. Defaults to both.

⚑ The secret is shown only once. Save it immediately.

RESPONSE
{
  "id": "wh_abc123...",
  "secret": "huma_whsec_abc123...",  // shown once
  "url": "https://yourdomain.com/webhooks/huma",
  "events": ["bot.detected", "human.verified"],
  "active": true
}
SIGNATURE VERIFICATION

Each webhook includes a Huma-Signature header. Verify it to confirm the request came from useHUMA.

HEADER FORMAT
Huma-Signature: t=1716840000,v1=abc123def456...
import crypto from 'crypto';

function verifyWebhook(rawBody, signature, secret) {
  const [tPart, vPart] = signature.split(',');
  const timestamp = tPart.replace('t=', '');
  const received  = vPart.replace('v1=', '');

  const payload  = `${timestamp}.${rawBody}`;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(received)
  );
}
WEBHOOK PAYLOAD
bot.detected / human.verified
{
  "id": "evt_9Kd3mP8luYMC",
  "event": "bot.detected",
  "created_at": "2026-08-06T21:00:00.000Z",
  "data": {
    "user_id": "user_123",
    "human": false,
    "confidence": 0.18,
    "score": 18,
    "threshold": 0.5,
    "reason": "no_human_signals",
    "token": "h_verified_Kd3mP8luYMC2Mi1x"
  }
}

Everything except id, event and created_at is nested under data. Read it from there: a handler written against a flat body sees undefined for every field, and if (!body.human) is then true on a human.verified event too.

When confidence is borderline (30–70%), present a visual challenge instead of hard-blocking. If passed, the user receives a verification token equivalent to a standard verify. Max 3 attempts per challenge. Challenges expire in 5 minutes.

POST/api/v1/challenge/createGenerate a challenge
ParameterTypeRequiredDescription
userIdstringYesYour internal user identifier.
RESPONSE
{
  "challenge_id": "ch_abc123...",
  "type": "click_targets",        // or "pattern"
  "payload": {
    "instruction": "Click the 🎯",
    "targets": [
      { "id": "t0", "x": 20, "y": 25, "label": "🌿" },
      { "id": "t1", "x": 65, "y": 25, "label": "🎯" },
      ...
    ]
  },
  "expires_in_seconds": 300
}
POST/api/v1/challenge/verifySubmit the user's answer
ParameterTypeRequiredDescription
challenge_idstringYesID from challenge/create.
answerstringYesFor click_targets: target id (e.g. "t1"). For pattern: dot sequence (e.g. "1-5-9").
PASSED
{ "passed": true, "token": "h_challenge_abc..." }
FAILED
{ "passed": false, "message": "Incorrect answer", "attempts_remaining": 2 }
REACT COMPONENT
// After verify() returns borderline confidence.
// Render the challenge yourself: there is no component to import yet.
const ch = await fetch('/api/v1/challenge/create', {
  method: 'POST',
  headers: { Authorization: 'Bearer huma_live_...' },
  body: JSON.stringify({ userId }),
}).then(r => r.json());

// ch = { challenge_id, type, payload }
// Show it, collect the answer, then verify it:
const res = await fetch('/api/v1/challenge/verify', {
  method: 'POST',
  headers: { Authorization: 'Bearer huma_live_...' },
  body: JSON.stringify({ challenge_id: ch.challenge_id, answer }),
}).then(r => r.json());

if (res.passed) continueLogin(res.token);
else router.push('/blocked');
StatusCodeMeaning
200Verification successful.
401MISSING_AUTHNo Authorization header provided.
401INVALID_API_KEY_FORMATKey doesn't match format (huma_live_...).
401INVALID_API_KEYKey not found in database. Check your dashboard.
403EMAIL_NOT_VERIFIEDYour key is valid, but your email isn't confirmed yet. Click the link in your signup email, or resend it from your dashboard.
400Missing or invalid userId in request body.
402TRIAL_EXPIREDFree trial has expired. Upgrade at humaverify.com/#pricing.
429MONTHLY_LIMIT_EXCEEDEDMonthly plan limit reached. Upgrade your plan.
429BURST_LIMIT_EXCEEDEDToo many requests per minute. Check Retry-After header.
429USER_FLOOD_DETECTEDSame userId verified too many times per minute (max 10/min).
503Service temporarily unavailable. Retry with exponential backoff.

There is no published SDK yet

This section used to document an npm install usehuma package with React hooks and Next.js helpers. None of it existed. Six copy-paste examples on this page imported things that were never written, which is worse than having no SDK at all, so they are gone until the package they describe is real.

You do not need one. The integration is a script tag and one POST, and that is the whole surface in any language:

// Browser: collect
<script src="https://humaverify.com/huma.js"></script>
const sessionData = window.Huma.debug().features;

// Server: score, and let people through if we are unreachable
let result = { human: true, confidence: 0 };
try {
  const res = await fetch("https://humaverify.com/api/v1/verify", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.HUMA_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ userId, sessionData }),
    signal: AbortSignal.timeout(5000),
  });
  if (res.ok) result = await res.json();
} catch {
  // Our outage must not become your outage. See /limits.
}

if (!result.human) { /* challenge, flag, or block — your call */ }

A Node package lives in the repository as @usehuma/node, and it will be documented here when it is published and not before. If you want it sooner, say so at team@humaverify.com and it moves up.

Ready to integrate?

Get your API key in seconds. 14-day free trial. No credit card required.

GET YOUR API KEY →
Help