Gatefee receipts — format v1

A receipt is a signed, independently verifiable record of what was licensed, from whom, under which terms, and when. It is issued in the same database transaction as the retrieval it describes: a document is served with a receipt, or it is not served.

You can verify a receipt with nothing but this page, a standard JWS library, and our public keys. You do not need to call us, and you do not need to trust us. This page describes a data format. It is not legal advice.

What a receipt is

A compact JWS (RFC 7515), signed with Ed25519 (alg: EdDSA, RFC 8037). Three base64url parts joined by dots: header.payload.signature.

Header

{ "alg": "EdDSA", "kid": "<key id>", "typ": "gatefee-receipt+jwt" }

kid is the RFC 7638 JWK thumbprint (SHA-256, base64url) of the signing key, so you can check that a key in our key set really is the key it claims to be.

Payload. The payload bytes are the RFC 8785 (JSON Canonicalization Scheme) form of the claims — keys sorted, no whitespace — so the signed bytes have exactly one reading.

Claim Meaning
iss The issuer: an https origin.
jti The receipt id (UUID).
iat When the retrieval was licensed, in whole seconds (Unix time). Rights were evaluated at this same instant.
sub The licensee: the buyer organisation's id.
gp.v Claim-set version. This page describes 1.
gp.publisher_org_id The licensor: the publisher organisation's id.
gp.document_id, gp.version_id, gp.version_no The document, and the exact immutable version that was served.
gp.content_hash sha256: + the SHA-256 of the body that was served. Hash the body_md you received; it must match.
gp.canonical_url The publisher's URL for the document.
gp.purpose The licensed purpose: rag, display or summarize.
gp.terms_hash SHA-256 of the public terms document (below).
gp.terms_url Where that document can be fetched — publicly, without an account.
gp.list_price_micro, gp.currency The publisher's list price for this purpose, in millionths of a US dollar.
gp.request_id The X-Request-Id of the API call, to join the receipt to your own logs.
gp.attribution_required Whether the publisher requires attribution.
gp.max_retention_days How long the content may be retained; null means no limit is stated.

What a receipt deliberately does not contain: what the buyer actually paid, how the retrieval was accounted for (part of a plan, metered overage, or free tier), their plan, spend cap or any negotiated rate; any person's name, email or IP address; any API key. A receipt is meant to be shown to customers, auditors and regulators, and it discloses nothing about the buyer's commercial terms: two buyers on different plans retrieving the same document under the same grant hold receipts that differ only in their identifiers and time.

A later version of Gatefee may add claims. It will never change the meaning of a claim listed here without changing gp.v. Verifiers should ignore claims they do not recognise.

The terms behind terms_hash

GET <terms_url> returns a small JSON document — the publisher's grant (purpose, list price, attribution, retention, geography, scope, effective date) and the version of the public buyer terms that applied. It contains nothing specific to any buyer. The response body is exactly the bytes that were hashed:

sha256(response body) == gp.terms_hash

How to verify

  1. Fetch our keys: GET <iss>/.well-known/jwks.json. Keys are never removed — a retired key stays published for at least seven years — so an old receipt stays verifiable.
  2. Find the key whose kid equals the receipt header's kid.
  3. Verify the JWS signature with that key (EdDSA). Accept no other algorithm, whatever the header says.
  4. Check the header typ is gatefee-receipt+jwt and iss is the issuer you expect.
  5. Optionally: hash the body you were served and compare with gp.content_hash; fetch gp.terms_url and compare its SHA-256 with gp.terms_hash.

A receipt is proof of what was licensed at iat. If a publisher later revokes a version, past receipts stay valid; future retrievals stop.

Known-answer test vector

Use this to test a verifier — yours or ours. The key below is a published test key (RFC 8032 §7.1, TEST 1). It is not a Gatefee signing key, it appears in no Gatefee key set, and no real receipt is ever signed with it. Ed25519 signatures are deterministic, so signing the input below with this key must reproduce the signature exactly; we reproduced it byte for byte with OpenSSL 3.

Test private key (32-byte seed, hex): 9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60

Test public key set:

{
  "keys": [
    {
      "kty": "OKP",
      "crv": "Ed25519",
      "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo",
      "kid": "kPrK_qmxVWaYVA9wwBF6Iuo3vVzz7TxHCTwXBygrS4k",
      "alg": "EdDSA",
      "use": "sig"
    }
  ]
}

Claims:

