Verifying AI agents
Every other check here weighs evidence. This one proves. An agent signs its request with a private key, we verify it against a key the agent publishes, and the answer is arithmetic — the only signal in this API with no false-positive rate.
It changes the question.A growing share of legitimate traffic is automated — a real customer delegating a task to an AI assistant trips every classical bot signal there is, and refusing them is simply wrong. “Is this automated?” is becoming the wrong question. “Is this a known agent?” is one a signature can answer and a user-agent string never could.
How it works
The agent sends three headers. You forward them, plus the URL it requested, and we do the rest: fetch the agent’s public keys from /.well-known/http-message-signatures-directory, match the key by thumbprint, and verify the Ed25519 signature over the request.
curl -X POST https://www.layercall.com/v1/verify/agent \
-H "X-Api-Key: tl_live_your_key" \
-H "Content-Type: application/json" \
-d '{
"method": "GET",
"url": "https://yoursite.com/pricing",
"headers": {
"signature-agent": "\"https://chatgpt.com\"",
"signature-input": "sig=(\"@authority\" \"@method\" \"@path\");created=…;expires=…;keyid=\"…\";tag=\"web-bot-auth\"",
"signature": "sig=:base64signature:"
}
}'{
"verified": true,
"agent": "https://chatgpt.com",
"keyid": "otMqcjr17mGyruktGvJU8oojQTSMHlVm7uO-lrcqbdg",
"purpose": "ai",
"reason": null,
"expires_in": 3421,
"replay_protection": "signature-window"
}Pass the agent’s request, not ours
The signature covers the method, authority and path the agent actually called — your URL. None of it can be inferred from the request you make to us, which is why url is required and why a wrong one produces signature_mismatch rather than a vague failure.
There is no score, on purpose
A verified signature is a fact, not a judgement. What to do with it is your policy: an assistant acting for one of your customers is usually welcome, a scraper usually is not, and both may be perfectly signed. We tell you who it is; you decide.
Why verification fails
reasonis always specific, because “not verified” on its own is not actionable.
| reason | Meaning |
|---|---|
| no_signature | The request carried no signature at all — most traffic |
| not_web_bot_auth | Signed, but tagged for a different purpose |
| expired | The signature's validity window has passed |
| created_in_future | Signed with a clock well ahead of ours |
| directory_unreachable | The agent's key directory did not answer |
| key_not_found | The keyid is not in the published directory |
| keyid_thumbprint_mismatch | The key does not hash to the id claiming it |
| key_expired | The published key is outside its own nbf/exp window |
| signature_mismatch | The signature does not verify — wrong key, altered request, or a replay against a different path |
Replay protection
replay_protection currently reports "signature-window". A captured signature cannot be replayed against a different path or method — the signature covers both — but within its own validity window (the spec recommends 24 hours or less) the same request can be repeated. Single-use nonce enforcement is not yet implemented, and the field says so rather than leaving you to assume otherwise.
In the SDKs
const result = await lc.verifyAgent({
method: req.method,
url: `https://${req.headers.host}${req.url}`,
headers: req.headers,
});
if (result.verified && result.purpose === "ai") {
// A known assistant. Serve it, log it, rate-limit it differently.
}result = client.verify_agent(
url=f"https://{request.host}{request.path}",
headers=dict(request.headers),
method=request.method,
)The standard
Web Bot Auth is an IETF draft (draft-meunier-web-bot-auth-architecture) built on RFC 9421 HTTP Message Signatures, with Cloudflare, Google, Amazon and OpenAI behind it. Adoption is early — chatgpt.com publishes a directory today, most do not — so treat verified: falseas “unsigned” rather than “suspicious”. For unsigned automation, use /v1/score/device instead.