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)

HTML
<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)

HTML
<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>
TurnstileAgentGateNotes
class="cf-turnstile"class="agentgate"
data-sitekeydata-sitekey
data-actiondata-actionrequired; [a-z0-9_]{1,64}, configured on the site
data-languagedata-langen, es, fr, de, pt, hi, ja, zh
widget mode (managed, non-interactive, invisible) and data-appearancedata-mode: managed, invisible, interactiveset per widget, not in a dashboard
data-callback, data-error-callback, data-expired-callbackcallback, errorCallback, expiredCallback options of AgentGate.render()the declarative widget has no callback attributes
data-response-field-name (default cf-turnstile-response)fixed: agentgate_tokenan existing field with that name is reused
data-refresh-expiredautomaticthe token is cleared before expiry and the widget verifies again at submit
data-execution="execute"alwaysthe widget verifies when the form is submitted
data-theme, data-sizenonethe widget uses system colours
data-cdatanonekeep your own data server-side
data-timeout-callbackerrorCallback with code timeout

JavaScript API:

TurnstileAgentGate
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=cbcall 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)

JavaScript
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)

JavaScript
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 requestAgentGate requestNotes
secretAuthorization: Bearer ags_… (or secret)
responsetoken
remoteipnoneAgentGate saw the client itself
idempotency_keyrequestIda retry with the same ID returns the same success
form-encoded or JSONJSON onlysend Content-Type: application/json
check action yourselfexpectedAction (required)AgentGate refuses a mismatch with action_mismatch
check hostname yourselfexpectedOrigina full origin, https://host[:port]
Turnstile responseAgentGate response
successsuccess
challenge_ts (ISO time)issuedAt (Unix milliseconds)
hostnameorigin
actionaction
cdatanone
error-codes (array)error (one code)
Turnstile errorAgentGate error
missing-input-secret, invalid-input-secretinvalid_credential (401)
missing-input-response, bad-requestinvalid_request (400)
invalid-input-responseinvalid_token (422)
timeout-or-duplicateexpired_token (422) or token_used (409)
internal-errorunavailable (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 wouldDecision shows 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_interactive and the secrets ags_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.

View as Markdown