---
title: "Protecting APIs from Bot Abuse"
description: "Shield isn't just for forms. Attach humanity tokens to API requests, verify them with siteverify or locally against the JWKS endpoint, and identify signed AI agents with agentverify."
url: https://edge.network/academy/protecting-apis-from-bots/
---

# Protecting APIs from Bot Abuse

[Back to Academy](https://edge.network/academy)
Security
10 min read

# Protecting APIs from Bot Abuse

Scraping, enumeration, and free-tier abuse don't come through your forms — they hit
your endpoints directly. Edge Shield's tokens work there too, with a verification
path fast enough for API traffic.

## Forms are the front door. APIs are every other door.

Once your web app moves logic into fetch calls — search, autocomplete, pricing
lookups, content feeds — those endpoints become the cheapest way to abuse your
service. A scraper doesn't render your page or fill in your form; it calls
`/api/search` ten
thousand times.

Rate limiting helps but is blunt: attackers rotate IPs, and your heaviest legitimate
users look exactly like abusers by request count alone. What's missing is the same
question Shield answers on forms — *is a human driving this?* — attached to
each request.

## 1. Attach a Shield token to API calls

The widget's JavaScript API can produce tokens on demand, not just on form submit.
Request one and send it in a header your server checks:

```
// Render an invisible widget once — it verifies in the background
// and keeps its token fresh automatically
const widgetId = edgeShield.render(document.getElementById('shield'), {
  sitekey: 'es_your_sitekey',
  mode: 'invisible',
})

// Attach the current token to each protected request
await fetch('/api/search', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Edge-Shield-Token': edgeShield.getResponse(widgetId),
  },
  body: JSON.stringify({ query }),
})
```

The verification runs invisibly in a background thread — your users never see
anything. See the [JS API docs](https://edge.network/docs/shield/js-api)
for the full programmatic interface.

## 2. Pick the right verification path

For forms, one call to `siteverify` per
submission is nothing. For an API taking hundreds of requests a second, a network
round trip per request is real latency. Shield gives you a second option:
**tokens are Ed25519-signed JWTs**, and the public
key is published at a standard JWKS endpoint — so your server can verify them
locally, with zero network calls.

```
import { createRemoteJWKSet, jwtVerify } from 'jose'

const JWKS = createRemoteJWKSet(
  new URL('https://shield.edge.network/.well-known/jwks.json')
)

// Zero network calls per request — the key set is cached
async function verifyOffline(token) {
  const { payload } = await jwtVerify(token, JWKS, {
    issuer: 'edge-shield',
    subject: process.env.SHIELD_SITEKEY, // REQUIRED — binds tokens to YOUR widget
  })
  // Narrow the replay window: reject tokens older than 60s
  if (Date.now() / 1000 - payload.iat > 60) throw new Error('stale token')
  return payload.score // 1–100
}
```

The trade-off: **offline verification cannot enforce single-use**. The ledger of spent tokens lives on Shield's servers, so a token
verified offline stays valid for its full five-minute lifetime, however many times
it's presented. The `iat` age
check above narrows that window, but the rule of thumb is:

- **Offline** for high-throughput, lower-stakes endpoints — search, previews, read APIs — where latency matters more than strict single-use.
- **siteverify** for anything a replayed token shouldn't touch — signups, payments, mutations. It's the only path with a strict single-use guarantee.

Full details, including key rotation behaviour, are in the
[offline verification docs](https://edge.network/docs/shield/offline-verification).

## 3. Don't lock out the automation you want

Some clients hitting your API without a widget token are exactly who you want:
AI agents shopping or researching on behalf of real customers. Agents that sign
their requests with **Web Bot Auth** can be
identified without any widget at all — forward the signature headers to
`agentverify`:

```
// A signed agent request hit your API directly — no widget involved.
// Forward its signature headers and Shield verifies the identity.
const res = await fetch('https://shield.edge.network/agentverify', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    secret: process.env.SHIELD_SECRET,
    authority: req.headers.host,
    headers: {
      'signature-agent': req.headers['signature-agent'],
      'signature-input': req.headers['signature-input'],
      'signature': req.headers['signature'],
    },
  }),
})
const { success, agent } = await res.json()
if (success && agent.verified) allowFor(agent.domain)
```

So the full policy for an API endpoint becomes: valid Shield token with a decent
score → human, allow. Valid Web Bot Auth signature → known agent, allow (with its
own rate tier if you like). Neither → the automation that refused to identify
itself, which is the traffic you wanted to stop.
[Verified AI Agents with Web Bot Auth](https://edge.network/academy/verified-ai-agents) covers
this in depth.

## Next steps

[Offline verification docs JWKS, claims, key rotation](https://edge.network/docs/shield/offline-verification) [Verified AI Agents Web Bot Auth and agent policy](https://edge.network/academy/verified-ai-agents)
