What a signed consent receipt actually proves

Published on September 16, 2026

Most consent tools store a row in their own database. A receipt signed with Ed25519 is a small document anyone can verify afterward, even offline and without having to trust us.

When a regulator reviews GDPR compliance, they don't just ask "did you log the consent?" They ask whether the audit trail is still intact years later, whether you can reconstruct exactly what the person saw and when, and whether that evidence holds up to scrutiny without depending on someone else's word for it.

That's where most consent platforms fall short: they produce a record inside their own system. It holds up as long as the vendor exists, cooperates, and their database hasn't changed. A signed receipt doesn't depend on any of that.

The problem with proof only you control

A database row is worth exactly as much as the trust placed in whoever controls it. If a user disputes having given consent three years from now, or an auditor asks for evidence of a specific event, the usual answer is "check our dashboard". That forces the regulator, or the other party, to either trust the vendor or ask for direct access to their internal systems.

A receipt changes that relationship. It's a small, self-contained document: "on this date, this person accepted exactly this text," with a cryptographic signature anyone can check on their own. No need to call our API. No need to believe anything we say.

What it actually contains

Every POST /v1/consent response includes a receiptId. The full receipt looks like this:

GET /v1/receipts/rcpt_9f2c…

{
  "receiptId": "rcpt_9f2c…",
  "issuedAt": "2024-05-01T10:03:00Z",
  "payload": {
    "subject": "<identifier hash>",
    "purpose": "marketing_email",
    "version": 1,
    "statementHash": "<hash of the exact text shown>",
    "state": "GRANTED",
    "controller": "Acme Ltd"
  },
  "algorithm": "EdDSA",
  "kid": "key_2024a",
  "signature": "<base64url>"
}

Notice what it doesn't carry: the payload contains no plaintext personal data. The subject and the exact clause text are hashed, so the receipt can be handed to a third party (an auditor, a DPA, the other side of a contract) without exposing anything beyond what's strictly needed to verify it.

How anyone verifies one without calling us

The signature is standard Ed25519. No SDK or cooperation from us required, just a crypto library and four steps:

  1. Download the public keys from GET /.well-known/jwks.json.
  2. Pick the key whose kid matches the receipt's.
  3. Re-serialize the payload with object keys sorted (JSON Canonicalization, RFC 8785).
  4. Verify signature over those bytes with the Ed25519 public key.

For anyone who'd rather not implement those four steps: await tc.verifyReceipt(receipt) in the JavaScript SDK does it with the browser's Web Crypto, or GET /v1/receipts/{id}/verify repeats it on our server and returns { valid: true }. But the real guarantee is a different one: neither option is required, because the cryptosystem is public and standard.

Why an old key is never retired

We rotate signing keys over time, like any serious system. The difference is that we never retire an old public key: a receipt issued today will still verify ten years from now, against the same key published at the same /.well-known/jwks.json. A record that lives only in a database has no such guarantee. You're simply trusting that the row is still there and hasn't changed.

What this means on the day of a real audit

When that day comes, the practical difference is this: instead of granting dashboard access or asking a vendor to confirm something in writing, you hand over a file. The auditor verifies it with their own tools, on their own machine, without depending on anyone replying in time. That's what separates proof from a plain record.

The full technical reference is in the receipts documentation, including the exact payload schema and the edge cases of verification. To see it working, the quickstart guide takes under ten minutes.