Edge
Back to 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 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.

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 covers this in depth.