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 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 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
givenandconfirmedproofs count against a monthly quota that depends on the plan. Past it, the call returns HTTP 429quota_exceededwithresetAtandRetry-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
givenandconfirmedproofs are refused with HTTP 403plan_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 earliergivenproof, named byconfirmsId), orwithdrawn(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 withcurrent: 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 bysubjectRef(<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, which keeps the identifier out of URLs and logs. - Time.
collectedAtis 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 carriesclientTimeRejected: true. - Channel. Proofs recorded with a publishable key have
channel: "browser"; proofs recorded with a secret key havechannel: "server". - Receipts. Recording a proof, getting one proof and exporting return a signed
receipt: a JWS (typecb-form-consent+jws) you can verify against the public keys at/.well-known/cookiebeam-jwks.json. It isnullonly while signing is temporarily unavailable. List and search results do not carry receipts. - Webhooks. A stored
givenorconfirmedproof emitsconsent.form_recorded, a withdrawal emitsconsent.form_withdrawn. See Webhooks.
Record a proof
POST /api/v1/consent/forms
Requires a live secret key with consent:forms:write, or a publishable key 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. 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.
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:
{
"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
GET /api/v1/consent/forms
Requires consent:forms:read. Test keys work. Newest first, cursor-paginated.
| 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 |
{
"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
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
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.
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
POST /api/v1/consent/forms/subjects/state
Requires consent:forms:read. Body: { "subject": { "type", "value" } }. Returns the latest record per purpose:
{
"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
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.
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
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.
{
"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.
<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 |