code, when there is one. Codes are stable, so match on them and not on the wording of a message.
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.
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.
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.
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.
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.
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.