Consent
The consent record object and the endpoints that record, read, change and withdraw one record, with parameters, examples and errors.
These endpoints record, read, change and withdraw one consent record. Paths are relative to https://console.dpconsent.com/consent/sdk/v1. The tasks are explained in Record consent.
The consent record#
Every endpoint on this page returns the record in this shape.
{
"consentId": "cns_3f1c9a7e2b8d4c6fa0e5d1b7c9f2a4e8",
"status": "PARTIAL",
"formId": "0b9c6f0e-6a55-4c1e-9d0a-2f6f0f4f7a11",
"formVersion": 3,
"formContentHash": "9b1f04c7…",
"decisions": [
{
"purposeKey": "course-updates-by-email",
"purposeName": "Course updates by email",
"category": "MARKETING",
"granted": true
},
{
"purposeKey": "class-reminders-by-sms",
"purposeName": "Class reminders by SMS",
"category": "MARKETING",
"granted": false
}
],
"countryCode": "IN",
"subdivisionCode": null,
"lawCode": "DPDP",
"collectedOn": "2026-09-24T10:12:03.000Z",
"expiresOn": "2027-09-24T10:12:03.000Z",
"withdrawnOn": null,
"proofHash": "5d1f6e0a…",
"test": true
}| Field | Type | Description |
|---|---|---|
consentId |
string | The record's id. Store it next to your user. |
status |
string | GRANTED, PARTIAL, DENIED, WITHDRAWN, EXPIRED or PENDING. See Status values. |
formId, formVersion, formContentHash |
UUID, integer, string | The form version the person answered. |
decisions |
array | One entry for each purpose: purposeKey, purposeName, category and granted. |
decisions[].partners |
object | Partner key to allowed. Present only when the purpose is allowed and at least one of its partners was refused. |
countryCode, subdivisionCode |
string or null |
Where the person was. |
lawCode |
string or null |
The law of the region rule that matched the person's country, such as DPDP. |
collectedOn, expiresOn |
time | When the current answer was given and when it ends. |
withdrawnOn |
time or null |
When the record was withdrawn. |
proofHash |
string | The seal of the record's current state. See How consent is proven. |
test |
boolean | true when the record was written with a test key. |
metadata |
object | Your own fields. Returned only by GET /consent/{consentId} to a secret key. |
basis |
string | PRE_COMMENCEMENT, when the answer was brought in with collectedAt and you stated that basis. Present only when set. |
noticeReference, noticeSha256 |
string | The notice the person saw, for consent brought in from earlier. Present only when set. |
ageDeclaration |
object | The person's answer to "Are you 18 or older?": eighteenOrOver, and parentalConsentUrl when the answer was No and the form names a page for a parent or guardian. Sealed. Present only when set. |
captureContext |
object | How the current state was taken, when the write that made it sent capture. Returned to a secret key only. See Capture fields. |
The record never contains the identifier it was recorded under.
Status values#
| Status | Meaning |
|---|---|
GRANTED |
Every optional purpose is allowed. |
PARTIAL |
Some optional purposes are allowed. |
DENIED |
No optional purpose is allowed. |
WITHDRAWN |
The record was withdrawn. Nothing optional is allowed. |
EXPIRED |
The record passed expiresOn. Nothing optional is allowed. |
PENDING |
An email opt-in is waiting for confirmation. Nothing optional is allowed. |
Endpoints#
POST /consent#
Records a person's answers to a form. The first answer creates a record, and a later answer from the same person on the same form updates it.
| Key | Scope | Idempotency-Key |
Webhook sent |
|---|---|---|---|
| Publishable or secret | consent:write for a secret key |
Accepted | consent.granted for a new record, consent.updated after that |
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
formKey |
string, up to 60 | One of formKey, formId |
The form's key. |
formId |
UUID | One of formKey, formId |
The form's id. |
formVersion |
integer, 1 or more | No | The version the person was shown. Left out, or higher than the published version, the published version is used. |
subjectId |
string, up to 255 | Yes | The identifier to record under. Only a keyed hash is stored. See Identifiers. |
subjectType |
string | No | COOKIE_ID (default), SESSION_ID, EMAIL, USER_ID, PHONE or DEVICE_ID. |
decisions |
array, 1 to 100 | Yes | One entry for every purpose on the form that is not always on. |
decisions[].purposeKey |
string, up to 60 | Yes | The purpose's key. |
decisions[].granted |
boolean | Yes | The answer. |
decisions[].partners |
object | No | Partner key to boolean, on forms that offer choices per partner. See Partner choices. |
countryCode |
2 letters | No | Where the person is. Without it the API locates the caller. |
subdivisionCode |
1 to 3 letters or digits | No | State or province. |
locale |
string, up to 10 | No | The language the form was shown in. |
pageUrl |
string, up to 500 | No | The page or screen the answer was given on. |
interaction |
string | No | What the person did. See Interaction values. |
identifiers |
array, up to 5 | No | Other identifiers of the same person. Each has subjectType, subjectId and, from a publishable key, proof. Entries must not repeat. |
identityProof |
64 hex characters | With a publishable key and EMAIL, USER_ID or PHONE |
The proof for subjectType and subjectId. See Signed-in visitors. |
metadata |
object | No | Your own fields, up to 8,192 bytes of JSON. |
collectedAt |
ISO 8601 time | No | Secret key only. When the consent was given, for consent brought in from earlier. Not in the future and not before 1990. See Earlier consent. |
basis |
string | No | Secret key only, with collectedAt. PRE_COMMENCEMENT: your statement that the person consented before the DPDP Act commenced. |
noticeReference |
string, up to 120 | No | Secret key only, with collectedAt. The name or number of the notice the person saw: letters, digits, spaces and . _ : / # ( ) -. |
noticeSha256 |
64 hex characters | No | Secret key only, with collectedAt. The SHA-256 of that notice. |
expiresAt |
ISO 8601 time | No | Secret key only, with collectedAt. When the consent ends: after the call and within 3,650 days of it. Without it, the form's term counts from the call. |
lastActivityAt |
ISO 8601 time | No | Secret key only, with collectedAt. When the person last used your service, between collectedAt and now. |
capture |
object | No | Secret key only. How consent was taken in a branch, by phone or on paper. Sealed with this write only. See Capture fields. |
dataPrincipalType |
string | No | Secret key only. CHILD or PERSON_WITH_DISABILITY. |
ageDeclaration |
object | No | { "eighteenOrOver": boolean }: the person's own answer to "Are you 18 or older?". false records the person as a child, with the page for a parent or guardian that the form version names. The browser runtime sends it when the form asks. |
guardian |
object | With dataPrincipalType, in most cases |
Secret key only. See Guardian fields. |
Request
curl -X POST https://console.dpconsent.com/consent/sdk/v1/consent \
-H "Authorization: Bearer $PIEDAP_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: signup-usr_8f2K1xQz" \
-d '{
"formKey": "kalpa-account",
"subjectId": "usr_8f2K1xQz",
"subjectType": "USER_ID",
"countryCode": "IN",
"interaction": "SAVE_PREFERENCES",
"pageUrl": "https://www.example.in/signup",
"decisions": [
{ "purposeKey": "course-updates-by-email", "granted": true },
{ "purposeKey": "class-reminders-by-sms", "granted": false }
],
"identifiers": [
{ "subjectType": "EMAIL", "subjectId": "kavya.menon@example.in" }
]
}'const record = await client.recordConsent({
formKey: 'kalpa-account',
subjectId: 'usr_8f2K1xQz',
subjectType: 'USER_ID',
countryCode: 'IN',
interaction: 'SAVE_PREFERENCES',
pageUrl: 'https://www.example.in/signup',
decisions: [
{ purposeKey: 'course-updates-by-email', granted: true },
{ purposeKey: 'class-reminders-by-sms', granted: false },
],
identifiers: [
{ subjectType: 'EMAIL', subjectId: 'kavya.menon@example.in' },
],
idempotencyKey: 'signup-usr_8f2K1xQz',
});from piedap_consent import Decision, Identifier
consent = client.record_consent(
form_key="kalpa-account",
subject_id="usr_8f2K1xQz",
subject_type="USER_ID",
country_code="IN",
interaction="SAVE_PREFERENCES",
page_url="https://www.example.in/signup",
decisions=[
Decision("course-updates-by-email", True),
Decision("class-reminders-by-sms", False),
],
identifiers=[Identifier("EMAIL", "kavya.menon@example.in")],
idempotency_key="signup-usr_8f2K1xQz",
)Response
201 with the consent record when a record was created. 200 when an existing record was updated, with the same consentId. A secret key also gets captureContext when the write sent capture.
A write with collectedAt that the records already overrule answers 409, writes nothing and sends no webhook. errorData.skipped holds the code, errorData.consentId the record that overrules it, and errorData.current its status, collectedOn and withdrawnOn. See Earlier consent.
Errors
| Status | Code | When |
|---|---|---|
400 |
INVALID_PURPOSE_DECISIONS |
A purpose was left unanswered, or an answer names a purpose the form does not offer. |
400 |
INVALID_PARTNER_DECISIONS |
A partners answer names a partner the version does not list, or the version offers no choices per partner. |
400 |
METADATA_TOO_LARGE |
metadata is over 8,192 bytes of JSON. |
400 |
INVALID_COLLECTED_AT |
collectedAt is in the future or before 1990. |
400 |
IMPORT_FIELDS_NEED_COLLECTED_AT |
basis, noticeReference, noticeSha256, expiresAt or lastActivityAt was sent without collectedAt. errorData.fields lists them. |
400 |
INVALID_EXPIRES_AT |
expiresAt is not after the call, or is more than 3,650 days after it. |
400 |
INVALID_LAST_ACTIVITY_AT |
lastActivityAt is before collectedAt or in the future. |
400 |
PHONE_NOT_E164 |
With collectedAt, a PHONE subject or identifier is not + and 7 to 15 digits. errorData.field names it. |
400 |
CAPTURE_REFERENCE_REQUIRED |
The capture method needs a reference that was not sent. errorData.needs lists the fields that would do. |
400 |
CAPTURE_REFERENCE_LOOKS_PERSONAL |
capture.methodReference looks like a phone number. |
400 |
CONSENT_FORM_NOT_PUBLISHED |
The form has no published version. |
400 |
AMBIGUOUS_FORM_KEY |
The formKey exists on more than one property. |
400 |
FORM_NOT_ON_PROPERTY |
The form belongs to a different property than the site or key sending the answer. |
400 |
EMAIL_NEEDS_DOUBLE_OPT_IN |
A publishable key recorded an EMAIL subject on a form that requires double opt-in. |
400 |
GUARDIAN_CONSENT_REQUIRED, GUARDIANSHIP_ORDER_REQUIRED |
dataPrincipalType was sent without a guardian verified in the way the rules ask. |
403 |
IDENTITY_PROOF_REQUIRED |
A publishable key sent an EMAIL, USER_ID or PHONE subject without a valid identityProof. |
403 |
IDENTIFIER_PROOF_REQUIRED |
A publishable key sent a linked EMAIL, PHONE, USER_ID or DEVICE_ID without a valid proof. |
403 |
GUARDIAN_CONSENT_SERVER_ONLY |
A publishable key sent dataPrincipalType or guardian. |
403 |
IMPORT_FIELDS_SERVER_ONLY |
A publishable key sent basis, noticeReference, noticeSha256, expiresAt or lastActivityAt. |
403 |
CAPTURE_SERVER_ONLY |
A publishable key sent capture. |
403 |
PROPERTY_NOT_FOR_KEY |
The key is tied to a property that is no longer active. |
404 |
CONSENT_FORM_NOT_FOUND |
No form matches formKey or formId. |
409 |
CONSENT_WITHDRAWN_ON_RECORD, CONSENT_ALREADY_ON_RECORD, CONSENT_NEWER_ON_RECORD, CONSENT_ERASED_ON_RECORD, CONSENT_HELD_UNDER_OTHER_IDENTIFIER |
A write with collectedAt was skipped. See Earlier consent. Not worth retrying. |
429 |
CHECK_BUDGET_EXCEEDED |
A write with collectedAt from a key that holds consent:check used up that key's check budget. Each identifier the write names counts. |
Also the errors every call can return.
GET /consent/{consentId}#
Returns the record as it stands now.
| Key | Scope | Idempotency-Key |
Webhook sent |
|---|---|---|---|
| Publishable or secret | consent:read for a secret key |
Ignored | None |
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
consentId |
string | Yes | Path parameter. The record's id. |
Request
curl https://console.dpconsent.com/consent/sdk/v1/consent/$CONSENT_ID \
-H "Authorization: Bearer $PIEDAP_SECRET_KEY"const record = await client.getConsent(consentId);consent = client.get_consent(consent_id)Response
200 with the consent record. A secret key also gets metadata, and captureContext when the current state carries one. The SDK calls return null or None for a 404.
Errors
| Status | Code | When |
|---|---|---|
404 |
None. The message is No live consent for this id |
No record has this id, the record's expiresOn has passed, or the key is tied to a different property. |
Also the errors every call can return.
PATCH /consent/{consentId}#
Records a change of mind on an existing record. Purposes you leave out keep their answer.
| Key | Scope | Idempotency-Key |
Webhook sent |
|---|---|---|---|
| Publishable or secret | consent:write for a secret key |
Accepted | consent.updated |
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
consentId |
string | Yes | Path parameter. The record's id. |
decisions |
array, 1 to 100 | Yes | The answers that changed, as in POST /consent. |
pageUrl |
string, up to 500 | No | Where the change was made. |
interaction |
string | No | What the person did. See Interaction values. |
metadata |
object | No | Replaces your fields on the record. Up to 8,192 bytes of JSON. |
identifiers |
array, up to 5 | No | More identifiers to link to this record. |
capture |
object | No | Secret key only. How this change was taken. A change without it clears the record's capture context; the earlier event keeps it. See Capture fields. |
Request
curl -X PATCH https://console.dpconsent.com/consent/sdk/v1/consent/$CONSENT_ID \
-H "Authorization: Bearer $PIEDAP_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"decisions": [
{ "purposeKey": "class-reminders-by-sms", "granted": true }
],
"interaction": "PREFERENCE_CENTRE",
"pageUrl": "https://www.example.in/account/privacy"
}'const record = await client.updateConsent(
consentId,
[{ purposeKey: 'class-reminders-by-sms', granted: true }],
{
interaction: 'PREFERENCE_CENTRE',
pageUrl: 'https://www.example.in/account/privacy',
},
);from piedap_consent import Decision
consent = client.update_consent(
consent_id,
[Decision("class-reminders-by-sms", True)],
interaction="PREFERENCE_CENTRE",
page_url="https://www.example.in/account/privacy",
)Response
200 with the updated consent record. collectedOn and expiresOn are counted again from now. Updating a withdrawn record puts it back in force. A change clears basis, noticeReference and noticeSha256.
Errors
| Status | Code | When |
|---|---|---|
400 |
CONSENT_RECORD_ERASED |
The record has been erased and can no longer change. |
400 |
INVALID_PURPOSE_DECISIONS, INVALID_PARTNER_DECISIONS |
As for POST /consent. |
400 |
METADATA_TOO_LARGE |
As for POST /consent. |
400 |
GUARDIAN_CONSENT_REQUIRED |
A secret key added a grant to a record kept for a child or a person with a guardian. Record new grants with POST /consent and the verified guardian. |
400 |
CAPTURE_REFERENCE_REQUIRED, CAPTURE_REFERENCE_LOOKS_PERSONAL |
As for POST /consent. |
403 |
IDENTIFIER_PROOF_REQUIRED |
As for POST /consent. |
403 |
CAPTURE_SERVER_ONLY |
A publishable key sent capture. |
404 |
CONSENT_RECORD_NOT_FOUND |
No record with this id exists for a key of this kind, test or live. |
404 |
None. The message is No live consent for this id |
The key is tied to a different property. |
Also the errors every call can return.
POST /consent/{consentId}/withdraw#
Withdraws one record: every optional purpose becomes refused, the status becomes WITHDRAWN and withdrawnOn is set.
| Key | Scope | Idempotency-Key |
Webhook sent |
|---|---|---|---|
| Publishable or secret | consent:write for a secret key |
Accepted | consent.withdrawn |
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
consentId |
string | Yes | Path parameter. The record's id. |
reason |
string, up to 500 | No | Kept on the withdrawal event. |
Request
curl -X POST https://console.dpconsent.com/consent/sdk/v1/consent/$CONSENT_ID/withdraw \
-H "Authorization: Bearer $PIEDAP_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{ "reason": "Asked by phone on the support line" }'await client.withdrawConsent(
consentId,
'Asked by phone on the support line',
);client.withdraw_consent(
consent_id,
reason="Asked by phone on the support line",
)Response
200 with the consent record. Strictly necessary purposes stay on. Withdrawing a record that is already withdrawn also answers 200, changes nothing and sends no webhook.
Errors
| Status | Code | When |
|---|---|---|
404 |
CONSENT_RECORD_NOT_FOUND |
No record with this id exists for a key of this kind, test or live. |
404 |
None. The message is No live consent for this id |
The key is tied to a different property. |
Also the errors every call can return.
Field values#
Interaction values#
interaction is sealed into the record. From a secret key it defaults to API, or to IMPORT when you send collectedAt.
| Value | Use it when |
|---|---|
ACCEPT_ALL |
The person chose a single "accept all" control. |
REJECT_ALL |
The person chose a single "reject all" control. |
SAVE_PREFERENCES |
The person made choices purpose by purpose and saved them. |
PREFERENCE_CENTRE |
The person changed earlier choices on a settings page. |
DISMISS |
The person closed a notice without choosing. |
DO_NOT_SELL |
The person used a "do not sell or share" control. |
API |
Your system recorded the answer and none of the above fits. |
IMPORT |
The record is brought in from another system. |
The service sets GPC_SIGNAL, EMAIL_CONFIRMATION, WITHDRAW, OPERATOR and EXPIRY itself.
Guardian fields#
| Field | Type | Values |
|---|---|---|
dataPrincipalType |
string | CHILD or PERSON_WITH_DISABILITY |
guardian.relationship |
string | PARENT or LAWFUL_GUARDIAN |
guardian.verification |
string | EXISTING_ACCOUNT, VIRTUAL_TOKEN, IDENTITY_DOCUMENT or GUARDIANSHIP_ORDER. PERSON_WITH_DISABILITY needs GUARDIANSHIP_ORDER. |
guardian.reference |
string, up to 200 | The account id, token id or order number you verified against. Only a hash of it is stored. |
Earlier consent#
A write with collectedAt is checked before anything changes. The first rule that matches skips it with 409:
| Code | When |
|---|---|
CONSENT_WITHDRAWN_ON_RECORD |
The person's record on this form is withdrawn, whatever the dates. |
CONSENT_ALREADY_ON_RECORD |
The record's answer has this exact collectedAt. The row was sent before. |
CONSENT_NEWER_ON_RECORD |
The record's answer is newer than collectedAt. |
CONSENT_ERASED_ON_RECORD |
A record of the person, on any form, was erased at or after collectedAt. errorData.consentId is null. |
CONSENT_HELD_UNDER_OTHER_IDENTIFIER |
Another record on this form, found through an identifier the request names or one linked to it, is withdrawn or holds the same or a newer answer. errorData.heldBy is WITHDRAWN, SAME or NEWER. |
A write that passes seals each decision at collectedAt. The record expires after the form's term counted from the call, or at expiresAt. A person known only under an identifier the request neither names nor links is not reached. The task is in Bring in consent given earlier.
Capture fields#
capture on POST and PATCH, from a secret key:
| Field | Type | Required | Description |
|---|---|---|---|
capture.channel |
string | Yes | BRANCH, CALL_CENTRE, IVR, KIOSK, FIELD_AGENT or PAPER. |
capture.method |
string | Yes | OTP, SIGNED_DECLARATION, RECORDED_CALL, PAPER_FORM, LINK_CONFIRMED or AGENT_ATTESTED. |
capture.agentRef |
string, up to 100 printable ASCII | For AGENT_ATTESTED |
Your id for the member of staff. Stored only as agentRefHash. |
capture.locationRef |
string, up to 100 printable ASCII | No | Your code for the branch or site. Stored only as locationRefHash. |
capture.methodReference |
string, up to 128 | For OTP, RECORDED_CALL, LINK_CONFIRMED |
Your id for the confirmation: letters, digits and . _ : / # -. Never personal data and never the OTP itself. |
capture.documentSha256 |
64 hex characters | For SIGNED_DECLARATION, PAPER_FORM, or methodReference |
The SHA-256 of the scanned form or declaration. |
The response carries captureContext: channel, method, agentRefHash, locationRefHash, methodReference and documentSha256, each only when sent, plus attestedByStaffOnly. That flag is worked out from method and is not part of the seal: it is true unless the method reports the person's own confirmation.
A reference hash is HMAC-SHA256(fingerprint key, "agent:" or "location:" + reference, trimmed, in lower case), as hex. The SDK helpers captureReferenceHash and capture_reference_hash compute it. The task is in Record consent taken in a branch, by phone or on paper.
Related#
- Record consent from your server
- Receipts for
GET /consent/{consentId}/verify - Consent events