Migrate from Turnstile
Replace a Cloudflare Turnstile widget and its siteverify call with AgentGate, attribute by attribute and field by field.
AgentGate's widget and server check follow the same shape as Turnstile: a script, an element with a site key inside your form, a token in a hidden field, and a server-side call that redeems it once. Most migrations change a handful of lines.
Note
Turnstile names below are from Cloudflare's public documentation. Check them against the version you use.
1. Create a site
Add your site in the console. You get a site key (site_…) and a backend secret (ags_…). Configure every origin that serves the form, and an action for each form: unlike Turnstile, AgentGate requires an action and checks it at validation.
2. Swap the widget
Before (Turnstile)
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<form method="post" action="/signup">
<div class="cf-turnstile" data-sitekey="0x4AAA…" data-action="signup" data-theme="light"></div>
<button>Sign up</button>
</form>After (AgentGate)
<script src="https://agentgate.example/sdk/v1/agentgate.js" async></script>
<form method="post" action="/signup">
<div class="agentgate" data-sitekey="site_…" data-action="signup" data-mode="managed"></div>
<button>Sign up</button>
</form>| Turnstile | AgentGate | Notes |
|---|---|---|
class="cf-turnstile" | class="agentgate" | |
data-sitekey | data-sitekey | |
data-action | data-action | required; [a-z0-9_]{1,64}, configured on the site |
data-language | data-lang | en, es, fr, de, pt, hi, ja, zh |
widget mode (managed, non-interactive, invisible) and data-appearance | data-mode: managed, invisible, interactive | set per widget, not in a dashboard |
data-callback, data-error-callback, data-expired-callback | callback, errorCallback, expiredCallback options of AgentGate.render() | the declarative widget has no callback attributes |
data-response-field-name (default cf-turnstile-response) | fixed: agentgate_token | an existing field with that name is reused |
data-refresh-expired | automatic | the token is cleared before expiry and the widget verifies again at submit |
data-execution="execute" | always | the widget verifies when the form is submitted |
data-theme, data-size | none | the widget uses system colours |
data-cdata | none | keep your own data server-side |
data-timeout-callback | errorCallback with code timeout |
JavaScript API:
| Turnstile | AgentGate |
|---|---|
turnstile.render(el, opts) | AgentGate.render(el, opts) |
turnstile.execute(el) | await AgentGate.execute({action}): resolves with the token directly |
turnstile.getResponse(id) | AgentGate.getResponse(id) |
turnstile.reset(id) | AgentGate.reset(id) |
turnstile.remove(id) | AgentGate.remove(id) |
?render=explicit&onload=cb | call AgentGate.init() / render() after the script loads |
If your page sets a CSP, replace challenges.cloudflare.com with your AgentGate endpoint in script-src and connect-src, and add worker-src blob: (details). AgentGate uses no iframe, so frame-src needs nothing.
3. Swap the server call
Before (Turnstile)
const form = new URLSearchParams({ secret: process.env.TURNSTILE_SECRET,
response: body["cf-turnstile-response"], remoteip: ip, idempotency_key: opId });
const r = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", { method: "POST", body: form });
const v = await r.json();
if (!v.success || v.action !== "signup") return reject(v["error-codes"]);After (AgentGate)
const r = await fetch("https://agentgate.example/v1/siteverify", {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: "Bearer " + process.env.AGENTGATE_SECRET },
body: JSON.stringify({ token: body.agentgate_token, expectedAction: "signup",
expectedOrigin: "https://app.example.com", requestId: opId }),
});
const v = await r.json();
if (!v.success) return reject(v.error);| Turnstile request | AgentGate request | Notes |
|---|---|---|
secret | Authorization: Bearer ags_… (or secret) | |
response | token | |
remoteip | none | AgentGate saw the client itself |
idempotency_key | requestId | a retry with the same ID returns the same success |
| form-encoded or JSON | JSON only | send Content-Type: application/json |
check action yourself | expectedAction (required) | AgentGate refuses a mismatch with action_mismatch |
check hostname yourself | expectedOrigin | a full origin, https://host[:port] |
| Turnstile response | AgentGate response |
|---|---|
success | success |
challenge_ts (ISO time) | issuedAt (Unix milliseconds) |
hostname | origin |
action | action |
cdata | none |
error-codes (array) | error (one code) |
| Turnstile error | AgentGate error |
|---|---|
missing-input-secret, invalid-input-secret | invalid_credential (401) |
missing-input-response, bad-request | invalid_request (400) |
invalid-input-response | invalid_token (422) |
timeout-or-duplicate | expired_token (422) or token_used (409) |
internal-error | unavailable (503) |
4. Differences to plan for
- Token lifetime. Two minutes by default (Turnstile: five). Verify at submit time; the widget already does.
- Pre-clearance. Turnstile's pre-clearance cookie corresponds to AgentGate clearances, accepted by the AgentGate gateway on clearance actions.
- Monitor mode. New sites start in monitor mode: every valid token passes and
wouldDecisionshows what enforce would have done. Switch to enforce when you are satisfied. - Testing keys. Turnstile's dummy keys map to
site_test_always_pass,site_test_always_fail,site_test_force_interactiveand the secretsags_test_always_pass,ags_test_always_fail,ags_test_token_spent(Testing). - Agents. AgentGate also admits AI agents that sign their requests; see Signed agents.