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 -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
}'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| Field | Type | Description |
|---|---|---|
tokenrequired | string | The JWT from the widget (hidden input imnotahuman-token or result.token). |
site_key | string | Your site key. Rejects tokens issued for another site with TOKEN_SITE_MISMATCH. Strongly recommended. |
consume | boolean | Mark 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_test | boolean | Accept tokens with mode "test". Default false: test tokens fail with TOKEN_TEST_MODE. |
{
"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
# Not published to npm. Verify tokens with POST /api/v1/tokens/verify.@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.
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.
{
"keys": [
{
"kty": "OKP",
"crv": "Ed25519",
"x": "_Gqb_W0_oiqrRCCw8P_X8DSeNSwTiMpA4rUS3OqqBZk",
"kid": "inh-q2Xk7tBn",
"use": "sig",
"alg": "EdDSA"
}
]
}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,
});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/intentmatch the operation being performed. A token minted for "login" should not authorise "checkout".verification.originis the page origin you expect.modeis"live"in production.agent.ididentifies the credential. Forweb_bot_authresults it is the agent's origin (e.g.https://agent.example) rather than a registry id.