Developers
Five minutes to agent-aware.
One page tag. One server check of the token. The hosted service is free for every site, and a test token cannot pass as a live one.
Install
The script tag works today.
Replace site_test_your_key with the test key from your dashboard. data-api keeps challenge traffic on imnotahuman.ai. The queue runs before the widget loads, which is what window.imnotahuman.q is for.
data-site-key— your test or live keydata-api— https://imnotahuman.aidata-action/data-intent— bound into the challenge and the token
Not on npm yet. Source is in packages/; the HTML tag and the REST API work today.
<script>
window.imnotahuman = {
q: [["onVerified", function (result) {
// result.agent_id, result.token, result.mode ("live" | "test"), result.challenge_id
// Your route. Verify result.token on the server. The browser flag is not proof.
fetch("/api/agent-token", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ token: result.token })
});
}]]
};
</script>
<div class="imnotahuman"
data-site-key="site_test_your_key"
data-api="https://imnotahuman.ai"
data-action="checkout"></div>
<script src="https://imnotahuman.ai/widget.js" async></script>In order
The whole integration.
- Page
- Sign in, add the staging origin, and copy the
site_test_key. Thenhttps://imnotahuman.ai/widget.jswithdata-api="https://imnotahuman.ai"anddata-action. - Server
POSThttps://imnotahuman.ai. Staging sends
/api/v1/ tokens/ verify allow_test. Live calls omit it. Or checkhttps://imnotahuman.ai.
/.well-known/jwks.json - Receive
- agent.id, action, and mode from the API response, plus the challenge and origin.
- Decide
- Checkout, an API, or a rate limit you control. The browser flag is not the decision.
Not on npm yet. Source is in packages/; the HTML tag and the REST API work today.
Test keys
Start with site_test_. Ship with site_live_.
Your keys
Every site gets a public site_test_ key and a public site_live_ key. Test challenges show TEST MODE, can be completed with Simulate agent, and issue tokens that production verification rejects unless the caller sends allow_test: true.
Public demo keys
site_test_imnotahumanpublicdemo and site_live_imnotahumanpublicdemo only work on imnotahuman.ai and local development origins. A challenge from your domain with either key fails with ORIGIN_UNAUTHORIZED. Create your own site for your origin.
Verify
One call on the server.
The widget can hand the page a token. Only POST /api/v1/tokens/verify tells you it was issued for your site key. Pass consume: true so a second presentation fails. Test-mode tokens need allow_test: true, and that flag stays off the production path.
Prefer no network call? Check the EdDSA signature offline with https://imnotahuman.ai.
/.well-known/
curl -sS -X POST https://imnotahuman.ai/api/v1/tokens/verify \
-H "content-type: application/json" \
-d '{"token":"<token from the widget>","site_key":"site_test_your_key","consume":true,"allow_test":true}'Result
Never just a boolean.
The verify response names the agent, the method, the challenge, the origin, and the action. Statuses are PENDING, VERIFIED, REJECTED, EXPIRED, REVOKED, or ERROR. Branch on those fields after the API returns them.
{
"verified": true,
"status": "VERIFIED",
"subject_type": "agent",
"mode": "live",
"agent": { "id": "agent_nova_8f2a", "name": "Nova" },
"verification": {
"method": "ed25519_challenge",
"verified_at": "2026-09-20T13:42:07.000Z",
"challenge_id": "ch_k7q2m5x4a3b3c6d2e6f4g2h5jk",
"origin": "https://example.com",
"action": "checkout",
"intent": ""
},
"token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImtpZCI6...",
"expires_at": "2026-09-20T13:52:07.000Z"
}Stable errors
Machine-readable codes such as CHALLENGE_EXPIRED, ORIGIN_UNAUTHORIZED, and TOKEN_TEST_MODE, under /api/v1.
Agent side
Building an agent? Register it, keep the private key, and sign challenges with https://imnotahuman.ai/agent.mjs or any Ed25519 library.
Company decisions
Checkout, APIs, your rate limits, and the event log are described on For companies. The proof limits are on Security.