SolveGate solves Cloudflare Turnstile through one REST call. POST the sitekey and the page it sits on; a successful solve returns a single-use token to submit as cf-turnstile-response.
// failed solves are never billed · credits never expire · first 1,000 solves free
// statistics use cached, retained solve records, including sandbox responses. Provider averages exclude queue wait and previous retries; request times vary.
Cloudflare Turnstile is a challenge widget that can be embedded on a website without routing its traffic through Cloudflare. It has three widget modes, all supported by SolveGate.
An adaptive widget. Cloudflare decides whether to ask for a checkbox interaction based on its risk signals.
A visible widget that never asks the visitor to click. It still issues a token that the origin verifies.
No visible widget. The challenge runs in the background and returns a token through the configured callback or response field.
The mode changes what the visitor sees, not what you send. All three take the same request: gate, sitekey, url. There is no mode parameter, because the sitekey already determines it.
Turnstile WAF challenge pages — the full-page interstitial Cloudflare serves before the site itself — are a different gate. They have their own page.
Before you solve anything, confirm it is Turnstile and not one of the things it gets mistaken for. Look for these integration signals, then confirm which challenge the active page is serving:
| Signal | What to look for |
|---|---|
| The markup | An element carrying class cf-turnstile, with the public key in its data-sitekey attribute. Read the key from the actual widget; test keys use other prefixes. |
| The script | challenges.cloudflare.com/turnstile/v0/api.js in the page's script tags. Its presence indicates a Turnstile integration, but does not prove a widget is currently active. |
| The hidden input | A field named cf-turnstile-response inside the form the widget guards. This is the default response field; the page can rename or disable it. |
| Explicit render | No data-sitekey anywhere, but a turnstile.render() call in the page's own JavaScript with the key passed as an argument. Read it out of the call. |
If instead the page itself is replaced by a holding screen — no form, no widget you can submit — that is a WAF challenge and a different gate value. If the widget belongs to another vendor entirely, none of the four signals will be present.
data-sitekey attribute on the widget, or the sitekey argument passed to turnstile.render().cf-turnstile-response.curl https://api.solvegate.io/v1/solve \
-H "Authorization: Bearer sk_live_•••" \
-d gate=turnstile \
-d sitekey=0x4AAAAAAAAA_target \
-d url=https://app.example.com{
"id": "slv_8Kd2…aF9",
"status": "solved",
"gate": "turnstile",
"token": "0.Xa3f…X9_cleared",
"solve_ms": 980,
"billed": true
}| Field | Type | Description |
|---|---|---|
| gate | string | Required. Which challenge stands in the way — turnstile or waf. |
| sitekey | string | Required. Actual widget sitekey for turnstile; use the non-empty placeholder waf for the waf gate. |
| url | string | Required. The page the challenge is on. The token is issued for that origin. |
| action | string | Optional. The Turnstile action value, when the site sets one. |
| async | boolean | Optional. Return a pending solve immediately and poll GET /v1/solve/{id} instead of holding the connection. |
| proxy | string | Optional. Solve through your own egress IP instead of ours. |
The call above waits for the solve result until the HTTP wait limit. Send async: true and it returns immediately with a pending solve instead, which you poll. Use it when you are solving many gates at once and would rather not hold a connection per solve.
{
"id": "slv_8Kd2…aF9",
"status": "pending",
"token": null,
"solve_ms": null
}{
"id": "slv_8Kd2…aF9",
"status": "solved",
"token": "0.Xa3f…X9_cleared",
"solve_ms": 980,
"expires_at": 1787620000,
"billed": true
}Poll the id until status leaves pending. Retrieval is free and never re-bills the solve, so polling costs nothing but the request.
Send a unique Idempotency-Key for each logical solve and reuse it with the same body after a lost response. An HTTP timeout does not cancel queued work; retrying without the original key can create another billable solve. See the solve reference.
| Field | Type | Description |
|---|---|---|
| id | string | The solve, prefixed slv_. Pass it to GET /v1/solve/{id} to retrieve. |
| status | string | solved, pending or failed. |
| gate | string | Which gate was cleared — turnstile or waf. |
| token | string | Turnstile token or JSON-encoded WAF cookies/headers. Null until status is solved. |
| solve_ms | integer | Final provider attempt duration; excludes queue wait, previous retries and persistence. |
| expires_at | integer | SolveGate's estimated use-by timestamp; not an expiry confirmed by Cloudflare. |
| mode | string | live for a real solve, sandbox for the deterministic test response an sk_test_ key returns. |
| billed | boolean | Whether this solve consumed a credit. Only successful solves are billed. |
| meter | string | credits, pass or sandbox — which meter paid for it. |
| error_code | string | Null unless status is failed. See the table below. |
| error_message | string | Human-readable explanation of error_code. |
Failures are typed, and the ones that are our fault or the network's are not billed. That is the column worth reading: a failure mode that costs money is one you have to engineer around.
| Status | Code | Meaning |
|---|---|---|
| 401 | invalid_key | Missing or revoked API key. |
| 402 | balance_empty | Out of credit. Top up from the dashboard. |
| 402 | pass_expired | The unlimited pass this key is attached to has ended. Expiry is a hard stop — we never quietly spend your credit balance instead. |
| 403 | forbidden_target | This target host is not permitted by the acceptable-use policy. |
| 422 | unknown_sitekey | The sitekey/url pair did not resolve to a live gate. Usually a stale sitekey, or the wrong url for it. |
| 429 | rate_limited | Above the key or shared pass rate limit. Honor Retry-After before retrying. |
| 502 | user_proxy_error | The proxy you supplied refused or dropped the connection. Check its credentials, allowlist and egress region — or omit proxy to use our pool. — not billed |
| 503 | target_unavailable | Too many recent solves for this target failed, so attempts are paused briefly. — not billed |
| 503 | maintenance | Solving is paused for platform maintenance. — not billed |
| 504 | solve_timeout | The wait or solve timed out. Queued work can still finish; retry with the same Idempotency-Key and body. — check final status |
Full reference is in the API documentation, and every code is listed on the error page.
A token is only useful in the place the site expects it. Where that is depends on how the widget was rendered:
| Case | What to do with it |
|---|---|
| Implicit render | Set the hidden cf-turnstile-response input on the guarded form, then submit the form the way a visitor would. |
| Explicit render | Pass the token to the callback given to turnstile.render(). The hidden input may not exist at all in this mode. |
| A JSON API | Some sites never submit a form — their own JavaScript posts the token in a request body under a name they chose. Watch the XHR the widget triggers and copy that field name. |
Turnstile tokens are single-use and valid for 300 seconds after generation. Request one when you need it. Cloudflare returns timeout-or-duplicate for an expired or already validated token; inspect Siteverify error codes when debugging a rejection.
Four limits worth knowing before you build against this, because each one looks like a bug in your code when you hit it unprepared.
Send the URL where the widget runs. The target can check hostname, action and other context in its Siteverify result; a returned token does not bypass those checks.
The turnstile gate returns a widget token. Pre-clearance can additionally issue a cookie on a site configured for it, but do not assume a token response includes that cookie. The waf gate returns a separate clearance payload.
If the origin also runs a second vendor's bot management, or rate-limits by IP, a valid token does not address either.
Where a site ties the challenge to a cookie issued at page load, the solve has to share that context. Send your own proxy so the solve and the submission leave from the same address.
Credits are prepaid in packs and one credit is one successful solve. Bigger packs carry a lower effective rate; nothing is metered in arrears and nothing auto-renews.
| Pack | Price | Per 1,000 solves |
|---|---|---|
| 30k credits | $12 | $0.40 |
| 100k credits | $25 | $0.25 |
| 250k credits | $60 | $0.24 |
| 500k credits | $100 | $0.20 |
| 1M credits | $150 | $0.15 |
| 5M credits | $600 | $0.12 |
| 10M credits | $1,000 | $0.10 |
| 20M credits | $1,500 | $0.075 |
Failed solves, timeouts and our internal retries are never billed. Credits do not expire. Full breakdown on the pricing page.
A service that completes a Turnstile challenge on your behalf and returns the token the widget would have produced. You send the sitekey and the URL the widget is on; you get back a cf-turnstile-response value to submit with your form or request. On the credit meter, SolveGate charges one credit when it stores a successful provider result. The target still decides whether to accept the token.
It is public and sits in the widget's markup — look for the element with class cf-turnstile and read its data-sitekey attribute, or inspect the sitekey argument passed to turnstile.render(). That is the value you pass as sitekey.
Into the hidden input named cf-turnstile-response inside the form the widget guards, then submit the form normally. If the site uses an explicit-render callback, pass the token to that callback instead. Tokens are single-use and short-lived, so request one at the moment you need it rather than caching.
Time varies by target, provider, proxy and retries. The average shown here measures the final provider attempt, not total request time. The default call waits for a result; async calls return an id to poll until status leaves pending. Stop on solved or failed.
Credits are prepaid in packs. The smallest pack works out at $0.40 per 1,000 solves and the largest at $0.075 per 1,000, with every pack in between priced on that curve. Credits do not expire. Terminal failed solves and individual internal retries do not consume credits. A successful provider result costs one credit; an HTTP wait timeout can still be followed by a successful, billable result.
You may use SolveGate only against properties you own or are explicitly authorized to test or automate — your own sites, staging environments, uptime and QA checks, and engagements where the owner has granted permission. Using it against third parties in breach of their terms is prohibited by our Acceptable Use Policy and we enforce it.
No. Tokens are single-use and expire 300 seconds after they are issued — siteverify returns timeout-or-duplicate for a second attempt at the same token, and for one presented after the window. Request a token when you are ready to use it and inspect the Siteverify error code if the target rejects it.
Yes, and the call is the same. Managed, non-interactive and invisible differ in what the visitor sees, not in what you send — there is no mode parameter, because the sitekey already determines the mode. The only practical difference is where you find the sitekey: an invisible widget often has no data-sitekey attribute and the key is passed to turnstile.render() in the page's own JavaScript instead.
A terminal failed solve returns error_code and consumes no credit. Correct a bad sitekey or proxy before retrying. A network or HTTP wait timeout does not cancel an in-flight solve; reuse the same Idempotency-Key and request body to recover its status without creating another solve. Without that key, a retry can be another billable request.
Send a proxy URL on the request and the solve egresses through it instead of our pool. Worth doing when the site ties the challenge to a session or scores the requesting IP, because then the solve and the request that uses the token leave from the same address. If your proxy refuses the connection you get user_proxy_error, which is not billed.
The token is a real token issued by Cloudflare for that sitekey, and siteverify validates it the same way it validates one a browser earned. What a site can still see is everything around the token — the IP it arrives from, the headers, the timing, the request pattern. Those are your side of the integration, not something a token fixes, which is why the proxy parameter exists.