Shield
Shield on a Static Site
You've added the widget to a site with no backend, it renders and produces a token, and the docs say "always validate server-side". This page is for that moment: how to see the check working from your own laptop, what Shield does and doesn't protect without a server, and the smallest thing you can add to protect a real form.
Where the check lives
Shield has two halves. The widget runs in the visitor's browser, talks only to
shield.edge.network,
and drops a signed token into a hidden
edge-shield-response
input. The check is a single HTTP call to
siteverify with your
secret key, which confirms the token is genuine, unexpired and unused, and returns a humanity score.
The check has to run somewhere that can hold the secret, which rules out the page itself. On a static site there's nothing on your side to run it, so from the browser the integration looks half-finished. It isn't. The widget half is done; the check simply needs a place to live, and for testing that place can be your terminal.
What a static site has to protect
Shield gates actions: a form submit, a signup, an API call. It does not gate page views, and it can't stop a bot reading a page the CDN serves to everyone. If your site has no form or endpoint that posts anywhere, there's nothing for Shield to check yet, and that's a fine place to be until there is.
Test the check from your laptop
siteverify is just an
HTTP call. You can make it with curl
and watch the whole flow work end to end, no server required.
- 1
Embed the widget
Use your real sitekey from Control → Shield. The form doesn't need a real
actionyet.<script src="https://shield.edge.network/api.js" defer></script> <form action="#" method="post"> <input name="email" type="email" required> <div class="edge-shield" data-sitekey="es_your_sitekey"></div> <button>Send</button> </form> - 2
Complete the widget and copy the token
Load the page, let the widget reach its success state, then read the hidden input in the browser console. The token is a long string starting
ey….// In your browser's console, after the widget shows its success state: document.querySelector('[name="edge-shield-response"]').value // Or via the JS API if you rendered the widget yourself: edgeShield.getResponse(widgetId) - 3
Verify it from a terminal
This is exactly the call a server would make. Your secret is also in Control → Shield. You have five minutes from the widget completing before the token expires.
# From a terminal on your own machine. Never put the secret in your page. curl -X POST https://shield.edge.network/siteverify \ -d "secret=es_secret_..." \ -d "response=PASTE_THE_TOKEN_HERE"You should see the token accepted, with a score and the hostname it was minted for:
{ "success": true, "score": 87, "challenge_ts": "2026-09-13T20:41:12.000Z", "hostname": "www.example.com", "error-codes": [] } - 4
Run it again
Repeat the same
curlwith the same token. It fails. Tokens are strictly single-use, which is what stops a bot replaying one good solve across a thousand submissions.{ "success": false, "score": 0, "hostname": "www.example.com", "error-codes": ["timeout-or-duplicate"] }
Don't want to touch real keys yet? Shield ships fixed test keys that work on any hostname, including a page opened straight from disk, and record no analytics:
<!-- Widget: always verifies, works on any hostname including localhost -->
<div class="edge-shield" data-sitekey="es_test_pass"></div>
# Terminal: always succeeds, score 90
curl -X POST https://shield.edge.network/siteverify \
-d "secret=es_secret_test_pass" \
-d "response=anything"
Swap to es_test_block to see the no-token path, or
es_secret_test_spent to see the replayed-token response.
Full list on Testing & CI.
Protecting a real form
Once you're happy the check works, the question becomes: where does the form post to? Whatever
answers that is where the siteverify
call goes. Cheapest first:
Your existing form service
If your forms already post to a hosted form backend, check whether it can call an HTTP endpoint
before accepting a submission, or supports a custom verification step. If it does, point that at
siteverify with your secret and forward the
edge-shield-response field. If it can't, it can't verify anything,
and the widget is decoration.
A small function or VM in front of the form
About fifteen lines in any runtime. A serverless function, or the smallest Edge Compute VM, receives the submission, verifies the token, and only then does whatever the form is for. Keep the secret in an environment variable, never in the page.
// The whole job of the function: verify, then do the thing.
// Any serverless runtime, or a tiny Edge Compute VM, is enough.
export default async function handler(req) {
const form = await req.formData()
const token = form.get('edge-shield-response')
const verify = await fetch('https://shield.edge.network/siteverify', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
secret: process.env.SHIELD_SECRET,
response: token ?? '',
remoteip: req.headers.get('x-forwarded-for') ?? ''
})
}).then(r => r.json())
if (!verify.success || verify.score < 60) {
return new Response('Verification failed', { status: 403 })
}
// Verified human. Email it, post it to a webhook, write it to storage...
await sendEmail({ to: 'hello@example.com', body: form.get('message') })
return Response.redirect('/thanks', 303)
} Offline verification, if you have a function but want zero round trips
Tokens are Ed25519-signed JWTs and the public key is published at a JWKS endpoint, so a function
can verify them locally with no call to Shield. You give up strict single-use, so keep
siteverify for signups, logins and payments. See
Offline Verification.
Never call siteverify from the page. Doing so means shipping your secret to every visitor, and a secret in a static bundle is public within the hour. The terminal test above works because your laptop isn't your website. The same call belongs on a server, a function, or a service you trust with the key, and nowhere else.
In short
-
The widget works on any static site with no configuration. That half is done. -
To see the check work, grab the token from the console and curlsiteverify from your terminal. -
To use the check, the form has to post somewhere that can hold the secret: a form service that supports it, or a small function or VM. -
Shield can't protect page views on a static site, and a widget with no server-side check protects nothing.