---
title: "Shield on a static site"
description: "Using Shield on a static site with no backend: test the siteverify check from your laptop, see what Shield can protect, and add the smallest server-side step."
url: "https://edge.network/docs/shield/static-sites"
section: "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.

> [!NOTE]
> **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](/console/shield). The form doesn't need a real `action` yet.

```html
<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…`.

```js
// 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.

```bash
# 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:

```json
{
  "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.

```json
{
  "success": false,
  "score": 0,
  "hostname": "www.example.com",
  "error-codes": ["timeout-or-duplicate"]
}
```

### Using test keys instead

If you'd rather not use real keys yet, Shield ships fixed test keys that work on any hostname, including a page opened straight from disk, and record no analytics:

```html
<!-- Widget: always verifies, works on any hostname including localhost -->
<div class="edge-shield" data-sitekey="es_test_pass"></div>
```

```bash
# 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](/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](/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.

```js
// 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](/docs/shield/offline-verification).

> [!CAUTION]
> **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](/docs/shield/siteverify) — The siteverify API, error codes and the humanity score
- [Testing & CI](/docs/shield/testing) — Fixed test keys, deterministic behaviour
