Consent Docs

Receipts

The endpoints that issue a signed consent receipt, publish the signing keys and check a receipt, with fields, examples and the offline check.

Reference3 min readFor developers calling the REST API

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#

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

cURLShell
curl https://console.dpconsent.com/consent/sdk/v1/consent/$CONSENT_ID/verify \
  -H "Authorization: Bearer $PIEDAP_SECRET_KEY"
Node.jsJavaScript
const signed = await client.receipt(consentId);
console.log(signed.receipt.proof.verification.valid);
Python
receipt = client.verify(consent_id)
print(receipt.valid, receipt.reason)

Response

200, trimmed:

JSON
{
  "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

cURLShell
curl https://console.dpconsent.com/consent/sdk/v1/keys
Node.jsJavaScript
const keys = await client.publicKeys();

Response

200. The response may be cached for an hour.

JSON
{
  "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

cURLShell
curl -X POST https://console.dpconsent.com/consent/sdk/v1/receipts/verify \
  -H "Content-Type: application/json" \
  -d @receipt.json
Python
import 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:

JSON
{
  "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.

  1. Write the receipt object as canonical JSON: object keys sorted at every depth, no spaces, values as JSON.stringify writes them.
  2. Find the key in GET /keys whose kid equals the keyId returned next to the receipt.
  3. Verify the base64url signature over the UTF-8 bytes of that JSON with the Ed25519 public key in x.

The Node.js SDK does these steps:

JavaScript
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.

↑↓ to move, Enter to open