---
title: "Form consent"
description: "Record, search, export and erase proofs of consent collected by your own sign-up, newsletter and contact forms."
url: "https://cookiebeam.com/docs/api-reference/form-consent"
lang: "en"
---

# Form consent

Record, search, export and erase proofs of consent collected by your own sign-up, newsletter and contact forms.

The form consent endpoints keep an append-only record of consent that people give on your own forms — a newsletter sign-up, a contact form, an account registration — and return a signed receipt for each record. You can record from your server with a secret key, or straight from the browser with the [forms snippet](#browser-forms-snippet) and a publishable key.

## Availability

- **Switched on per deployment.** Form consent is off unless it has been switched on for the CookieBeam deployment you use. While it is off, every endpoint on this page (and every [marketing preferences](https://cookiebeam.com/docs/api-reference/preferences) endpoint) answers HTTP 404 `feature_disabled`, and the dashboard's **Form consents** section says that recording is not switched on.
- **Plan.** Recording needs a plan that includes API access; otherwise HTTP 403 `plan_required`.
- **Monthly quota.** New `given` and `confirmed` proofs count against a monthly quota that depends on the plan. Past it, the call returns HTTP 429 `quota_exceeded` with `resetAt` and `Retry-After`, and nothing is stored. Withdrawals never count.
- **After a downgrade.** Keys keep working for withdrawals and erasure, so people can still withdraw consent and you can still honour erasure requests. New `given` and `confirmed` proofs are refused with HTTP 403 `plan_required`.

## How a proof works

- **Events.** A proof records one of three events: `given` (the person ticked one or more consent boxes), `confirmed` (a double opt-in confirmation of an earlier `given` proof, named by `confirmsId`), or `withdrawn` (the person withdrew one or more purposes).
- **Purposes.** For `given`, the purposes are the keys of the ticked checkboxes. A later record for the same person and purpose supersedes the earlier one; superseded proofs stay stored with `current: false`.
- **The person.** You send the person's e-mail address, phone number or your own id as `subject`. CookieBeam normalises it and stores only a team-scoped keyed hash; the value itself is never returned. Responses identify the person by `subjectRef` (`<key id>:<hex hash>`), which you can use to filter lists. To find a person by their e-mail address or phone number, use [search](#find-every-proof-for-a-person), which keeps the identifier out of URLs and logs.
- **Time.** `collectedAt` is the time the person submitted the form. A value more than 24 hours from server time is replaced by the time the request arrived, and the proof carries `clientTimeRejected: true`.
- **Channel.** Proofs recorded with a publishable key have `channel: "browser"`; proofs recorded with a secret key have `channel: "server"`.
- **Receipts.** Recording a proof, getting one proof and exporting return a signed `receipt`: a JWS (type `cb-form-consent+jws`) you can verify against the public keys at `/.well-known/cookiebeam-jwks.json`. It is `null` only while signing is temporarily unavailable. List and search results do not carry receipts.
- **Webhooks.** A stored `given` or `confirmed` proof emits `consent.form_recorded`, a withdrawal emits `consent.form_withdrawn`. See [Webhooks](https://cookiebeam.com/docs/api-reference/webhooks#event-payloads).

## Record a proof

```text
POST /api/v1/consent/forms
```

Requires a live secret key with `consent:forms:write`, or a [publishable key](https://cookiebeam.com/docs/api-reference/authentication#publishable-keys) from one of its allowed origins. Publishable keys can only record `given`; confirmations and withdrawals need a secret key, because they change the state of a person you have verified on your side. Accepts an [`Idempotency-Key`](https://cookiebeam.com/docs/api-reference/overview#safe-retries-with-idempotency-key). Bodies are limited to 64 KB.

| Field | Type | Required | Notes |
| ----- | ---- | -------- | ----- |
| `formId` | string | yes | Your stable id for the form, 1–100 characters, for example `newsletter-footer` |
| `formName` | string | no | Up to 200 characters |
| `bannerId` | string (UUID) | no | The **public** Banner ID of the site the form is on. Required for a key restricted to specific banners |
| `event` | string | no | `given` (default), `confirmed`, or `withdrawn` |
| `confirmsId` | string (UUID) | for `confirmed` | The proof id the double opt-in confirms |
| `subject.type` | string | yes | `email`, `phone`, or `external_id` |
| `subject.value` | string | yes | The e-mail address, phone number or your own id, 1–320 characters. Hashed, never returned |
| `consentText` | string | for `given` | The consent statement shown, verbatim, up to 10,000 characters |
| `checkboxes` | array | for `given` | Up to 50 entries of `{ key, label, checked, required? }`. `key` is the purpose (letters, digits, `_ . : -`); `label` is the text shown next to the box |
| `purposes` | string[] | no | `withdrawn` only: the purposes withdrawn. Default: every purpose currently given |
| `documents` | array | no | Up to 20 documents the person agreed to: `{ type, documentId?, versionId?, version?, url?, contentHash? }`. `type` is `privacy_policy`, `terms_of_service` or your own type. Reference a CookieBeam-hosted legal document by `documentId` / `versionId`, or an external one by `url` |
| `sourceUrl` | string (URL) | no | The page the form is on |
| `language` | string | no | 2–16 characters |
| `collectedAt` | string (ISO 8601) | no | When the person submitted the form |

A `given` proof must have at least one checked box, and every box marked `required` must be checked.

```bash
curl -X POST https://cookiebeam.com/api/v1/consent/forms \
  -H "Authorization: Bearer $COOKIEBEAM_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "formId": "newsletter-footer",
    "formName": "Newsletter",
    "subject": { "type": "email", "value": "alice@example.com" },
    "consentText": "Send me the monthly newsletter. I can unsubscribe at any time.",
    "checkboxes": [
      { "key": "newsletter", "label": "Send me the monthly newsletter.", "checked": true, "required": true },
      { "key": "offers", "label": "Send me partner offers.", "checked": false }
    ],
    "documents": [
      { "type": "privacy_policy", "url": "https://example.com/privacy", "version": "7" }
    ],
    "sourceUrl": "https://example.com/blog",
    "language": "en",
    "collectedAt": "2026-10-22T09:14:58.000Z"
  }'
```

A stored proof returns HTTP 201:

```json
{
  "data": {
    "id": "7b9e2c14-6a3d-4f58-9e01-2c3d4e5f6a7b",
    "event": "given",
    "subjectRef": "k1:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
    "purposes": ["newsletter"],
    "receipt": "eyJhbGciOiJFZERTQSIsInR5cCI6ImNiLWZvcm0tY29uc2VudCtqd3MifQ…",
    "recordedAt": "2026-10-22T09:15:00.412Z",
    "clientTimeRejected": false
  },
  "version": "v1"
}
```

A **test publishable key** validates the request and stores nothing: the response is HTTP 200 with `test: true`, a zero `id` and a placeholder `subjectRef`.

To confirm a double opt-in, send `event: "confirmed"` with `confirmsId` and the same `subject`. To withdraw, send `event: "withdrawn"` with the `subject` and, optionally, `purposes`.

## List proofs

```text
GET /api/v1/consent/forms
```

Requires `consent:forms:read`. Test keys work. Newest first, [cursor-paginated](https://cookiebeam.com/docs/api-reference/overview#pagination).

| Query parameter | Notes |
| --------------- | ----- |
| `limit`, `cursor` | Page size 1–200 (default 50), and the `nextCursor` of the previous page |
| `subjectRef` | Only this person's proofs |
| `formId`, `purpose` | Filter by form or purpose |
| `event` | `given`, `confirmed`, or `withdrawn` |
| `from`, `to` | ISO 8601 date-times |
| `bannerId` | **Public** Banner ID: only proofs recorded for that site |

```json
{
  "data": {
    "items": [
      {
        "id": "7b9e2c14-6a3d-4f58-9e01-2c3d4e5f6a7b",
        "formId": "newsletter-footer",
        "formName": "Newsletter",
        "event": "given",
        "confirmsId": null,
        "subjectType": "email",
        "subjectRef": "k1:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
        "consentText": "Send me the monthly newsletter. I can unsubscribe at any time.",
        "consentTextHash": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b",
        "checkboxes": [
          { "key": "newsletter", "label": "Send me the monthly newsletter.", "checked": true, "required": true },
          { "key": "offers", "label": "Send me partner offers.", "checked": false, "required": false }
        ],
        "purposes": ["newsletter"],
        "documents": [{ "type": "privacy_policy", "url": "https://example.com/privacy", "version": "7" }],
        "sourceUrl": "https://example.com/blog",
        "language": "en",
        "collectedAt": "2026-10-22T09:14:58.000Z",
        "clientTimeRejected": false,
        "channel": "server",
        "current": true,
        "recordedAt": "2026-10-22T09:15:00.412Z"
      }
    ],
    "nextCursor": null
  },
  "version": "v1"
}
```

## Get one proof

```text
GET /api/v1/consent/forms/{id}
```

Requires `consent:forms:read`. Returns `{ proof, receipt }`: the proof in the list shape and a freshly signed receipt. A proof of another team, or outside a banner-restricted key's list, returns HTTP 404 `not_found`.

## Find every proof for a person

```text
POST /api/v1/consent/forms/search
```

Requires `consent:forms:read`. Send the identifier in the body, so it never appears in a URL or a log. The search covers every hashing key the team has used, so proofs recorded before a key rotation are found too. Returns a page in the list shape.

```bash
curl -X POST https://cookiebeam.com/api/v1/consent/forms/search \
  -H "Authorization: Bearer $COOKIEBEAM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "subject": { "type": "email", "value": "alice@example.com" }, "limit": 50 }'
```

| Field | Notes |
| ----- | ----- |
| `subject` | Required: `{ type, value }` |
| `formId`, `purpose`, `event` | Optional filters |
| `limit`, `cursor` | Optional paging |

## A person's current state

```text
POST /api/v1/consent/forms/subjects/state
```

Requires `consent:forms:read`. Body: `{ "subject": { "type", "value" } }`. Returns the latest record per purpose:

```json
{
  "data": {
    "subjectRef": "k1:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
    "proofCount": 2,
    "purposes": [
      {
        "purpose": "newsletter",
        "status": "confirmed",
        "proofId": "1c2d3e4f-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
        "formId": "newsletter-footer",
        "updatedAt": "2026-10-22T09:20:11.000Z"
      }
    ]
  },
  "version": "v1"
}
```

## Export proofs

```text
GET /api/v1/consent/forms/export
```

Requires `consent:forms:export`. Test keys work. Takes the same filters as the list (without paging) plus `format`: `json` (default, `{ "proofs": [ … ] }`) or `csv`. The response streams as a file download, newest first, with a freshly signed receipt on every row. The number of rows is capped per plan; the cap is in the `X-Export-Row-Cap` header. Each export is written to the team's audit log.

```bash
curl "https://cookiebeam.com/api/v1/consent/forms/export?format=csv&from=2026-01-01T00:00:00Z" \
  -H "Authorization: Bearer $COOKIEBEAM_API_KEY" \
  -o form-consents.csv
```

## Erase a person's proofs

```text
DELETE /api/v1/consent/forms/subjects
```

Requires `consent:forms:erase`, a live key, and a key that is not restricted to specific banners (erasure covers every banner of the team). Body: `{ "subject": { "type", "value" } }`. Deletes the person's proofs under every hashing key the team has used and writes a minimal erasure record (subject reference, count, key) to the team's audit log.

```json
{
  "data": {
    "subjectRef": "k1:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
    "deleted": 2
  },
  "version": "v1"
}
```

`deleted` is `0` when nothing was stored.

## Browser forms snippet

The forms snippet records a `given` proof when a marked form is submitted. It never delays or blocks the submission, and it reads only the fields you mark.

```html
<script
  src="https://cookiebeam.com/forms/v1/cookiebeam-forms.js"
  data-key="cb_pub_live_your_publishable_key"
  data-banner-id="PUBLIC_BANNER_ID"
  defer
></script>

<form data-cookiebeam-consent-form="newsletter-footer" name="Newsletter">
  <input type="email" name="email" data-cookiebeam-subject="email">
  <label>
    <input type="checkbox" data-cookiebeam-consent="newsletter" required>
    Send me the monthly newsletter.
  </label>
  <p data-cookiebeam-consent-text>Send me the monthly newsletter. I can unsubscribe at any time.</p>
  <a href="https://example.com/privacy" data-cookiebeam-document="privacy_policy" data-version="7">Privacy policy</a>
  <button type="submit">Subscribe</button>
</form>
```

| Attribute | On | Meaning |
| --------- | -- | ------- |
| `data-key` | script | The publishable key |
| `data-banner-id` | script | Optional **public** Banner ID, sent as `bannerId` |
| `data-api` | script | Optional API origin. Default: the origin the script was loaded from |
| `data-cookiebeam-consent-form` | form | Marks the form and sets `formId` |
| `data-cookiebeam-subject` | input | The field that identifies the person: `email`, `phone` or `external_id`. Without it, the first `type="email"` input is used |
| `data-cookiebeam-consent` | checkbox | A consent box; the value is the purpose key. `required` boxes must be ticked |
| `data-cookiebeam-consent-text` | any element | The consent statement. Without it, the labels of the ticked boxes are used |
| `data-cookiebeam-document` | link | A document the person agrees to; `data-version` adds its version |

The snippet records nothing when there is no subject value, when no consent box is ticked, or when a required box is unticked. It sends the page's origin and path as `sourceUrl`, never the query string. Your dashboard's **Form consents** section shows the snippet with your key filled in.

The snippet also exposes `window.CookieBeamForms`:

| Member | Description |
| ------ | ----------- |
| `version` | The snippet version, currently `1.0.0` |
| `collect(form)` | Build the request body from a marked form, or `null` when there is nothing to record |
| `record(payload)` | Send a body you built yourself (same shape as `POST /api/v1/consent/forms`, `given` only). Resolves to the parsed response |

## Errors

| HTTP status | Code | Cause |
| ----------- | ---- | ----- |
| 400 | `invalid_request` | Validation failed, for example no checked box, a required box unchecked, a missing `consentText` or `confirmsId`, or a withdrawn proof being confirmed |
| 403 | `plan_required` | The plan does not include form consent, or the plan was downgraded and the event is not a withdrawal |
| 403 | `publishable_key_not_allowed` | A publishable key was used for a confirmation, a withdrawal, or any endpoint other than recording |
| 403 | `origin_not_allowed` | The publishable key does not list the request's origin |
| 403 | `key_restricted` | A banner-restricted key recorded without one of its banners, or tried to erase |
| 403 | `test_key_read_only` | A secret test key was used to record or erase |
| 404 | `feature_disabled` | Form consent is not switched on for this deployment |
| 404 | `not_found` | The banner, the proof, or the proof to confirm was not found |
| 429 | `quota_exceeded` | The monthly quota is used up; `resetAt` says when it resets |
| 503 | `identity_unavailable` | Identifier hashing is temporarily unavailable; retry later |
