---
type: reference
---

# API endpoints

Three HTTP endpoints. All follow the [inverted envelope](#inverted-envelope) — Cap's wire format is the top level, Caputchin data rides under `platform`. The platform server is a dumb proxy for Cap parts and never parses or transforms Cap's shapes. See [ADR-0003](adr/0003-inverted-envelope.md) for why.

The browser-side endpoints (`/game/start`, `/game/complete`) are reached via Cap's `CAP_CUSTOM_FETCH` hook, which the [widget](widget.md) uses to inject the `platform` field into Cap's outgoing request bodies. The customer-backend endpoint (`/siteverify`) is reCAPTCHA-shape-compatible at the top level.

## Inverted envelope

```
{
  ...cap_native_shape,
  "platform": { ...caputchin_extensions }
}
```

Why this shape: Cap's protocol can change without affecting platform code; the server forwards bytes and only reads the `platform` field. See [cap-integration](cap-integration.md) for the full proxy posture.

## `POST /game/start` (browser → platform)

Fires when the user *starts the game*, not on widget mount. The widget calls Cap's `solve()` lazily so users who never play don't generate challenges.

**Request:**

```json
{
  "...cap_challenge_request fields": "...",
  "platform": { "sitekey": "cpt_pub_...", "gameId": "bubble-pop" }
}
```

`sitekey` is the customer's public site key (visible in the widget mount attribute). `gameId` is optional — absent for the invisible-default mode, present otherwise (marketplace ID or customer-supplied label).

**Response:**

```json
{
  "...cap_challenge_response fields": "...",
  "platform": { "sessionId": "...", "gameNonce": "..." }
}
```

The `gameNonce` is server-issued, signed with the platform secret, and carries `{ sessionId, gameId, issuedAt, expiresAt }`. See [wrapped token](#wrapped-token).

## `POST /game/complete` (browser → platform)

Fires every time the game's `onComplete` callback fires. Games may legitimately fire `onComplete` multiple times (replay, multi-round) — see [game-sdk](game-sdk.md). The server dedups Cap's `redeem` per session: only the first call proxies redeem and constructs the wrapped token; subsequent calls only record score for scoreboard purposes.

**The endpoint never blocks on user input.** Score persistence is decoupled from nickname collection — the row is recorded immediately, and the optional scoreboard handle is set later via the separate [`/game/nickname`](#post-gamenickname-browser--platform) endpoint. See [ADR-0014](adr/0014-scoreboard-3letter-async-naming.md).

**Request:**

```json
{
  "...cap_redeem_request fields": "...",
  "platform": { "sessionId": "...", "score": 0.847, "durationMs": 4200 }
}
```

`score` and `durationMs` are both nullable. The [invisible default widget mode](widget.md#modes) (no game configured) sends `score: null`; manual mode customers may also omit either field. Either way they are client-claimed metadata, not security signals.

**Response (first call):**

```json
{
  "...cap_redeem_response fields": "...",
  "platform": { "wrappedToken": "..." }
}
```

**Response (subsequent calls):**

```json
{ "platform": { "recorded": true } }
```

The wrapped token is locked at first completion. The widget uses Cap's `CAP_CUSTOM_FETCH` to hold the redeem call until the first `onComplete` fires.

## `POST /game/nickname` (browser → platform)

Sets or updates the 3-letter scoreboard handle for the session. **Non-blocking from the verification flow:** the wrapped token is already returned by the time this fires, and the customer's `/siteverify` does not depend on it. A session that never calls this endpoint records its entries with a `NULL` nickname (rendered as `---` on the [dashboard scoreboard view](dashboard.md#scoreboards-post-mvp)). See [ADR-0014](adr/0014-scoreboard-3letter-async-naming.md).

**Request:**

```json
{
  "platform": { "wrappedToken": "...", "nickname": "AAA" }
}
```

**Response:**

```json
{ "platform": { "set": true } }
```

| Field | Constraint |
|---|---|
| `wrappedToken` | Authenticates the call; must be unexpired |
| `nickname` | Server-validates `^[A-Z]{3}$` and a small profanity blocklist; reject → `400` |

The call is idempotent and last-write-wins; the user may resubmit until the wrapped token expires. The update applies to **all** scoreboard entries for the session — one nickname per session, not per round. Rate-limited per session to bound abuse without identifying.

## `GET /games/:id/resolve` (browser → platform)

Returns the script URL and SRI integrity hash for a marketplace game ID so the widget can build its sandboxed iframe at mount. Customer-hosted games skip this endpoint — the customer supplies the URL via the widget's `game-src` attribute. See [widget](widget.md#game-loading), [marketplace](marketplace.md#indexer-rules), and [ADR-0015](adr/0015-sandbox-game-iframe.md).

**Request:** none (URL parameter only).

**Response:**

```json
{
  "url": "https://cdn.jsdelivr.net/npm/@cooperative-games/bubble-pop@1.2.0/dist/bubble-pop.js",
  "integrity": "sha384-..."
}
```

The hash is computed by the marketplace indexer at index time and pinned per version. The widget passes both into the iframe's `<script src integrity>`; jsDelivr tampering between index time and load time fails closed.

**404** when the ID is not in the marketplace index. Customer-hosted games never resolve here by design.

## `POST /siteverify` (customer backend → platform)

Top level is reCAPTCHA-shape-compatible (Cap's siteverify shape). Platform extensions ride flat under `platform`.

**Request:**

```json
{ "secret": "cpt_sec_...", "response": "<wrappedToken>" }
```

**Response:**

```json
{
  "...cap_siteverify_response fields": "...",
  "platform": {
    "game_id": "bubble-pop",
    "score": 0.847,
    "duration_ms": 4200
  }
}
```

The platform server verifies the wrapped token's HMAC, checks replay, calls Cap's `/siteverify` server-to-server, and merges responses. See [verify server-side guide](guides/verify-server-side.md).

The `platform.score`, `platform.duration_ms`, and `platform.game_id` are **client-claimed metadata**, not security signals — see [principles](principles.md#game-data-is-metadata-not-security) and [ADR-0002](adr/0002-no-risk-scoring.md).

## Wrapped token

Built server-side at the first `/game/complete` call, returned to the client, dropped into the form by the widget.

| Field | Source | Purpose |
|---|---|---|
| `powToken` | Opaque, from Cap's redeem response | The actual humanity proof |
| `gameNonce` | Server-issued HMAC | Replay-tracking ID; encodes `{ sessionId, gameId, issuedAt, expiresAt }` |
| `score` | Client-claimed, nullable | Scoreboard metadata. `null` for invisible-default and manual-mode flows that did not supply a score. |
| `durationMs` | Client-claimed, nullable | Scoreboard metadata |

**No `v` field.** Versioning rides on token expiry plus the bundled-widget / server deploy cycle (they ship together).

**No separate `tokenId`.** `gameNonce` is the unique identifier for replay tracking.

**No top-level `gameId`.** It's encoded inside the signed `gameNonce`, so it cannot be spoofed by mutating the token.

**Wire encoding.** `wrappedToken` is base64url-JSON of the four fields above. `gameNonce` is `base64url(payload).base64url(HMAC-SHA256(payload, PLATFORM_HMAC_SECRET))`. Only the `gameNonce` is signed — `score` and `durationMs` are tamperable in transit and explicitly not security signals per [ADR-0002](adr/0002-no-risk-scoring.md). See [ADR-0018](adr/0018-token-format-base64url-json-hmac.md) for the encoding rationale.

## OpenAPI spec

The platform serves a machine-readable OpenAPI spec at a stable URL (e.g. `https://api.caputchin.com/openapi.json`) covering both the runtime [`/siteverify`](#post-siteverify-customer-backend--platform) endpoint and the [management API](management-api.md) (site keys, secrets, hosted-verification config, tokens, etc.). Customers wanting a typed client in any language can run their preferred generator against it (`openapi-typescript`, `openapi-generator`, etc.) — Caputchin does not ship per-language SDKs. See [ADR-0011](adr/0011-drop-server-library-mvp.md) for the rationale and [snippets](snippets.md) for copy-paste examples that don't need a generator.

OpenAPI is one of four [management modalities](management-api.md); MCP and Terraform are also first-class — see [ADR-0012](adr/0012-four-management-modalities.md).
