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 steps to integrate
Step 1 — In the browser: add the snippet, read the signals
<script src="https://humaverify.com/huma.js"></script>

const sessionData = window.Huma.debug().features;
// send userId + sessionData to your own backend
Step 2 — On your server: score it, where your key lives
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 }),
});
const result = await res.json();

if (result.human && result.confidence > 0.8) { /* allow */ }

The snippet silently collects behavioral signals (mouse, keyboard, scroll). Your key never reaches the browser: it authorizes webhook management as well as verification, so anyone who reads it from your page can spend your quota and rewire where your results are sent.

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.
agentobjectEvery plan, Free included. AI-agent detection: { is_agent, agent_confidence (0–1), signals[] }. Parallel to the human score — present only when behavioral signals are sent.
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": {                       // every plan, Free included
    "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, WebGL or WebGPU 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 every plan, the free one included, 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. Returned on every plan.
  // 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 your server's verify() call came back human, it returns you the token.
// Point the heartbeat at your own route so the key stays on your server.
Huma.startSession(userId, '', verifyResult.token, {
  endpoint: '/api/huma/heartbeat', // your backend adds the Authorization header
  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.

StatusCodeMeaning
200—Verification 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.
400—Missing or invalid userId in request body.
429MONTHLY_LIMIT_EXCEEDEDPlan quota used up for the month. The response carries the reset date. Nothing is ever billed as overage.
429BURST_LIMIT_EXCEEDEDToo many requests per minute. Check Retry-After header.
429USER_FLOOD_DETECTEDSame userId verified too many times per minute (max 10/min).
503—Service temporarily unavailable. Retry with exponential backoff.

npm install usehuma

The usehuma package is published on npm with three entries: usehuma for any browser app, usehuma/react for hooks and components, and usehuma/next for verifying on the server. It collects behavioral signals and calls the same endpoint documented above.

npm install usehuma

// Browser: collect signals, then verify
import { init, verify } from "usehuma";
init(); // call once, early — signals need time to accumulate
const result = await verify({ apiKey: "huma_live_...", userId: "user_123" });
if (result.human) { /* ... */ }

// React: gate content behind verification
import { HumaGate } from "usehuma/react";
<HumaGate apiKey="huma_live_..." userId={userId} fallback={<Spinner />}>
  <SensitiveContent />
</HumaGate>

// Next.js: verify on the server, key stays in HUMA_API_KEY
import { verifyHumaSession } from "usehuma/next";
const result = await verifyHumaSession({ userId, sessionData });

One thing to know. The browser entries send your API key from the page, which is fine while testing; in production keep the key on your server, either with usehuma/next or by pointing endpoint at your own proxy route. Since 1.2.2 the SDK collects the same signal families as huma.js, so responses include the agent verdict on plans that carry it; on 1.2.1 and earlier they do not.

No package is required, though. The whole surface in any language is a script tag and one POST:

// 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 separate server-only package, @usehuma/node, exists in the repository but is not on npm; usehuma/next covers the same ground. Missing something? Say so at team@humaverify.com and it moves up.

Ready to integrate?

Get your API key in seconds. Free plan: 1,000 verifications a month, no card, no expiry.

GET YOUR API KEY →
Help