Consent Docs

Errors

The two error body shapes, every HTTP status and every error code the API returns, what each one means, what to do about it and when to retry.

Reference11 min readFor developers calling the REST API

Every error the API under /sdk/v1 returns is listed here, with its meaning and the fix. For worked problems, see Key and API errors.

Error body#

Read an error in this order:

  1. The HTTP status.
  2. code, when there is one. Codes are stable, so match on them and not on the wording of a message.
  3. message, for people. It is a string in some errors and a list of strings in others.

Do not rely on any other field. Errors come in two shapes.

An error raised while the request is handled has message as a string. Some codes add details in errorData.

JSON
{
  "ok": false,
  "error": true,
  "warn": false,
  "message": "Every purpose on the form must be answered. Missing: newsletter",
  "code": "INVALID_PURPOSE_DECISIONS",
  "requestId": "0d8a3c1e-5a52-4f0b-9c0e-2f1b7a9d4e63",
  "statusCode": 400,
  "errorData": {
    "unknownPurposeKeys": [],
    "missingPurposeKeys": ["newsletter"]
  },
  "data": {},
  "redirect": false
}

An error raised before that has message as a list. A failed field check, a refused key, a missing scope and a rate limit all look like this.

JSON
{
  "statusCode": 403,
  "code": "SCOPE_REQUIRED",
  "message": [
    "This needs a secret key with the right scope, from your server."
  ],
  "error": "FORBIDDEN",
  "timestamp": "2026-09-24T10:12:03.000Z",
  "path": "/sdk/v1/subjects/erase",
  "requestId": "0d8a3c1e-5a52-4f0b-9c0e-2f1b7a9d4e63"
}

A few refusals are shorter, with only statusCode, message and sometimes code.

SDK Status Code Message
Node.js, ConsentApiError status code message
Python, ConsentError status_code code message
iOS, ConsentAPIError status code message
Android, ConsentApiException getStatus() getCode() getMessage()

Status codes#

Status Meaning
200, 201, 202 Done. 201 is a new consent record. 202 is an email opt-in waiting for confirmation.
400 The request is not valid. Fix it before you send it again.
401 The key was not accepted.
403 The key was accepted but may not do this.
404 The record, form or request was not found.
409 A request with the same idempotency key is still running, a write with collectedAt was skipped, or a batch of notice links is not ready.
410 The links of a notice campaign batch can no longer be read.
413 The body is too large for the web server in front of the API.
422 The idempotency key was already used for a different request.
429 Too many requests. Wait for Retry-After.
500 Something failed on the server.
502, 503, 504 The deployment could not be reached.

Errors every call can return#

Status Code Meaning What to do
400 None. The message is a list A field is missing, too long or of the wrong type. Each entry names the field. Fix the fields named.
401 None. The message is Unauthorized The key is missing, wrong, revoked or turned off. For a publishable key, the origin or app id is not on its list, or the endpoint takes only a secret key. Check the key and its allowed origins. From a server, send Authorization: Bearer sk_….
403 SECRET_KEY_IN_BROWSER A secret key arrived with an Origin or Sec-Fetch-Site header. The key is now flagged as exposed. Revoke the key and create a new one. Move the call to your server.
403 SCOPE_REQUIRED The secret key lacks the scope the call needs, or a publishable key made a call on a person. Grant the scope under Scopes on the key. See Authentication.
403 PROPERTY_NOT_FOR_KEY The key is tied to one property and the request named another, or the key's property is no longer active. Use a key of the right property.
429 None. The message is Too many requests A rate limit was reached. Wait for the seconds in Retry-After. See Rate limits.
500 SOMETHING_WENT_WRONG The server failed while handling the request. Nothing was stored for an idempotency key. Retry with the same idempotency key. If it persists, report it with the request id.
500 None. The message is Internal server error The request failed before it was handled. A JSON body over 100 KB is answered this way. Send a smaller body. A retry of the same body fails the same way.

Error codes by area#

Forms#

Code Status Meaning What to do
FORM_NOT_FOUND 404 GET /form: no form has that formKey. Check the key shown on the form in the console.
CONSENT_FORM_NOT_FOUND 404 A write named a form that does not exist, or named none. Send formKey or formId.
CONSENT_FORM_NOT_PUBLISHED 400 The form has no published version. Publish the form.
AMBIGUOUS_FORM_KEY 400 The same form key exists on more than one property. Use a key tied to one property, or send propertyKey to GET /form.
FORM_NOT_ON_PROPERTY 400 The form belongs to a different property than the site, app or key sending the answer. Record against a form of the same property.
CONSENT_FORM_VERSION_NOT_FOUND 404 The form's version could not be loaded. Publish the form again. If it persists, report it with the request id.

Decisions and fields#

