# Test Cloudflare Turnstile in CI With Testing Sitekeys

> Test your own Turnstile forms in CI with Cloudflare's testing sitekeys and secret keys: always pass, always fail, forced challenges, and spent tokens.

- Source: https://zerocaptcha.io/guides/test-cloudflare-turnstile-in-ci
- Published: 2026-09-30
- Updated: 2026-10-01
- Author: ZeroCaptcha Engineering

If Turnstile protects forms on your own site, your end-to-end tests have to get past it too. You
do not need a solving service for that. Cloudflare publishes testing sitekeys and secret keys
that make the widget and the server-side check behave predictably, so tests can cover success and
failure without depending on a real challenge. This guide lists them and shows how to wire them
into a test setup.

These keys are Cloudflare's, documented on its
[testing page](https://developers.cloudflare.com/turnstile/troubleshooting/testing/). They are the
right tool whenever you own the site.

## The testing sitekeys (client side)

Put one of these in the widget's `data-sitekey`, or in `sitekey` for `turnstile.render()`:

| Sitekey | Behavior | Visibility |
| --- | --- | --- |
| `1x00000000000000000000AA` | Always passes | Visible |
| `2x00000000000000000000AB` | Always fails | Visible |
| `1x00000000000000000000BB` | Always passes | Invisible |
| `2x00000000000000000000BB` | Always fails | Invisible |
| `3x00000000000000000000FF` | Forces an interactive challenge | Visible |

With a passing sitekey, the widget produces a dummy token straight away and fills the
`cf-turnstile-response` field, so your test can submit the form as a person would.

## The testing secret keys (server side)

Configure one of these as your server's Turnstile secret, the value it sends to siteverify:

| Secret key | siteverify answers |
| --- | --- |
| `1x0000000000000000000000000000000AA` | Always passes |
| `2x0000000000000000000000000000000AA` | Always fails |
| `3x0000000000000000000000000000000AA` | Fails as a token that was already spent |

Pair a sitekey and a secret key to make the combination you want to test: a passing widget with
a failing secret, for example, checks that your server really refuses a bad token instead of
trusting the browser.

The [Cloudflare Turnstile test sitekeys page](https://zerocaptcha.io/captcha-test/cloudflare-turnstile-test-sitekeys)
renders each sitekey live, and runs siteverify with each secret key, so you can see what your tests
will meet before you write them.

## Wire them in by environment

Keep the real keys out of test environments entirely. Read both values from configuration, and
set the testing ones in CI:

```sh
# CI and local development
TURNSTILE_SITEKEY=1x00000000000000000000AA
TURNSTILE_SECRET=1x0000000000000000000000000000000AA
```

Render the sitekey from that setting:

```text
<div class="cf-turnstile" data-sitekey="{{ TURNSTILE_SITEKEY }}"></div>
```

And verify with the secret from the same place:

```python
import os, requests

def verify(token: str, remote_ip: str | None = None) -> bool:
    reply = requests.post(
        "https://challenges.cloudflare.com/turnstile/v0/siteverify",
        data={"secret": os.environ["TURNSTILE_SECRET"], "response": token, "remoteip": remote_ip},
        timeout=10,
    ).json()
    return reply.get("success") is True
```

A production deploy that still carries a testing key accepts everyone, so add a startup check
that refuses any key starting with `1x`, `2x` or `3x` when the environment is production.

## Test cases worth having

1. **Happy path**: sitekey `1x…AA`, secret `1x…AA`. The form submits and your server accepts it.
2. **Rejected token**: sitekey `1x…AA`, secret `2x…AA`. Your server refuses the submission and
   shows the visitor a useful message.
3. **Spent token**: secret `3x…AA`. Your server treats `timeout-or-duplicate` as a failure and
   asks for a new challenge instead of retrying the same token. See
   [Cloudflare Turnstile token expiry](https://zerocaptcha.io/guides/cloudflare-turnstile-token-expiry) for why tokens are single-use.
4. **Blocked visitor**: sitekey `2x…AB`. Your form stays disabled and says why.
5. **Interactive challenge**: sitekey `3x…FF`. Your layout still works when the widget asks for a
   click.
6. **Invisible widget**: sitekeys `1x…BB` and `2x…BB`, if you use invisible mode. See
   [Cloudflare Turnstile widget modes](https://zerocaptcha.io/guides/cloudflare-turnstile-widget-modes).

## When a solving API is the right tool instead

Testing keys only work on sites whose configuration you control. When you automate a page you do
not run, such as a partner's portal you have permission to use or a public page whose terms allow
it, the widget uses its real sitekey, and a token must be solved. That is what the
[Cloudflare Turnstile solver](https://zerocaptcha.io/cloudflare-turnstile-solver) is for: a task with the page URL and sitekey returns a real
token, charged only when it is solved.

Keep the two apart. Use Cloudflare's keys for your own CI, and use a solving API only against
sites you are allowed to automate under the [Acceptable Use Policy](https://zerocaptcha.io/legal/acceptable-use).

## Questions

### Do I need a CAPTCHA solver to test my own Cloudflare Turnstile forms?

No. Cloudflare's testing sitekeys and secret keys make the widget and the server check pass or fail on demand, so end-to-end tests of your own site need no solver.

### Can the testing keys be used in production?

No. They accept every visitor, or reject every visitor, so they must only ever be configured in development, CI and staging.

### What does the 3x secret key test?

It makes siteverify answer as if the token had already been used, so you can check how your server handles the timeout-or-duplicate case.