{
  "gp": {
    "attribution_required": true,
    "canonical_url": "https://news.example/local/council-vote",
    "content_hash": "sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
    "currency": "USD",
    "document_id": "00000000-0000-7000-8000-0000000000d0",
    "list_price_micro": 8000,
    "max_retention_days": 30,
    "publisher_org_id": "00000000-0000-7000-8000-0000000000b0",
    "purpose": "rag",
    "request_id": "01932f4e-7b3a-7c1d-9e2f-000000000002",
    "terms_hash": "7a3ed7e6ee4a6ec4b06cc650b206eb8fa4386ca13daf40d9c51777b7386545f9",
    "terms_url": "https://dev.gatefee.invalid/v1/terms/7a3ed7e6ee4a6ec4b06cc650b206eb8fa4386ca13daf40d9c51777b7386545f9",
    "v": 1,
    "version_id": "00000000-0000-7000-8000-0000000000d1",
    "version_no": 3
  },
  "iat": 1789819200,
  "iss": "https://dev.gatefee.invalid",
  "jti": "01932f4e-7b3a-7c1d-9e2f-000000000001",
  "sub": "00000000-0000-7000-8000-0000000000e0"
}

Payload — the RFC 8785 bytes of those claims (one line, UTF-8):

{"gp":{"attribution_required":true,"canonical_url":"https://news.example/local/council-vote","content_hash":"sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9","currency":"USD","document_id":"00000000-0000-7000-8000-0000000000d0","list_price_micro":8000,"max_retention_days":30,"publisher_org_id":"00000000-0000-7000-8000-0000000000b0","purpose":"rag","request_id":"01932f4e-7b3a-7c1d-9e2f-000000000002","terms_hash":"7a3ed7e6ee4a6ec4b06cc650b206eb8fa4386ca13daf40d9c51777b7386545f9","terms_url":"https://dev.gatefee.invalid/v1/terms/7a3ed7e6ee4a6ec4b06cc650b206eb8fa4386ca13daf40d9c51777b7386545f9","v":1,"version_id":"00000000-0000-7000-8000-0000000000d1","version_no":3},"iat":1789819200,"iss":"https://dev.gatefee.invalid","jti":"01932f4e-7b3a-7c1d-9e2f-000000000001","sub":"00000000-0000-7000-8000-0000000000e0"}

Signing input — base64url(header) + "." + base64url(payload) — and the receipt:

eyJhbGciOiJFZERTQSIsImtpZCI6ImtQcktfcW14VldhWVZBOXd3QkY2SXVvM3ZWeno3VHhIQ1R3WEJ5Z3JTNGsiLCJ0eXAiOiJnYXRlZmVlLXJlY2VpcHQrand0In0.eyJncCI6eyJhdHRyaWJ1dGlvbl9yZXF1aXJlZCI6dHJ1ZSwiY2Fub25pY2FsX3VybCI6Imh0dHBzOi8vbmV3cy5leGFtcGxlL2xvY2FsL2NvdW5jaWwtdm90ZSIsImNvbnRlbnRfaGFzaCI6InNoYTI1NjpiOTRkMjdiOTkzNGQzZTA4YTUyZTUyZDdkYTdkYWJmYWM0ODRlZmUzN2E1MzgwZWU5MDg4ZjdhY2UyZWZjZGU5IiwiY3VycmVuY3kiOiJVU0QiLCJkb2N1bWVudF9pZCI6IjAwMDAwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBkMCIsImxpc3RfcHJpY2VfbWljcm8iOjgwMDAsIm1heF9yZXRlbnRpb25fZGF5cyI6MzAsInB1Ymxpc2hlcl9vcmdfaWQiOiIwMDAwMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwYjAiLCJwdXJwb3NlIjoicmFnIiwicmVxdWVzdF9pZCI6IjAxOTMyZjRlLTdiM2EtN2MxZC05ZTJmLTAwMDAwMDAwMDAwMiIsInRlcm1zX2hhc2giOiI3YTNlZDdlNmVlNGE2ZWM0YjA2Y2M2NTBiMjA2ZWI4ZmE0Mzg2Y2ExM2RhZjQwZDljNTE3NzdiNzM4NjU0NWY5IiwidGVybXNfdXJsIjoiaHR0cHM6Ly9kZXYuZ2F0ZWZlZS5pbnZhbGlkL3YxL3Rlcm1zLzdhM2VkN2U2ZWU0YTZlYzRiMDZjYzY1MGIyMDZlYjhmYTQzODZjYTEzZGFmNDBkOWM1MTc3N2I3Mzg2NTQ1ZjkiLCJ2IjoxLCJ2ZXJzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMGQxIiwidmVyc2lvbl9ubyI6M30sImlhdCI6MTc4OTgxOTIwMCwiaXNzIjoiaHR0cHM6Ly9kZXYuZ2F0ZWZlZS5pbnZhbGlkIiwianRpIjoiMDE5MzJmNGUtN2IzYS03YzFkLTllMmYtMDAwMDAwMDAwMDAxIiwic3ViIjoiMDAwMDAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMGUwIn0.6o8T6MPDaD1dwd-ZdSRkLp9bvfoKLm4AFw88at9lNwEGzmOo6AstYCef9ZgFT2egFlaKhaG5WuPVRBNTIeCeBA

The receipt is the signing input, a dot, and the base64url of the 64-byte Ed25519 signature. A correct verifier accepts it against the test key set above, and rejects it if any character of the payload changes.