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.
Listing and reconciling
Section titled “Listing and reconciling”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.
// 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);"""Receipts: reconcile what you asked for against what MasterDB says it served you."""
import jsonimport osfrom datetime import datetime, timedelta, timezone
import httpxfrom masterdb_signing import SANDBOX_API, MasterDBAuth, load_keyfrom masterdb_verifier import SANDBOX_ANCHORS, KeySet, parse_strict, verify_receipt
base_url = os.environ.get("MASTERDB_API_URL", SANDBOX_API)auth = MasterDBAuth(load_key(os.environ["MASTERDB_KEY_FILE"]))client = httpx.Client(base_url=base_url, auth=auth)anchors = json.load(open(os.environ["MASTERDB_ANCHORS_FILE"])) if "MASTERDB_ANCHORS_FILE" in os.environ else SANDBOX_ANCHORSkeys = KeySet.fetch(base_url, anchors=anchors, sandbox=True)
# Make one search now, and keep its receipt as your own record of it.since = (datetime.now(timezone.utc) - timedelta(hours=1)).isoformat(timespec="milliseconds").replace("+00:00", "Z")search = client.post("/v1/search", json={"collection": "jobs", "filter": {"all": [{"country": "GB"}]}, "sort_by": "published_at:desc", "limit": 5})search.raise_for_status()mine = verify_receipt(parse_strict(search.content)["receipt"], keys)
# Every receipt MasterDB issued to your keys in the window, oldest first, 100 to a page.cursor, count, found = None, 0, Falsewhile True: params = {"from": since, "key_id": auth.key_id, **({"cursor": cursor} if cursor else {})} page = client.get("/v1/receipts", params=params) page.raise_for_status() body = parse_strict(page.content) for r in body["receipts"]: v = verify_receipt(r["receipt"], keys) found = found or v.retrieval_id == mine.retrieval_id count += 1 cursor = body.get("next_cursor") if not cursor: break
print(count, "receipts since", since, "; this search's receipt", mine.retrieval_id, "is among them:", found)assert foundIn the transparency log
Section titled “In the transparency log”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).