---
title: "Server-side validation"
description: "Validate Shield tokens with the siteverify API: Turnstile-compatible responses, error codes, and how to use the 1–100 humanity score."
url: "https://edge.network/docs/shield/siteverify"
section: "Shield"
---

# Server-side validation

Every token must be validated from your server with the siteverify endpoint. The response shape is compatible with Cloudflare Turnstile, plus a humanity score.

## The endpoint

```http
POST https://shield.edge.network/siteverify
Content-Type: application/x-www-form-urlencoded

secret=es_secret_...&response=<token>
```

Both `application/x-www-form-urlencoded` and `application/json` bodies are accepted.

| Parameter | Required | Description |
|---|---|---|
| `secret` | Yes | Your widget's secret key (`es_secret_…`) |
| `response` | Yes | The token from the `edge-shield-response` form input |
| `remoteip` | No | The submitting visitor's IP address. Recommended: tokens are bound to the IP they were issued to, so passing this rejects tokens farmed on one machine and spent from another |

## Responses

Success:

```json
{
  "success": true,
  "score": 92,
  "challenge_ts": "2026-07-21T08:00:00.000Z",
  "hostname": "example.com",
  "error-codes": []
}
```

Failure:

```json
{
  "success": false,
  "error-codes": ["timeout-or-duplicate"]
}
```

| Field | Description |
|---|---|
| `success` | Whether the token is genuine, unexpired, unused, and matches your widget |
| `score` | Humanity score, 1–100. Not present on failures. *(Shield extension, not in Turnstile)* |
| `challenge_ts` | ISO 8601 timestamp of when the token was issued |
| `hostname` | The hostname the widget ran on |
| `agent` | Present when the token belongs to a [verified agent](/docs/shield/verified-agents) that declared its identity. *(Shield extension, not in Turnstile)* |
| `shadow` | Present only for widgets in shadow (report-only) mode. See below. *(Shield extension, not in Turnstile)* |
| `error-codes` | Machine-readable failure reasons (see below) |

## Error codes

| Code | Meaning |
|---|---|
| `missing-input-secret` | No secret was supplied |
| `invalid-input-secret` | The secret doesn't match any active widget |
| `missing-input-response` | No token was supplied |
| `invalid-input-response` | The token is malformed, expired, from a different widget, or from a disallowed hostname |
| `timeout-or-duplicate` | The token was already used. Tokens are strictly single-use |
| `agent-blocked` | A verified agent was rejected because the widget's agent policy is set to Block |

## Verified agent tokens

When automation cryptographically declares its identity (via [Web Bot Auth](/docs/shield/verified-agents) or a confirmed crawler check) and your widget's agent policy allows it, the token carries the verified identity. The score stays an honest humanity estimate, which is low because it's automation. Use the `agent` field to route:

```json
{
  "success": true,
  "score": 8,
  "agent": { "domain": "agent.bot.goog", "verified": true, "method": "web-bot-auth" },
  "challenge_ts": "2026-07-21T08:00:00.000Z",
  "hostname": "example.com",
  "error-codes": []
}
```

Check `agent.verified` before trusting a low score: a verified agent is declared automation you may want to serve, while a low score with no agent field is automation that refused to identify itself.

## Shadow (report-only) mode

A widget can be switched to **shadow mode** in the dashboard (**Shield → widget → Settings**). In shadow mode siteverify **never returns `success: false`**, so nothing is ever blocked. Instead, the response carries what strict enforcement would have decided:

```json
{
  "success": true,
  "shadow": {
    "enabled": true,
    "enforced": false,
    "would_succeed": false,
    "error-codes": ["invalid-input-response"]
  },
  "error-codes": []
}
```

Log `shadow.would_succeed` and `shadow["error-codes"]` during the trial; would-fail and would-escalate counts also accumulate in your widget's [analytics](/docs/shield/analytics). When the numbers look right, switch shadow mode off. No code changes are needed, since a correct integration already checks `success`.

> [!NOTE]
> Shadow mode is the risk-free way to trial Shield on production traffic: real visitors can't be inconvenienced, and you get a fully quantified preview of what enforcement would have done.

## Testing your integration

Fixed test secrets (`es_secret_test_pass`, `es_secret_test_fail`, `es_secret_test_spent`) return deterministic responses so your test suite can exercise every branch without real keys. See [Testing & CI](/docs/shield/testing).

## Using the humanity score

The score expresses confidence: **1 is confirmed automation, 100 is a confirmed human**. "Not human" doesn't have to mean "not welcome". A legitimate AI agent acting for a customer will score low, and you may prefer to route it to your API rather than block it.

```js
const { success, score } = await siteverify(token)

if (!success) reject()          // invalid, expired, or replayed token
else if (score >= 70) allow()   // confident human
else if (score >= 40) stepUp()  // uncertain: e.g. require email confirmation
else routeOrBlock()             // automation: block, or redirect to your API
```

The thresholds are yours to tune. As reference points: in **managed mode**, background attempts scoring below 40 are automatically escalated to an interactive confirmation before a token is issued, so tokens arriving at your server from managed widgets already had a floor applied.

The score you receive is also the freshest available: the widget verifies at page load, then re-mints its token with a behaviour-informed score once the visitor has actually typed or moved the pointer. By the time the form is submitted, the token usually reflects real interaction as well as the load-time environment. Human motor noise raises the score; machine-perfect input lowers it.

> [!NOTE]
> Token lifetime is **5 minutes** and each token is valid for exactly one siteverify call. Validate at the moment of form processing, not ahead of time.

## Next steps

- [Offline verification](/docs/shield/offline-verification) — Verify tokens locally via JWKS with zero network calls
- [Migrating from Turnstile](/docs/shield/turnstile-migration) — Same response shape, so you can swap and go
