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:

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

  1. 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.
  2. SHA-256 of the canonical bytes = receipt_hash.
  3. 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.
  4. The next receipt's prev_receipt_hash = this receipt's receipt_hash. Hash chaining means tampering with any prior receipt invalidates every subsequent receipt.
  5. 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

LimitValue
Payload size64 KB after canonical encoding
Rate limit60 receipts / minute / project
agent_name length1–80 characters
AuthX-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.