Creating receipts
A receipt is a signed, hash-chained record of a single agent decision. The signature is your tenant's per-key Ed25519. The chain links each receipt to the previous one. Tampering with any field breaks the chain detectably.
What you put in a receipt
Three pieces:
-
agent_name— short label for the agent that made the decision."purchase_approver","refund_router", etc. 1–80 characters. -
payload— arbitrary JSON object describing the decision. Anything an auditor would want to reconstruct the action. Max 64 KB after canonical encoding. -
agent_run_id(optional) — a stable identifier you assign to a single run. Useful if you want to link a receipt back to a specific request in your own system.
The server computes receipt_hash, prev_receipt_hash,
receipt_seq, signing_kid, signature,
and created_at.
Python
import os
import requests
def record_receipt(agent_name: str, payload: dict, run_id: str | None = None):
resp = requests.post(
"https://marturia.dev/api/marturia/v1/receipts",
headers={"X-Marturia-Key": os.environ["MARTURIA_API_KEY"]},
json={
"agent_name": agent_name,
"payload": payload,
"agent_run_id": run_id,
},
timeout=5,
)
resp.raise_for_status()
return resp.json()
# Wrapping a function so every call ships a receipt automatically.
from functools import wraps
import uuid
def audited(agent_name: str):
def decorator(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
run_id = str(uuid.uuid4())
result = fn(*args, **kwargs)
record_receipt(
agent_name=agent_name,
payload={
"args": args,
"kwargs": kwargs,
"result": result,
},
run_id=run_id,
)
return result
return wrapper
return decorator
@audited(agent_name="purchase_approver")
def approve_purchase(order_id: int, amount: float) -> dict:
return {"order_id": order_id, "decision": "approve"}
Node.js
async function recordReceipt(agentName, payload, runId) {
const resp = await fetch('https://marturia.dev/api/marturia/v1/receipts', {
method: 'POST',
headers: {
'X-Marturia-Key': process.env.MARTURIA_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
agent_name: agentName,
payload,
agent_run_id: runId,
}),
});
if (!resp.ok) {
throw new Error(`Marturia ${resp.status}: ${await resp.text()}`);
}
return resp.json();
}
curl
curl -X POST https://marturia.dev/api/marturia/v1/receipts \
-H "X-Marturia-Key: $MARTURIA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"agent_name": "refund_router",
"payload": {
"ticket_id": "T-9981",
"decision": "refund_full",
"reasoning": "duplicate charge confirmed via stripe lookup"
}
}'
What you get back
{
"id": 98, // receipt row id
"tenant_id": 2, // your tenant
"project_id": 70, // project the API key belongs to
"receipt_seq": 54, // monotonic per-tenant sequence
"signing_kid": "t2-v1", // key id used to sign
"receipt_hash": "803e993d40faef9071dd0d458d6b5382...",
"agent_name": "refund_router",
"agent_run_id": null,
"verify_url": "https://marturia.dev/v1/verify/2/98"
}
How the chain works
-
Server computes the canonical JSON of
{tenant_id, receipt_seq, agent_name, payload, prev_receipt_hash, signing_kid, created_at}. Canonical = sorted keys, no whitespace, deterministic encoding of floats and unicode. -
SHA-256 of the canonical bytes =
receipt_hash. - Server signs the hash with your tenant's Ed25519 private key. The private key never leaves the server. The matching public key is published in the verify endpoint.
-
The next receipt's
prev_receipt_hash= this receipt'sreceipt_hash. Hash chaining means tampering with any prior receipt invalidates every subsequent receipt. - Every 15 minutes, the chain's tail is anchored into a Merkle root which is cosigned by independent witness operator nodes. That's the external "this hash existed at this time" anchor.
Limits
| Limit | Value |
|---|---|
| Payload size | 64 KB after canonical encoding |
| Rate limit | 60 receipts / minute / project |
agent_name length | 1–80 characters |
| Auth | X-Marturia-Key header (the same one you use for OTLP) |
Receipts are tenant-scoped, not project-scoped.
You have one chain per Marturia tenant, regardless of which project's
API key generated the receipt. The dashboard surfaces all receipts on
every project for convenience. If you need a separate chain, create a
separate tenant.
Next: prove a receipt is real
The point of the receipt is that anyone — you, your customer, an auditor — can verify it offline. See offline verification.