---
title: "Proof-of-work challenge served by your own app · camada"
description: "How a camada challenge works: a first-party proof-of-work page your SDK serves to suspect browsers, the _cch cookie, API signals and SDK support."
url: https://www.camada.app/docs/challenge
last_updated: 2026-10-26
---

Docs

# Challenge

A challenge is the verdict between letting a request through and blocking it: make the client prove it is a browser. Where your SDK can serve one, a suspicious request gets a small first-party page instead of your app. A real browser passes it in well under a second and carries on; a headless client that does not run JavaScript does not.

## The flow

1. The snapshot your SDK polls marks the key as challenge.
2. The request arrives without a valid `_cch` cookie, and the SDK could resolve a client IP for it, so the SDK answers a page load with a **403** and a self-contained HTML page: inline JavaScript, **no external assets**, `Cache-Control: no-store`. Nothing is fetched from camada or from anyone else to display it.
3. The page computes a SHA-256 proof of work at **16 bits** of difficulty over a server nonce. The nonce is an HMAC-SHA256 of your project key, the client IP and the UTC day, so a proof done for one address is worthless from another. Yesterday's nonce is still accepted, so a solve begun just before midnight UTC is not thrown away.
4. It posts the answer back to the SDK's own verify route, on your own origin.
5. The SDK checks the work, sets the `_cch` cookie and answers **302** back to the URL that was asked for.
6. The retried request carries the cookie and passes straight through.

## The cookie

`_cch` is an HMAC token your SDK issues and verifies itself, valid **one hour**, set `HttpOnly; SameSite=Lax`. No third party is involved and nothing is stored outside your app. The cookie carries no identity beyond the address it was issued to — only the fact that this client did the work.

## Requests that are not browsers

A challenge needs a client IP: the nonce and the cookie are both bound to one, so when the SDK cannot resolve an address it lets the request through rather than minting a cookie anyone could present. That is the same fail-open stance IP rules take.

A request that does not accept HTML, or whose `sec-fetch-dest` is not `document`, is not shown a page it could never solve. It passes to your app, and the response carries this header so your frontend can challenge it, for example by loading a page:

response header

```http
x-camada-challenge: 1
```

SDK releases that serve the challenge but predate this header answer such a request with a 403 instead. Upgrade to a release that sends the header to let it through.

The one exception to passing through is a client-side navigation's data request (React Router `.data`, SvelteKit `__data.json`, Next Pages Router `/_next/data`): it is challenged like a page, and the router turns the 403 into a full page load that meets the challenge. Next App Router soft navigations are not in that list: they pass with the header like any other fetch, and the next full page load meets the challenge.

When a request passes to your app with the header, its response carries `no-store` on `Cache-Control`, `CDN-Cache-Control`, `Surrogate-Control` and the Cloudflare, Vercel and Netlify CDN cache headers, replacing whatever your app set, because the signal is about one client and no cache should hand it to another. If your API responses are normally cacheable, they are not cached while that client is under a challenge.

## What it reports

Each outcome ships as an ordinary event, so the activity pages show how many challenges were served, how many were passed, and how many API calls were signalled with the header. A signalled event's `st` is the status your app actually answered, not always 200.

events

```text
served    { "st": 403, "blk": "challenge" }
passed    { "st": 200, "ch": 1 }
signalled { "st": 200, "chs": 1 }
```

## Which SDKs enforce it

| Package | From | Verifies at | Covers |
| --- | --- | --- | --- |
| `@camada/node` | `0.1.0` | `POST /__camada/challenge` | Express, Fastify, NestJS and Koa. |
| `@camada/next` | `0.1.0` | `POST /api/camada/challenge` | Next.js middleware and the route handler it installs. |
| `@camada/hono` | `0.1.0` | `POST /__camada/challenge` | Hono on Cloudflare Workers. |
| `@camada/sveltekit` | `0.1.0` | `POST /__camada/challenge` | SvelteKit handle hook. |
| `@camada/nuxt` | `0.1.0` | `POST /__camada/challenge` | Nuxt server middleware (h3). |
| `@camada/remix` | `0.1.0` | `POST /_cam/challenge` | React Router framework mode (Remix) middleware. |
| `@camada/bun` | `0.1.0` | `POST /__camada/challenge` | Bun.serve fetch handler. |
| `@camada/deno` | `0.1.0` | `POST /__camada/challenge` | Deno.serve handler. |
| `@camada/python` | `0.2.0` | `POST /__camada/challenge` | Python: Django, Flask, FastAPI, any ASGI or WSGI app. |
| `@camada/go` | `0.1.0` | `POST /__camada/challenge` | Go: net/http, any router built on http.Handler. |
| `@camada/php` | `0.1.0` | `POST /__camada/challenge` | PHP: any front controller, Laravel. |
| `@camada/ruby` | `0.1.0` | `POST /__camada/challenge` | Ruby: Rails, Sinatra, any Rack app. |
| `@camada/java` | `0.1.0` | `POST /__camada/challenge` | Java: Spring Boot, any servlet container. |
| `@camada/dotnet` | `0.1.0` | `POST /__camada/challenge` | .NET: ASP.NET Core. |

Challenge rows only reach an SDK that asks for snapshot version 4, by sending `x-camada-snapshot: 4`. camada keeps serving version 3 to everyone else, and version 3 has no challenge section at all — so **older SDKs** treat a challenge verdict as a recommendation: it is recorded and alerted, and nothing is enforced. Upgrading the package is what turns it on.

Inside the SDK the match order is **allow, then block, then challenge**: an allow-list entry always wins, and a key that is blocked is blocked rather than challenged.

## Turning it off

Pass `challenge: false` in the middleware options (the default is `true`) and the SDK treats challenge verdicts as recommendations without giving up the rest of snapshot v4.

What a challenge verdict means next to a block, and what else a list entry can key on: [rules and list entries](https://www.camada.app/docs/rules). Serving one from a Cloudflare Worker: [@camada/hono](https://www.camada.app/docs/hono).

## Sitemap

[Every page on this site](https://www.camada.app/sitemap.md)
