---
schema_version: "1"
id: 627f5eb9e7212eaa2f6113214ebfec0fb84bb3b90236972ea96e5b29396fc218
title: eCDN Turnstile
collection: developer
version: current
locale: en-us
portal_url: https://docs.salesforce.com/developer/current/en-us/commerce-cloud/commerce-api/guides/cdn-zones-turnstile.md
canonical_url: https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/cdn-zones-turnstile.html
source_site: developer.salesforce.com
content_type: documentation
content_hash: 1f9f7ddc76b41781ef898e43bc2f6efb2c5abdd82304b7998ae04211f9c69d4e
products:
  - Composable Storefront
  - Salesforce APIs
---
# eCDN Turnstile

Cloudflare Turnstile is a bot-mitigation solution that verifies that visitors are real without requiring them to solve a CAPTCHA. The eCDN Turnstile endpoints let you manage Turnstile widgets for your Commerce Cloud storefronts—creating, listing, updating, and deleting widgets programmatically.

Each widget is identified by a `sitekey` (the public key embedded in your storefront HTML) and has a corresponding `secret` (used server-side to validate challenge responses). The API returns the secret so that you can store it in your application's server-side configuration.

For general eCDN concepts and the zones API, see [CDN Zones](https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/cdn-zones.md).

## Widget Modes

A widget operates in one of three modes:

| Mode              | Behavior                                                                           |
| ----------------- | ---------------------------------------------------------------------------------- |
| `managed`         | Cloudflare determines the optimal challenge type (recommended for most use cases). |
| `non-interactive` | A non-interactive challenge runs automatically without user action.                |
| `invisible`       | The challenge runs entirely in the background with no visible widget rendered.     |

## Scope

Widgets are scoped to the realm and environment derived from the organization ID. For example, a `GET` request from `f_ecom_bcxj_prd` returns only production widgets for realm `bcxj`.

## Domains

Each widget is associated with one or more domains (hostnames) where it can be presented. Only requests that originate from a listed domain pass validation. Update the domain list as hostnames are added to or removed from your storefront.

## Authorization

All Turnstile endpoints require an OAuth 2.0 bearer token with the `sfcc.cdn-zones.rw` scope.

The Turnstile endpoints are relative to the following base URL:

`https://{shortCode}.api.commercecloud.salesforce.com/cdn/zones/v1/organizations/{organizationId}`

## List Turnstile Widgets

Retrieves all Turnstile widgets for the requesting instance. Results are paginated and cached for 5 minutes.

`GET /organizations/{organizationId}/turnstile/widgets`

Query parameters:

| Parameter | Required | Default | Description                          |
| --------- | -------- | ------- | ------------------------------------ |
| `name`    | No       | —       | Filter by widget name (exact match). |
| `offset`  | No       | `0`     | Pagination offset.                   |
| `limit`   | No       | `25`    | Results per page (1–50).             |

A `200 OK` response includes the following pagination headers:

- `SFDC-Pagination-Offset`
- `SFDC-Pagination-Limit`
- `SFDC-Pagination-Total-Count`
- `SFDC-Pagination-Result-Count`

Example response body:

```json
{
  "data": [
    {
      "sitekey": "0x4AAAAAAAAbCd1eFgHiJkLm",
      "name": "checkout-widget",
      "mode": "managed",
      "domains": ["shop.example.com", "www.example.com"],
      "bot_fight_mode": false,
      "region": "world",
      "offlabel": false,
      "clearance_level": "interactive",
      "created_on": "2026-01-15T12:00:00Z",
      "modified_on": "2026-02-20T08:30:00Z",
      "secret": "0x4AAAAAAAAbCd1eFgHiJkLm_secretvalue"
    }
  ]
}
```

## Create a Turnstile Widget

Creates a new Cloudflare Turnstile widget for the requesting instance. Each instance allows up to 5,000 widgets.

`POST /organizations/{organizationId}/turnstile/widgets`

Required attributes:

| Attribute | Type             | Description                                                     |
| --------- | ---------------- | --------------------------------------------------------------- |
| `name`    | string           | Logical widget name.                                            |
| `domains` | array of strings | Hostnames allowed to present the widget. At least one required. |
| `mode`    | string           | Widget mode: `managed`, `non-interactive`, or `invisible`.      |

Example request body:

```json
{
  "name": "checkout",
  "domains": ["shop.example.com", "www.example.com"],
  "mode": "managed"
}
```

A `201 Created` response returns the new widget, including its `secret`:

```json
{
  "data": {
    "sitekey": "0x4AAAAAAAAbCd1eFgHiJkLm",
    "name": "checkout-widget",
    "mode": "managed",
    "domains": ["shop.example.com", "www.example.com"],
    "bot_fight_mode": false,
    "region": "world",
    "offlabel": false,
    "clearance_level": "interactive",
    "created_on": "2026-01-15T12:00:00Z",
    "modified_on": "2026-01-15T12:00:00Z",
    "secret": "0x4AAAAAAAAbCd1eFgHiJkLm_secretvalue"
  }
}
```

**note:** The response includes the `secret`. Store it securely in your server-side configuration—you need it to validate Turnstile challenge responses.

## Get a Turnstile Widget

Retrieves the full configuration of a widget by sitekey, including the shared secret.

