Website integration

Server verification

Verify a widget token on your backend with POST /api/v1/tokens/verify, or offline against the JWKS. @imnotahuman/server is not on npm.

The widget's verified state lives in the browser and can be faked by anyone with devtools. The token cannot: it is an EdDSA JWT signed by imnotahuman.ai, scoped to your site key, valid for 10 minutes and consumable exactly once. Always verify it server-side.

Online verification (recommended)

No API secret is required; the token itself is the credential.

curl
curl -sS -X POST https://imnotahuman.ai/api/v1/tokens/verify \
  -H 'content-type: application/json' \
  -H 'accept: application/json' \
  -d '{
    "token": "<imnotahuman-token>",
    "site_key": "site_live_exampleweb2ab3cd4ef5gh6j",
    "consume": true
  }'
fetch
const response = await fetch("https://imnotahuman.ai/api/v1/tokens/verify", {
  method: "POST",
  headers: {
    "content-type": "application/json",
    accept: "application/json",
  },
  body: JSON.stringify({
    token,                  // hidden input "imnotahuman-token" or result.token
    site_key: siteKey,      // site_live_… in production
    consume: true,          // omit allow_test in production
  }),
});

const body = await response.json();
if (!response.ok || body.verified !== true) {
  // body.error.code — TOKEN_INVALID, TOKEN_EXPIRED, TOKEN_SITE_MISMATCH,
  // TOKEN_ALREADY_USED, TOKEN_TEST_MODE, …
  throw new Error(body.error?.code ?? "NOT_VERIFIED");
}

// body.status === "VERIFIED"
// body.mode === "live" | "test"
// body.agent.id, body.agent.name
// body.verification.method, .action, .intent, .origin, .challenge_id
// body.consumed === true
// body.expires_at
FieldTypeDescription
tokenrequiredstringThe JWT from the widget (hidden input imnotahuman-token or result.token).
site_keystringYour site key. Rejects tokens issued for another site with TOKEN_SITE_MISMATCH. Strongly recommended.
consumebooleanMark the token used. A second call for the same token then fails with TOKEN_ALREADY_USED. Default false at the API level; the @imnotahuman/server client defaults to true.
allow_testbooleanAccept tokens with mode "test". Default false: test tokens fail with TOKEN_TEST_MODE.
200 OK
{
  "verified": true,
  "status": "VERIFIED",
  "subject_type": "agent",
  "mode": "live",
  "site_key": "site_live_exampleweb2ab3cd4ef5gh6j",
  "agent": { "id": "agent_nova_8f2a", "name": "Nova" },
  "verification": {
    "method": "ed25519_challenge",
    "verified_at": "2026-09-20T12:00:07.000Z",
    "challenge_id": "ch_mfr4q2xk7tbn3ycz5w6ah2ep4d",
    "origin": "https://example.com",
    "action": "checkout",
    "intent": "purchase"
  },
  "consumed": true,
  "expires_at": "2026-09-20T12:10:07.000Z"
}

Failures return an error body with one of TOKEN_INVALID, TOKEN_EXPIRED, TOKEN_SITE_MISMATCH, TOKEN_ALREADY_USED, TOKEN_TEST_MODE. Treat any non-200 as not verified. Symptom-by-symptom notes are on Troubleshooting.

Node.js with @imnotahuman/server

Terminal
# Not published to npm. Verify tokens with POST /api/v1/tokens/verify.
Not on npm

@imnotahuman/agent, @imnotahuman/react and @imnotahuman/server are private packages. They are not installed from npm. Use the script tag, the REST API, and the agent CLI at https://imnotahuman.ai/agent.mjs.

checkout.ts
import { verifyToken } from "@imnotahuman/server";

export async function handleCheckout(req, res) {
  const token = req.body["imnotahuman-token"];
  const result = await verifyToken(token, {
    siteKey: "site_live_exampleweb2ab3cd4ef5gh6j",
    consume: true,          // default
    allowTest: false,       // default
  });

  if (!result.verified) {
    return res.status(403).json({ error: result.error?.code ?? "NOT_VERIFIED" });
  }

  // result.agent  -> { id: "agent_nova_8f2a", name: "Nova" }
  // result.verification.action -> "checkout"
  // Your authorization logic decides what this agent may do.
}

verifyToken(token, options) returns a TokenVerification object and never throws on verification failure; network failures come back as error.code = "NETWORK_ERROR". Options: siteKey, consume (default true), allowTest (default false), apiBase, fetch.

Offline verification with JWKS

Public signing keys are published at https://imnotahuman.ai/.well-known/jwks.json (alias of /api/v1/.well-known/jwks.json, cacheable for 5 minutes). You can verify the JWT signature and claims locally.

JWKS
{
  "keys": [
    {
      "kty": "OKP",
      "crv": "Ed25519",
      "x": "_Gqb_W0_oiqrRCCw8P_X8DSeNSwTiMpA4rUS3OqqBZk",
      "kid": "inh-q2Xk7tBn",
      "use": "sig",
      "alg": "EdDSA"
    }
  ]
}
offline.ts
import { verifyTokenOffline } from "@imnotahuman/server";

const jwks = await fetch("https://imnotahuman.ai/.well-known/jwks.json").then((r) => r.json());

const result = await verifyTokenOffline(token, {
  jwks,
  siteKey: "site_live_exampleweb2ab3cd4ef5gh6j",
  allowTest: false,
});
Offline checks do not detect replay

verifyTokenOffline validates the signature, issuer, exp, audience and mode, but it cannot know whether the token was already presented. Track jti (the challenge id) yourself for the token's lifetime, or use the online endpoint with consume: true.

If you use another JWT library: algorithm EdDSA (Ed25519), header kid selects the JWK, issuer https://imnotahuman.ai, audience is the site key, and the custom claim inh must have v: 1. Claims are documented on Tokens.

What to check after verification

  • verification.action / intent match the operation being performed. A token minted for "login" should not authorise "checkout".
  • verification.origin is the page origin you expect.
  • mode is "live" in production.
  • agent.id identifies the credential. For web_bot_auth results it is the agent's origin (e.g. https://agent.example) rather than a registry id.