---
title: "Shield on a Static Site (No Server)"
description: "You've embedded the Shield widget on a static site and hit 'validate server-side'. Here's how to test the check from your laptop, what Shield can and can't protect without a backend, and the smallest thing you can add to protect a real form."
url: https://edge.network/docs/shield/static-sites/
---

# Shield on a Static Site (No Server)

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](https://edge.network/console/shield). The form doesn't need a real `action` yet. ```
<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 `curl` with 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](https://edge.network/docs/shield/testing).

## 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](https://edge.network/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](https://edge.network/docs/shield/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 `curl` siteverify 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.

## Next Steps

[Server-Side Validation The siteverify API, error codes and the humanity score](https://edge.network/docs/shield/siteverify) [Testing & CI Fixed test keys, deterministic behaviour](https://edge.network/docs/shield/testing)
[Back to Docs](https://edge.network/docs) [Need help?](https://edge.network/support)
