Skip to content

Receipts

Every search and every fetch carries a receipt: a DSSE envelope, payloadType application/vnd.masterdb.receipt.v3+json, signed with the receipt key of the region that answered. Every receipt names your key (caller_key_id), so a receipt is bound to the caller it was issued to. Receipts of earlier formats still verify.

{
"retrieval_id": "r_…",
"served_at": "2026-10-01T14:02:11.482Z",
"region": "…",
"request_hash": "sha256:…",
"country": "US",
"rows": [ { "id": "mdb_…", "adl_origin": "sha256:…", "row_sig": "…", "ai_policy_version": 1 } ],
"key_id": "…",
"caller_key_id": "…",
"sandbox": true
}
Member
retrieval_id Unique to this delivery.
served_at, region When and where.
request_hash SHA-256 of your request’s RFC 9421 signature base: binds the receipt to the exact request you signed.
country The one country a search named.
rows For a search, each row served: its id, origin hash (adl_origin), row signature and the business’s AI policy version applied (ai_policy_version). For a fetch, one entry: the record’s id and the SHA-256 of its bytes.
key_id The receipt key that signed.
caller_key_id Your retrieval key that signed the request. A receipt presented by any other key is refused — a sponsored render is confirmed only by the caller its search receipt names.
sandbox Present, and true, on every sandbox receipt.

What a receipt settles, and what it does not

Section titled “What a receipt settles, and what it does not”

Your request signature is your company’s statement that it asked; the receipt is MasterDB’s that it answered, and with which version of each record. A dispute about an old price is settled by the receipt alone; a dispute about a bill is settled by the receipts, because billing counts exactly what the receipts show.

A receipt does not prove the response arrived, and it says nothing about what you did afterwards. There is no signature over the response as a whole: fifty rows from fifty businesses are fifty claims, each carrying its own origin.

GET /v1/receipts?from=&to= lists the receipts issued to your own group’s keys in a window, oldest first, a hundred to a page with a cursor, narrowed by key_id or ai_company_uuid if you like. Only ever your own: the key that signs the request decides whose receipts these are.

samples/typescript/receipts.ts
// Receipts: reconcile what you asked for against what MasterDB says it served you.
import assert from 'node:assert/strict';
import { createPrivateKey } from 'node:crypto';
import { readFileSync } from 'node:fs';
import { SANDBOX, createEd25519Signer, createRetrievalClient } from '@masterdb/client';
import { KeySet, SANDBOX_ANCHORS, verifyReceipt } from '@masterdb/verifier';
const baseUrl = process.env.MASTERDB_API_URL ?? SANDBOX.retrieval;
const signer = createEd25519Signer(createPrivateKey(readFileSync(process.env.MASTERDB_KEY_FILE as string)));
const client = createRetrievalClient({ baseUrl, signer });
const anchors = process.env.MASTERDB_ANCHORS_FILE ? JSON.parse(readFileSync(process.env.MASTERDB_ANCHORS_FILE, 'utf8')) : SANDBOX_ANCHORS;
const keys = await KeySet.fetch(baseUrl, { anchors, sandbox: true });
// Make one search now, and keep its receipt as your own record of it.
const since = new Date(Date.now() - 60 * 60 * 1000).toISOString();
const search = await client.POST('/v1/search', {
body: { collection: 'jobs', filter: { all: [{ country: 'GB' }] }, sort_by: 'published_at:desc', limit: 5 },
});
if (search.error) throw new Error(search.error.code);
const mine = verifyReceipt(search.data.receipt, keys);
// Every receipt MasterDB issued to your keys in the window, oldest first, 100 to a page.
let cursor: string | undefined;
let count = 0;
let found = false;
do {
const page = await client.GET('/v1/receipts', { params: { query: { from: since, key_id: signer.keyId, ...(cursor ? { cursor } : {}) } } });
if (page.error) throw new Error(page.error.code);
for (const r of page.data.receipts) {
const v = verifyReceipt(r.receipt, keys);
assert.equal(v.key_id !== undefined, true);
if (v.retrieval_id === mine.retrieval_id) found = true;
count++;
}
cursor = page.data.next_cursor ?? undefined;
} while (cursor);
console.log(`${count} receipts since ${since}; this search's receipt ${mine.retrieval_id} is among them: ${found}`);
assert.ok(found);

Each region folds every minute’s receipts, in the order served, into one Merkle tree, and the tree’s root goes into MasterDB’s transparency log. A receipt’s inclusion proof (two levels: the receipt in its minute, the minute in the log) comes from GET /v1/log/proof, and checkpoints from GET /v1/log/checkpoint. So MasterDB cannot later alter what it asserted it served without it showing to anyone who compares checkpoints (security model).