Receipts

A receipt is the audit primitive Lipafy emits on every successful capability call. It’s a signed JSON blob saying: “at time T, account A called capability C for amount X, and the upstream returned 2xx.” Anyone with the receipt and Lipafy’s public key can verify it — without needing access to the Lipafy database.

Why signed (not just logged)

A plain log line is fine for internal monitoring but useless as evidence to a third party — they have to trust Lipafy that the log wasn’t edited. A signed receipt is independently verifiable: take the receipt, take the public key from the docs, run the signature check yourself. You don’t need to trust us.

Use cases:

  • Dispute resolution: A user claims they didn’t authorize a charge. The receipt shows which credential authorized it, what it called, and when.
  • Provider settlement audit: A provider checks that the revenue Lipafy paid out matches the receipts they accumulated.
  • Compliance: AP2-style mandate fulfillments need verifiable records linking the user’s signed mandate to the actual purchase.

What’s in a receipt

1{
2 "version": "lipafy.receipt.v1",
3 "capability_request_id": "cr_8f3a92...",
4 "capability_id": "cap_abc...",
5 "capability_slug": "eats.local-lunch.order",
6 "account_id": "acc_def...",
7 "grant_id": "grn_ghi...",
8 "amount": "500",
9 "currency_code": "KES",
10 "upstream_status": 200,
11 "forwarded_path": "/orders",
12 "forwarded_method": "POST",
13 "wallet_balance_after": "4500",
14 "executed_at": "2026-05-17T09:14:00.000Z",
15 "signature": {
16 "alg": "Ed25519",
17 "key_id": "lipafy-receipt-2026-q2",
18 "value": "base64..."
19 }
20}

Fields:

  • capability_request_id — the same id as X-Payment-Reference returned in the gateway response.
  • grant_id — which authorization spent the money (API key grant, OAuth grant, or account_default grant).
  • upstream_status — confirms it was a 2xx success.
  • wallet_balance_after — the available balance after the debit landed. Useful for clients reconciling without a separate balance call.
  • signature — Ed25519 over a canonical JSON serialization of everything above. key_id rotates quarterly; the active and historical public keys are published at the receipts API root.

Fetching a receipt

By id (capability_request_id):

1GET /v1/receipts/cr_8f3a92...
2Authorization: Bearer <credential>

Returns the receipt as JSON. Visible to the owner of the call.

The receipt is also reachable from the URL in the X-Payment-Receipt response header on a gateway call.

Verifying a receipt

You don’t need a Lipafy credential to verify — that’s the point. Use the public endpoint:

1POST /v1/receipts/verify
2Content-Type: application/json
3
4{
5 "receipt": "<the entire JSON receipt as a string>"
6}

Returns:

1{
2 "valid": true,
3 "key_id": "lipafy-receipt-2026-q2",
4 "verified_at": "2026-05-17T10:00:00Z",
5 "details": { /* echo of the receipt fields */ }
6}

If the signature doesn’t verify (tampered receipt or wrong key), valid: false plus a reason. The endpoint is rate-limited but unauthenticated — fine for external auditors.

Verifying offline

Offline verification depends on the public key for the receipt’s signature.key_id. Today, the supported public verification path is POST /v1/receipts/verify; use that endpoint when you need a stable external check.

Receipts vs. capability_requests

The capability_requests table is the mutable internal record. The receipt is a snapshot at success time of the immutable fields plus the signature. If a refund is later issued, the capability_request row gains a refunded_at field and a corresponding refund transaction — but the original receipt still validates. A receipt being valid does not mean “the charge wasn’t refunded”; it means “the charge happened.” Always check both the receipt and the request’s current status when reconciling.

Refunds

When a successful call needs to be reversed:

1POST /v1/capability-requests/:id/refund
2Authorization: Bearer <credential>
3Content-Type: application/json
4
5{ "reason": "duplicate charge" }

Allowed for:

  • The account that paid (within the refund window — currently 7 days post-call).
  • Lipafy support can assist with exceptional refund cases.

The refund:

  • Credits the caller’s wallet (refund transaction).
  • Debits the provider’s internal balance.
  • Marks the capability_request as refunded.
  • Does not invalidate the original receipt — that signed fact stays true. A consumer reconciling sees both: receipt for the charge, refund line in the ledger.

Retention

Receipts are retained indefinitely on the platform side — they’re cheap (small JSON), auditable forever, and there’s no PII pressure to delete them. Today, fetch a known receipt by capability request id or use the X-Payment-Receipt header returned from the gateway.

  • Gateway flow — where receipts are generated
  • Settlements — receipts are the per-call ledger that feeds period rollup