---
type: how-to
title: Server side integration
summary: Take the caputchin-token from a submission and confirm it with /siteverify before trusting the request.
---

The widget hands the browser a one-time token. That token proves nothing until your server confirms it with Caputchin. This is the step that turns a visible challenge into real protection, and it is a single HTTPS call.

You need a backend that can make an outbound request, and your site key's secret (`cpt_sec_...`) available there, never in the browser.

## 1. Read the token

When the form posts, your handler receives one extra field alongside your own: `caputchin-token`. The widget injected it into the form when the visitor cleared the challenge.

## 2. Confirm it with /siteverify

POST the token and your secret. The request and response are shaped like reCAPTCHA's `siteverify`, so an existing integration ports with little change:

```http
POST https://verify.caputchin.com/v1/siteverify
Content-Type: application/json

{ "secret": "cpt_sec_...", "response": "<caputchin-token>" }
```

```js
// Node 18+
const verdict = await fetch("https://verify.caputchin.com/v1/siteverify", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({
    secret: process.env.CAPUTCHIN_SECRET,
    response: token,
  }),
}).then((r) => r.json());

if (!verdict.success) {
  // reject the submission
}
```

The same call in other languages is on [backend integration examples](/docs/integration-guides/backend-examples).

## 3. Read the response

```json
{
  "success": true,
  "error-codes": [],
  "platform": { "game_id": "caputchin/games/leaf-memory", "score": 847, "duration_ms": 4200 }
}
```

- **`success`** is the only thing to branch on. `true` means a real visitor cleared the challenge; trust the request. `false` means drop it.
- **`error-codes`** appears when `success` is `false`, naming what went wrong (a missing, expired, or already-used token, and so on).
- **`platform`** is client-claimed metadata (which game, the score, the duration). It is nullable and is **not a security signal**. Record it for analytics if you like, but never gate trust on the score.

## The rules that protect you

- **Single-use.** A token verifies once. Submitting the same one again returns `success: false`, so you do not have to track replays yourself.
- **Short-lived.** A token expires ten minutes after it is issued, so verify it while handling the submission, not on a delayed job.
- **The secret stays on the server.** It never goes to the browser, and it is what authenticates the call.

## Common mistakes

- **Verifying client-side.** The `pass` event and the token's presence are UX only. Every trust decision happens here, server-side, with your secret.
- **Gating on `score`.** It is metadata, not a confidence value.

## Common questions

### How long is a token valid?

Ten minutes after it is issued. Verify it while you handle the submission, not from a delayed job or a queue that might run later.

### Can I verify the same token twice?

No. A token is single-use. Submitting the same one again returns `success: false`, so you do not have to track replays yourself.

### Should I trust the score or game_id?

No. The `platform` block (game, score, duration) is client-claimed metadata and is nullable. Branch only on `success`, and never gate trust on the score.

### Do I need an SDK for this?

No. The check is a single HTTPS POST to `/v1/siteverify`. The same call in Node, Python, PHP, Go, Ruby, .NET, and curl is on [backend integration examples](/docs/integration-guides/backend-examples).

## See also

- [Backend integration examples](/docs/integration-guides/backend-examples) for this call in your language.
- [Hosted verification](/docs/hosted-verification/overview) to skip the backend entirely.
