DutyRadar API.
One POST /v1/classify call. A three-state verdict you can act on (verified_bot, unverified, or unknown), plus the operator and the method that produced it.
DutyRadar is the positive-ID layer for declared AI bots. It does not detect stealth bots running on residential proxies; for that traffic the honest answer is unverified. Compose DutyRadar with your existing bot-management vendor for stealth scoring.
Quickstart
Three call sites, same request. The verdict comes back in under 50 ms steady-state from a Cloudflare edge.
Production traffic needs a bearer key. Sign up for a free key in seconds at dutyradar.com/signup; you'll mint the plaintext from the dashboard immediately after verifying your email. The verifier on dutyradar.com stays accessible without a signup so anyone can paste a UA + IP and see a real verdict.
Authentication
DutyRadar uses bearer tokens. Sign up for a free key on the landing page; keys look like dr_live_<32 hex> and are shown once at creation. DutyRadar stores only a sha-256 hash.
Put the key in an env var; never commit it. Rotate by minting a new key and revoking the old one. The free tier covers 500 verifications per month, enough to evaluate the API end-to-end before upgrading to a paid tier.
Rate limits
| field | type | description |
|---|---|---|
free | evaluation-grade | 500 verifications/month with a free signed-up key. For evaluation, not production. |
starter | production-grade | 250,000 verifications/month at $39/mo. For indie sites, small SaaS. |
pro | production-grade | 500,000 verifications/month at $79/mo. Higher per-key rate limit, email support. |
scale | negotiated | Above 500k/month, custom SLAs. Email hi@dutyradar.com. |
On 429 the response body is the same error envelope as everywhere else. See Errors. Retry with backoff; the limit is a rolling window, not a hard cliff.
POST /v1/classify
Submit a user_agent (and optionally an IP and a subset of request headers). DutyRadar runs the appropriate verification path for the claimed vendor and returns the verdict envelope plus the underlying 7-category taxonomy.
Request body
| field | type | description |
|---|---|---|
user_agent req | string | Raw User-Agent header value from the request you want to verify. 1–2048 chars. |
ip | string | Client IP, IPv4 or IPv6. Optional but strongly recommended; without an IP the verdict can only be ua_only at best. |
headers | object<string, string> | Subset of request headers, lower-cased keys. Pass signature-input, signature, signature-agent to enable Web Bot Auth verification. Max 32 entries, 8 KiB per value. |
authority | string | Request authority (host). Required if a Web Bot Auth signature covers @authority. |
method | string | HTTP method. Required if a Web Bot Auth signature covers @method. |
path | string | Request path. Required if a Web Bot Auth signature covers @path. |
Response body
All responses follow the envelope shape { data: T, error: null } on success and { data: null, error: { code, message, request_id } } on failure. Never both.
| field | type | description |
|---|---|---|
data.verdict req | verified_bot | unverified | unknown | Top-line answer. See Verdict envelope below. |
data.operator req | string | null | Vendor slug for the identified operator (lower-cased, snake-case). null when no operator was identified. See /openapi.json for the current enum. |
data.method req | web_bot_auth | ip_list | reverse_dns | ua_only | null | Which verification path produced the verdict. null when no bot pattern matched the UA. |
data.category req | string | 7-category taxonomy: human, training_crawler, retrieval_crawler, user_triggered_fetcher, traditional_bot, undeclared_agent, unknown. |
data.bot req | Bot | null | Matched bot record (name, vendor, category, match string), or null. |
data.verification req | Verification | { ua_match, ip_match, signature_agent_verified }: three booleans for the underlying signals. |
data.recommended_action req | allow | allow_with_caution | allow_or_block_per_policy | block | Suggested policy for routing the request. Advisory; your WAF rule can branch on verdict directly. |
Examples
Three real shapes you'll see in production:
Verdict envelope
Most callers only care about three fields: verdict, operator, and method. Everything else is supporting evidence.
Identity confirmed via signature, IP allow-list, or rDNS + forward-confirm. Safe to allow training crawlers, bypass anti-bot rate limits, attribute traffic.
UA claims a bot but no method confirmed identity, or the signal said the UA is impersonating. Spoofers, vendor-claim mismatches, and undeclared agents all land here. Treat as suspicious.
Looks human, or unrecognised UA we have no opinion on. Default allow. DutyRadar is not a stealth-bot detector, so this bucket includes both real users and stealth scrapers we can't see.
Verification methods
Each operator publishes a different way to prove their bot is real. DutyRadar runs the right check for the claimed vendor; you read the verdict.
| field | type | description |
|---|---|---|
web_bot_auth | RFC 9421 Ed25519 | Signature verified against the operator's published JWK directory. Strongest signal: cryptographic proof of origin. |
ip_list | operator allow-list | Source IP falls within the operator's published allow-list, kept current from each vendor's authoritative source. |
reverse_dns | rDNS + forward-confirm | Reverse-DNS lookup against the operator's domain, then forward-confirmed against the source IP. |
ua_only | no verification confirmed | UA matched a known bot pattern but no verification method confirmed identity. Returned with verdict: unverified. |
The three booleans on data.verification tell you which signals fired: ua_match, ip_match, signature_agent_verified. The verdict is derived from those, so you can branch on them directly if you want finer-grained policy.
Coverage
We tier each bot by what its vendor actually publishes. Tiers run from strongest signal (signed) to UA-only (no source). The honest read for the no-source bots is part of the product, not a footnote, so you can write WAF rules that don't pretend Bytespider or TikTokSpider are verifiable when they aren't.
Signed (4)
Web Bot Auth signature plus an IP allow-list.
- GPTBot OpenAI
- OAI-SearchBot OpenAI
- ChatGPT-User OpenAI
- DuckAssistBot DuckDuckGo
IP-verified (16)
Vendor publishes a machine-readable IP allow-list.
- ClaudeBot Anthropic
- Claude-User Anthropic
- Claude-SearchBot Anthropic
- PerplexityBot Perplexity
- Bingbot Microsoft
- Googlebot Google
- GoogleOther Google
- GoogleOther-Image Google
- GoogleOther-Video Google
- Google-Extended Google
- Google-CloudVertexBot Google
- Applebot Apple
- Applebot-Extended Apple
- CCBot Common Crawl
- MistralAI-User Mistral
- MistralAI-Index Mistral
rDNS only (4)
No machine-readable list. Forward-confirmed reverse DNS is the documented path.
- YandexBot Yandex
- Baiduspider Baidu
- Mail.RU_Bot Mail.ru
- PetalBot Huawei
No source (18)
Vendor publishes nothing verifiable. UA-only confidence.
- Bytespider ByteDance
- TikTokSpider ByteDance
- Meta-ExternalAgent Meta
- Meta-ExternalFetcher Meta
- Meta-WebIndexer Meta
- FacebookBot Meta
- anthropic-ai Anthropic
- Perplexity-User Perplexity
- Sogou web spider Sogou
- 360Spider Qihoo 360
- HaosouSpider Qihoo 360
- Diffbot Diffbot
- Amazonbot Amazon
- DuckDuckBot DuckDuckGo
- YouBot You.com
- cohere-ai Cohere
- UptimeRobot UptimeRobot
- Slurp Yahoo
Per-bot research notes (vendor-published sources, citations, and the verification path we run for each) are available in your dashboard once you have a key. Coverage moves over time; live captured-at timestamps are surfaced on /health.
Errors
Every error returns the same envelope shape: a JSON body with data: null and a populated error object that always includes a request_id for support correlation.
Status codes
| field | type | description |
|---|---|---|
400 invalid_input | client | Body wasn't valid JSON, or schema validation failed. Error message names the offending field. |
401 unauthorized | client | Missing or invalid API key. Returned once production auth is enabled. |
413 payload_too_large | client | Request body exceeded the 32 KiB hard cap. Most callers won't hit this; typical bodies are < 1 KiB. |
429 rate_limited | client | Per-IP or per-tier rate limit. Retry with backoff. |
404 not_found | client | Unknown route. The API surface is one POST + /health, so you probably want /v1/classify. |
500 internal | server | Unexpected error. Capture the request_id and email hi@dutyradar.com. |
GET /health
Public, unauthenticated, CORS-open. Returns the bundled snapshot ages (compiled-in fallbacks) and the live KV snapshot ages. KV ages are not_yet_refreshed until the daily cron has populated each namespace at least once.
Use this for a Cloudflare-side health check or your own external monitor. Don't poll it from clients on a tight loop; it isn't rate-limited per-key (no key concept on this endpoint), but it is per-IP rate-limited by Cloudflare's platform defaults.
OpenAPI spec
The full machine-readable contract is at /openapi.json. OpenAPI 3.1, hand-written so descriptions and examples carry the same weight as schemas. Useful for importing into Postman / Insomnia, or for grafting DutyRadar into your existing API gateway.