Code Status Meaning What to do
INVALID_PURPOSE_DECISIONS 400 A purpose on the form was not answered, or an answer names a purpose the form does not offer. Read errorData.missingPurposeKeys and errorData.unknownPurposeKeys. Answer every purpose that is not always on.
INVALID_PARTNER_DECISIONS 400 A partners answer names a partner the version does not list, or the version offers no choices per partner. Read errorData.unknownPartners. See Partner choices.
METADATA_TOO_LARGE 400 metadata is over 8,192 bytes of JSON. Send less.
INVALID_COLLECTED_AT 400 collectedAt is in the future or before 1990. Send the time the consent was given.

Records#

Code Status Meaning What to do
None. The message is No live consent for this id 404 The id is not known, the record has expired, or the key is tied to a different property. Treat it as no consent. The SDKs return null or None for a read.
CONSENT_RECORD_NOT_FOUND 404 An update or withdrawal named a record that does not exist for this kind of key, or a receipt named an id that is not known. A test key cannot change a live record, or the reverse. Check the id and which key you are using.
CONSENT_RECORD_ERASED 400 The record has been erased and can no longer change. Record a new consent with POST /consent.

Identity#

Code Status Meaning What to do
IDENTITY_PROOF_REQUIRED 403 A browser or app recorded consent under an email address, user id or phone number without a valid identityProof. Compute the proof on your server for exactly that type and identifier. See Signed-in visitors.
IDENTIFIER_PROOF_REQUIRED 403 A browser or app linked an email address, phone number, user id or device id without a valid proof. The body names the subjectType. Add the proof for that identifier, or link it from your server.
Code Status Meaning What to do
GUARDIAN_CONSENT_SERVER_ONLY 403 A browser or app sent dataPrincipalType or guardian. Record it from your server after you verify the parent or guardian.
GUARDIAN_CONSENT_REQUIRED 400 Consent for a child, or for a person with a lawful guardian, was sent without a verified guardian. Send guardian with verification and reference.
GUARDIANSHIP_ORDER_REQUIRED 400 For PERSON_WITH_DISABILITY, the guardian was not verified against the order appointing them. Send verification: "GUARDIANSHIP_ORDER".

Email opt-in#

Code Status Meaning What to do
EMAIL_CONSENT_SERVER_ONLY 403 POST /email-consent was called with a publishable key. Call it from your server.
EMAIL_NEEDS_DOUBLE_OPT_IN 400 A browser or app recorded an EMAIL identifier on a form that requires double opt-in. Start the opt-in from your server. See Email double opt-in.
VERIFICATION_TOKEN_NOT_FOUND 404 The confirmation link is not known. Start the opt-in again.
EMAIL_CONSENT_TOKEN_EXPIRED 400 The confirmation link is older than 48 hours. Start the opt-in again.

Imports#

These come from POST /consent with collectedAt, and from the console import.

Code Status Meaning What to do
CONSENT_WITHDRAWN_ON_RECORD 409 The person withdrew on this form. An earlier answer never replaces a withdrawal. Log it and move on. Do not retry.
CONSENT_ALREADY_ON_RECORD 409 An answer with this exact collectedAt is on record: the row was sent before. Log it and move on. Do not retry.
CONSENT_NEWER_ON_RECORD 409 A newer answer is on record. Log it and move on. Do not retry.
CONSENT_ERASED_ON_RECORD 409 The person was erased at or after collectedAt. errorData.consentId is null. Log it and move on. Do not retry.
CONSENT_HELD_UNDER_OTHER_IDENTIFIER 409 Under an identifier the request names, or one linked to it, the person withdrew or has the same or a newer answer. errorData.heldBy says which. Log it and move on. Do not retry.
IMPORT_FIELDS_NEED_COLLECTED_AT 400 basis, noticeReference, noticeSha256, expiresAt or lastActivityAt came without collectedAt. errorData.fields lists them. Send collectedAt, or leave the fields out.
IMPORT_FIELDS_SERVER_ONLY 403 A publishable key sent one of those fields. Send them from your server.
INVALID_EXPIRES_AT 400 expiresAt is not after the call, or is more than 3,650 days after it. Fix the date, or leave it out.
INVALID_LAST_ACTIVITY_AT 400 lastActivityAt is before collectedAt or in the future. Fix the date.
PHONE_NOT_E164 400 With collectedAt, a phone number is not in E.164. errorData.field names it. Write it as +919876543210.
IMPORT_FILE_MISMATCH 400 Console only: a notice campaign list said it came from an import, but its SHA-256 is not that import's file. Choose the file you imported.

The five 409 codes are skips: nothing was written and no webhook was sent. The check covers every identifier the request names and every identifier linked to one of them, and nothing else. errorData.skipped repeats the code, and errorData.current holds the overruling record's status, collectedOn and withdrawnOn.

A console import sends no webhook for each record, only one consent.imported when it ends. A system that follows records through webhooks reads that event, then exports the import's records from Records. The file SHA-256 on an import run is what the browser reported. It does not prove which rows were written.

