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
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:
{
"success": true,
"score": 92,
"challenge_ts": "2026-07-21T08:00:00.000Z",
"hostname": "example.com",
"error-codes": []
} Failure:
{
"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 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 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 — low, because it's
automation — and the agent
field is how you route:
{
"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 —
nothing is ever blocked — and instead the response carries what strict enforcement
would have decided:
{
"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.
When the numbers look right, switch shadow mode off — no code changes needed, since a
correct integration already checks success.
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.
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.
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 — human motor noise raises the score, machine-perfect input lowers it — not just the load-time environment.
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.