APIs are different from form-submit surfaces. A single page can make many API calls, so you decide how often to ask Foil for a fresh handoff. And you want to distinguish LLM scrapers (
ai-agent) from crawlers and signed agents that you allow, because the right answer for those is different.The threat
Two problems get lumped together as “API abuse,” and they want different responses:- LLM scraping. Cloud-hosted browser agents and their self-hosted equivalents scrape the web on behalf of model training, retrieval pipelines, or end-user automation. They look like browsers because they are browsers, just driven by an agent loop. Foil attributes these as
ai-agent. - Scripted scraping. Classic headless Chrome iterating your listing endpoints, pricing APIs, or search results. Foil attributes these as
automation. HTTP-level clients that don’t run a browser never produce a Foil session, so the guard below rejects their requests because they carry no token.
- Search crawlers (Googlebot, Bingbot) - you want them indexing your public content. Foil labels a session
crawlerfrom the request’s User-Agent header, which anyone can send, so verify a crawler before you trust it. - Signed agents - some browser agents sign their requests with HTTP message signatures (Web Bot Auth) to identify themselves. When Foil verifies a signature, it adds a
trustlabel with the valueverifiedto the session’s attribution, and it adds the signing domain as aproductlabel. For signers that Foil recognizes, the actor label isai-agent, so a policy that blocks everyai-agentsession also blocks these agents. - Your own internal tooling - health checks, monitoring, analytics pipelines. These calls don’t come from a browser, so they have no Foil session. Authenticate them with API keys.
attribution.bot.labels lists labels that each have a kind, a value, and a confidence from 0 to 1. The actor label is the one whose kind is actor, and its value is automation, ai-agent, crawler, or unknown. A session can have no actor label, so treat a missing label as unknown.
The flow
1
Start Foil once at app boot
For authenticated SPAs, start the client when the shell mounts. For public APIs accessed directly (no browser), see the bottom of this page.
2
Request a handoff when the page makes a protected call
Call
getSession() and send the sealedToken as a header. Every call returns a fresh token for the same sessionId. To avoid a Foil call for every request on a chatty page, you can reuse a handoff for a short time.3
Verify on each protected request
Verify the token locally with
safeVerifyFoilToken() and reject a token that is more than a couple of minutes old. For very hot endpoints, look up the verified session in the Sessions API instead and cache the verdict.4
Re-verify on high-value actions or periodically
For mutations or sensitive reads, request a fresh handoff. For cached verdicts, refresh every few minutes.
5
Split policy by attribution
Allow signed agents (
trust label verified) and verified crawlers on read requests; block automation and ai-agent; treat human normally.Client integration
Request a sealed handoff when the page makes a protected API call, and send the token as a header.getSession() is a call to Foil, so it adds latency to the request that waits for it. The example reuses a handoff for up to one minute so that a burst of requests doesn’t make a Foil call each time, and it requests a fresh handoff for high-value actions.
A token that the page reuses is a token that a script could capture from a real browser session and replay. Keep the reuse window short, and set the maximum token age on the server only slightly above it. The example reuses a handoff for one minute, and the server in the next section accepts tokens that are up to two minutes old.
getSession() fails, the request goes out without a token and the guard in the next section rejects it. If you also serve clients that don’t use your front end, route them by API key before the guard.
Server verification: two patterns
You have two ways to check a session on your server. You can verify the sealed token on each protected request, which is a local crypto operation with no network call, or you can bind the verified session to your own session and read its latest verdict from the Sessions API. The server SDKs verify a token’s contents but don’t check its age. Both patterns need your own age check ondecision.evaluated_at, so that a token captured from one session can’t be replayed later.
Pattern A: verify the sealed token per request
The examples do the following:- Verify the sealed token and treat it as missing if verification fails or if the token is more than two minutes old.
- Allow a signed agent (a session with a
trustlabel ofverified) onGETrequests, and block it on everything else. - Allow a session that Foil labels
crawleronGETrequests only after your own check confirms the crawler. The verified-crawler helper in each example stands in for that check. Use a reverse DNS lookup of the request’s IP address that a forward lookup confirms, or compare the address with the IP ranges that the crawler’s operator publishes. - Block every other session with a
botverdict.
Pattern B: read the verdict from the Sessions API
For very hot endpoints, verify a fresh token once, store the verifiedsession_id in your own server-side session, and read the session’s latest verdict from Foil on later requests, caching it for a few minutes. Foil’s server-side behavioral scoring can move a verdict between the snapshot and behavioral phases as evidence accumulates, so a lookup can be more current than the token that you verified first.
Take the session ID from a token that you verified, not from a request header. A client can send any session ID, including one that it copied from a real browser session.
Node.js
decision.automation_status ("automated" | "human" | "uncertain") rather than the sealed-token verdict field, and its attribution is null when Foil has nothing to attribute. The session’s attribution.labels have the same kind and value as the token’s labels, plus a display label and a confidence from 0 to 100. See Server verification for the full shape.
Foil limits all of your organization’s secret keys together to 600 requests per minute by default, and a limited request returns 429 with a Retry-After header. See Rate limits. The server SDKs don’t retry for you, so the example keeps using the last cached verdict when Foil returns a 429, but only until that verdict is 15 minutes old. After that, getCachedVerdict() throws, and your handler should treat the session as having no verdict and ask the client for a fresh token. Each cached session costs one request per cache period, so a longer period lets you serve more concurrent sessions within the limit.
When to re-verify
Session reuse is great for throughput, but a long-lived session becomes a stale verdict. Three refresh triggers worth coding:- High-value mutations - a user changing their email, deleting data, exporting an archive. Always ask for a fresh sealed handoff from the client, as
apiMutatedoes in the client example. - Periodic refresh - every 5–15 minutes for long sessions, re-fetch
GET /v1/sessions/:sessionId. Foil’s server-side behavioral scoring can move a verdict betweensnapshotandbehavioralphases as evidence accumulates, and you want the latest. Each refresh counts against your secret-key rate limit, as described in Pattern B. - Suspicious pattern observed - your own application logic (burst of identical queries, geographic jump) can trigger a client-side
foil.getSession()that produces a new, freshly-attested handoff.
Splitting policy by attribution
For read APIs, the policy matrix that works on most sites:
Check the
trust label before you check the actor label. Foil gives signed agents from signers it recognizes the actor label ai-agent, so a policy that blocks every ai-agent session also blocks the agents that identify themselves.
“Allow” doesn’t mean unlimited. Keep a generous rate limit on signed agents and verified crawlers - a badly-written crawler can still hurt you - but don’t return 403s.
If you want AI agents to read your content but not scrape it, set different caps for ai-agent: a low QPS limit that’s fine for an agent answering one user’s question and painful for a training-data crawler.
APIs called directly (no browser)
Some API consumers don’t run a browser at all - a mobile app, a server-to-server integration, a CLI tool. Requests to your own API go to your servers, not to Foil, so Foil never sees them. Direct API traffic has no Foil session, and the browser SDK doesn’t apply. For this traffic:- Require API keys. Issue a key to each customer or integration, and route requests that don’t carry a Foil token down the API-key path. Apply Foil only to requests from your browser front end.
- Rate limit by key. Limit each API key, and limit by IP address the traffic that has no key. A per-key limit contains a leaked or shared key.
- Use the native SDKs for your own mobile apps. The iOS SDK and the Android SDK return a sealed handoff that you verify on your server in the same way as a browser token.
What’s next
User-generated content
The write-side counterpart: stop LLM posts at the composer.
Server verification
Reference for both sealed-token and session-readback paths.
Detection categories
How Foil combines its checks into a verdict.
Going to production
Rollout plan for API-wide enforcement.