Code Status Meaning What to do
PURPOSE_NOT_FOUND 404 No purpose has the key in this project. Check the purpose key in the console.
PARTNER_NOT_FOUND 404 The purpose does not list the partner now. Check the partner key on the purpose, or leave partnerKey out.
CHECK_BUDGET_EXCEEDED 429 The key checked its limit of identifiers this minute, 600 unless set. Writes with collectedAt from a key that holds consent:check count too. Wait for Retry-After, or raise Checks per minute on the key.

Capture context#

Code Status Meaning What to do
CAPTURE_SERVER_ONLY 403 A browser or app sent capture. Send it from your server.
CAPTURE_INVALID 400 capture was not one object with a channel and a method. Send capture as an object.
CAPTURE_INVALID 400 capture was not one object with a channel and a method. Send capture as an object.
CAPTURE_REFERENCE_REQUIRED 400 The method needs a reference that was not sent. Read errorData.needs and send one of the fields.
CAPTURE_REFERENCE_LOOKS_PERSONAL 400 methodReference looks like a phone number. Send the id your system gave the confirmation.

Notice campaigns#

Code Status Meaning What to do
CAMPAIGN_NOT_FOUND 404 No such campaign, or it belongs to the other environment than the key. Check the id and the key.
BATCH_NOT_FOUND 404 No such batch in the campaign. Check the id.
BATCH_NOT_READY 409 The links are still being made. Wait for campaign.batch_issued.
LINKS_NO_LONGER_AVAILABLE 410 The batch was issued more than 7 days ago. Issue new links to people who still need one.

Processor tasks and webhooks#

Code Status Meaning What to do
DUPLICATE_TASK_ID 400 A task appears twice in one report call. Send each task once.
TEXT_LOOKS_PERSONAL 400 A note, reference or file name holds an email address or a phone number. In the API it is a code on one result; in the console the whole report is refused and errorData.field names the field. Leave out anything that names the person.
EVIDENCE_REQUIRED 400 Console only: a report of a final outcome came without an evidence file. Choose the file.
WEBHOOK_URL_NOT_ALLOWED 400 Console only: the endpoint's private address is not on the deployment's allow-list. Ask whoever runs the deployment to add it to PRIVATE_NETWORK_ALLOWLIST.
PRIVATE_NETWORK_UNAVAILABLE 400 Console only: Private network was turned on, and the deployment allows no private networks. Ask whoever runs the deployment to set PRIVATE_NETWORK_ALLOWLIST.
WEBHOOK_DISABLED 409 Console only: failed events were replayed to an endpoint that is off. Turn the endpoint on first.

The codes of single reports, such as TASK_ALREADY_DONE, are in Processor tasks.

Audit log and cursors#

Code Status Meaning What to do
INVALID_CURSOR 400 GET /audit-log or GET /processor-tasks: the cursor is not one the endpoint returned. Start again with since or without a cursor, or send the last cursor unchanged.
LIVE_KEY_REQUIRED 403 GET /audit-log with a test key. Audit entries belong to the project, not to an environment. Use a live key.

Rights requests#

Code Status Meaning What to do
REQUEST_NOT_FOUND 404 POST /requests/link: no request in the project has that reference. Check the reference, in the form DSR-2026-4F2A91.

Idempotency#

Code Status Meaning What to do
IDEMPOTENCY_KEY_REUSED 422 This Idempotency-Key was already used for a different request. Use a new key for a new request. Reuse a key only to repeat the same request.
IDEMPOTENCY_REQUEST_IN_PROGRESS 409 The first request with this key has not finished. Wait a second or two and send the same request with the same key.
None 400 The Idempotency-Key header is empty, longer than 255 characters, or has spaces or characters outside printable ASCII. Send a plain key.

When to retry#

What happened Retry
No response: a network error or a timeout Yes, for a GET or a write with an idempotency key. Without a key, the write may already have been applied.
502, 503, 504 Yes, under the same condition.
500 Yes, with the same idempotency key, after a pause. Not when the body is over 100 KB.
429 Yes, after Retry-After.
409 IDEMPOTENCY_REQUEST_IN_PROGRESS Yes, with the same key, after a second or two.
409 BATCH_NOT_READY Yes, once campaign.batch_issued arrives.
409 with a skip code, such as CONSENT_NEWER_ON_RECORD No. The records already say more. Log it and move on.
429 CHECK_BUDGET_EXCEEDED Yes, after Retry-After. The consent check helpers return unavailable instead.
400, 401, 403, 404, 422 No. The same request fails the same way. Fix it first.

Wait a little longer before each retry, and add some randomness. Give up after a few attempts and raise an alert.

The Node.js and Python SDKs retry a GET, and a write that has an idempotency key. They make three attempts in all by default, after a network error, a timeout or a 502, 503 or 504. They do not retry a 429 or a 500. The mobile SDKs never retry.

↑↓ to move, Enter to open