`GET /organizations/{organizationId}/turnstile/widgets/{sitekey}`

Path parameters:

| Parameter | Description                          |
| --------- | ------------------------------------ |
| `sitekey` | The unique identifier of the widget. |

A `200 OK` response returns the widget. The endpoint returns `404 Not Found` if the widget doesn't exist.

```json
{
  "data": {
    "sitekey": "0x4AAAAAAAAbCd1eFgHiJkLm",
    "name": "checkout-widget",
    "mode": "managed",
    "domains": ["shop.example.com", "www.example.com"],
    "bot_fight_mode": false,
    "region": "world",
    "offlabel": false,
    "clearance_level": "interactive",
    "created_on": "2026-01-15T12:00:00Z",
    "modified_on": "2026-02-20T08:30:00Z",
    "secret": "0x4AAAAAAAAbCd1eFgHiJkLm_secretvalue"
  }
}
```

## Update a Turnstile Widget

Updates the domains and/or mode of an existing widget.

`PUT /organizations/{organizationId}/turnstile/widgets/{sitekey}`

Path parameters:

| Parameter | Description                                    |
| --------- | ---------------------------------------------- |
| `sitekey` | The unique identifier of the widget to update. |

Optional attributes (at least one required):

| Attribute | Type             | Description                                                    |
| --------- | ---------------- | -------------------------------------------------------------- |
| `domains` | array of strings | Replacement list of allowed hostnames.                         |
| `mode`    | string           | New widget mode: `managed`, `non-interactive`, or `invisible`. |

Example request body:

```json
{
  "domains": ["shop.example.com", "www.example.com", "checkout.example.com"],
  "mode": "invisible"
}
```

A `200 OK` response returns the updated widget. The endpoint returns `404 Not Found` if the widget doesn't exist, and `400 Bad Request` if the request exceeds the Cloudflare hostname limit for the widget.

## Delete a Turnstile Widget

Deletes an existing widget. This operation is irreversible and immediately invalidates the sitekey and secret.

`DELETE /organizations/{organizationId}/turnstile/widgets/{sitekey}`

Path parameters:

| Parameter | Description                                    |
| --------- | ---------------------------------------------- |
| `sitekey` | The unique identifier of the widget to delete. |

Query parameters:

| Parameter         | Required | Default | Description                                                                                                            |
| ----------------- | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------- |
| `forceProduction` | No       | `false` | Must be `true` to delete a production widget. Without this flag, deleting a production widget returns `403 Forbidden`. |
| `dryRun`          | No       | `false` | When `true`, validates the request without performing the deletion.                                                    |

A successful deletion returns `204 No Content`. The endpoint returns `404 Not Found` if the widget doesn't exist, and `403 Forbidden` if you attempt to delete a production widget without `forceProduction=true`.

**note:** Production widgets (environment = `prd`) require an explicit `forceProduction=true`. Use `dryRun=true` to validate parameters before committing to deletion. Post-deletion verification runs automatically to confirm removal.

## Error Responses

All Turnstile endpoints return errors in `application/problem+json` format:

```json
{
  "title": "Short description of the error",
  "type": "ErrorType",
  "detail": "Additional context about what went wrong"
}
```

| Status Code | Meaning                                                                       |
| ----------- | ----------------------------------------------------------------------------- |
| `400`       | Bad request—invalid input, name too long, hostname limit exceeded, and so on. |
| `401`       | Unauthorized—missing or invalid bearer token.                                 |
| `403`       | Forbidden—insufficient scope, or production deletion without the force flag.  |
| `404`       | Not found—a widget with the given sitekey doesn't exist.                      |
| `500`       | Internal server error.                                                        |

## Integration Workflow

A typical workflow:

1. **Create a widget.** Call `POST /turnstile/widgets` with your desired name, domains, and mode.
2. **Configure your storefront.** Add the `sitekey` and `secret` from the response to your Managed Runtime application's `.env` file. For the exact environment variables and configuration, see steps 2(a) and 2(b) in the [Storefront Next One-Click Checkout guide](https://developer.salesforce.com/docs/commerce/storefront-next/guide/sfnext-one-click-checkout.html).
3. **Update domains.** As you add or remove storefront hostnames, call `PUT /turnstile/widgets/{sitekey}` to keep the domain list in sync.

## Limitations and Considerations

- Each instance allows up to 5,000 widgets.
- Each widget's domain list is subject to Cloudflare's per-widget hostname limit.
- List results are cached for 5 minutes, so newly created widgets can take up to 5 minutes to appear in list responses. To retrieve a new widget right away, use `GET /turnstile/widgets/{sitekey}`, which isn't cached.
- You can't change the `name` after creation. To rename a widget, delete and recreate it.
- Each environment (development, staging, production) has its own widgets. Widgets can't be shared across environments.
- If you delete a widget that is still in use, its sitekey and secret are immediately invalidated, and any Turnstile challenges that use that sitekey fail. Remove the widget HTML from your storefront before, or immediately after, deletion.

To choose a mode, use this guidance:

- Use `managed` (recommended) for most storefronts—Cloudflare picks the optimal challenge.
- Use `invisible` for a seamless experience where no widget is shown.
- Use `non-interactive` when you want the challenge to run automatically without a visible puzzle but still show the widget.
