Receipts
The endpoints that issue a signed consent receipt, publish the signing keys and check a receipt, with fields, examples and the offline check.
These endpoints issue a signed receipt for a consent record and let anyone check one. Paths are relative to https://console.dpconsent.com/consent/sdk/v1. How a receipt proves consent is in Receipts.
Endpoints#
GET /consent/{consentId}/verify#
Replays the record's proof chain and returns a signed receipt.
| 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/verify \
-H "Authorization: Bearer $PIEDAP_SECRET_KEY"const signed = await client.receipt(consentId);
console.log(signed.receipt.proof.verification.valid);receipt = client.verify(consent_id)
print(receipt.valid, receipt.reason)Response
200, trimmed:
{
"receipt": {
"specification": "piedap-consent-receipt/2.0",
"issuer": "https://console.dpconsent.com/consent",
"issuedOn": "2026-09-24T10:15:40.112Z",
"consentId": "cns_3f1c9a7e2b8d4c6fa0e5d1b7c9f2a4e8",
"subjectHash": "b7c1…",
"subjectType": "USER_ID",
"subjectFingerprint": "8c41…",
"status": "PARTIAL",
"environment": "test",
"interaction": "SAVE_PREFERENCES",
"jurisdiction": {
"countryCode": "IN",
"subdivisionCode": null,
"lawCode": "DPDP"
},
"form": {
"id": "0b9c6f0e-6a55-4c1e-9d0a-2f6f0f4f7a11",
"version": 3,
"contentHash": "9b1f04c7…",
"name": "Account messages"
},
"decisions": [],
"collectedOn": "2026-09-24T10:12:03.000Z",
"expiresOn": "2027-09-24T10:12:03.000Z",
"withdrawnOn": null,
"proof": {
"proofHash": "5d1f6e0a…",
"chain": [],
"verification": {
"valid": true,
"checkedEvents": 1,
"brokenAt": null,
"reason": "Chain verified across 1 keyed event",
"keyed": true
},
"anchors": []
}
},
"signature": "kq3n…",
"algorithm": "Ed25519",
"keyId": "…",
"publicKeysUrl": "https://console.dpconsent.com/consent/sdk/v1/keys"
}| Field | Type | Description |
|---|---|---|
receipt |
object | The signed document. Keep it unchanged, or the signature no longer matches. |
receipt.environment |
string | test or live. |
receipt.form |
object | The form version the person answered, with its copy, the wording shown. |
receipt.dataPrincipal |
object | Present for guardian consent: the type and the guardian's relationship, verification and verifiedOn. |
receipt.basis, receipt.noticeReference, receipt.noticeSha256 |
string | For consent brought in from earlier, when set: the basis you stated and the notice the person saw. |
receipt.captureContext |
object | How the current state was taken, when it carries a capture context, with attestedByStaffOnly. See Capture fields. |
receipt.proof.chain |
array | The sealed events of the record, each linked to the one before. |
receipt.proof.chain[].captureContext |
object | The capture context sealed with that event, when it carried one. |
receipt.proof.verification.valid |
boolean | The verdict on the chain. |
receipt.proof.anchors |
array | Proofs that events are included in a signed daily anchor. See Daily anchors. |
signature |
string | The base64url signature over the receipt. |
algorithm |
string | Ed25519. |
keyId |
string | The signing key, matched against kid in GET /keys. |
publicKeysUrl |
string | The address of GET /keys. |
Errors
| Status | Code | When |
|---|---|---|
404 |
CONSENT_RECORD_NOT_FOUND |
No record has this id. |
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.
GET /keys#
Returns the public half of the deployment's signing key as a JWK Set. Receipts and daily anchors are signed with it.
| Key | Scope | Idempotency-Key |
Webhook sent |
|---|---|---|---|
| None | None | Ignored | None |
Parameters
None.
Request
curl https://console.dpconsent.com/consent/sdk/v1/keysconst keys = await client.publicKeys();Response
200. The response may be cached for an hour.
{
"keys": [
{
"kid": "…",
"kty": "OKP",
"crv": "Ed25519",
"x": "…",
"use": "sig",
"alg": "EdDSA"
}
]
}Errors
None of its own.
POST /receipts/verify#
Checks a receipt someone hands you. Nothing in the receipt is trusted: the signature, the chain and every anchor proof are worked out again.
| Key | Scope | Idempotency-Key |
Webhook sent |
|---|---|---|---|
| None | None | Ignored | None |
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
receipt |
object | Yes | The receipt exactly as issued. |
signature |
string, up to 200 | Yes | The signature issued with it. |
algorithm |
string | No | Ed25519 (default), or HMAC-SHA256 for receipts issued before signing moved to Ed25519. |
Request
curl -X POST https://console.dpconsent.com/consent/sdk/v1/receipts/verify \
-H "Content-Type: application/json" \
-d @receipt.jsonimport json
import urllib.request
signed = client.verify(consent_id).to_dict()
request = urllib.request.Request(
"https://console.dpconsent.com/consent/sdk/v1/receipts/verify",
data=json.dumps({
"receipt": signed["receipt"],
"signature": signed["signature"],
"algorithm": signed["algorithm"],
}).encode("utf-8"),
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(request, timeout=10) as response:
result = json.load(response)
print(result["valid"])receipt.json holds one receipt with its receipt, signature and algorithm.
Response
200:
{
"valid": true,
"checks": [
{
"name": "signature",
"passed": true,
"detail": "Ed25519 signature over the canonical receipt"
},
{
"name": "chain",
"passed": true,
"detail": "2 sealed event(s) link from genesis to the current state"
}
]
}| Field | Type | Description |
|---|---|---|
valid |
boolean | true when every check passed. |
checks |
array | One entry for the signature, one for the chain, and one for each anchor proof in the receipt. |
checks[].passed |
boolean | The result of that check. |
checks[].detail |
string | What was checked, or why it failed. |
Errors
| Status | Code | When |
|---|---|---|
400 |
None. The message is a list | receipt is not an object, or signature is missing or too long. |
A receipt that fails a check still answers 200, with "valid": false.
Check a receipt offline#
An Ed25519 receipt can be checked with no call to the API once you hold the keys.
- Write the
receiptobject as canonical JSON: object keys sorted at every depth, no spaces, values asJSON.stringifywrites them. - Find the key in
GET /keyswhosekidequals thekeyIdreturned next to the receipt. - Verify the base64url
signatureover the UTF-8 bytes of that JSON with the Ed25519 public key inx.
The Node.js SDK does these steps:
import { verifyReceipt } from '@piedap/consent/server';
const keys = await client.publicKeys();
const signed = await client.receipt(consentId);
console.log(verifyReceipt(signed, keys) ? 'verified' : 'NOT verified');The Python package has no offline check; use POST /receipts/verify.
Related#
- Receipts
- Verify a receipt in the console
- Node.js helpers