Receipts
3 operations under /v1/consumers (GET POST): parameters, status codes, refusal bodies, and the same request in curl, HTTP, Python and TypeScript.
Base URL: https://api.codespar.dev
Every operation below requires a Bearer token. See Authentication.
A receipt is the signed record of one spend: the mandate that authorized it, the quote the agent accepted, the payment that settled, and the delivery proof when one lands, chained by a hash and sealed twice. Its id (rcpt_...) comes back on the spend that sealed it: the receipt_id field of the payment response, the receipt object of a codespar_pay result, or codespar_ledger with action: "receipt". What a receipt is, which spend paths seal one and which do not, and which channel exposes each operation are on the concept page, Mandates, receipts and approvals.
The two seals prove different things to different people.
| Field | Made with | Proves it to | Checked by |
|---|---|---|---|
receipt_sig | HMAC under the same consumer secret that signed the mandate | Whoever holds that secret. Verifying it means holding the key that also mints it, so to anybody else it is a claim rather than evidence | CodeSpar |
receipt_sig_ed25519, with receipt_sig_kid naming the key that made it | Ed25519 under CodeSpar's platform issuer key | Anybody. The public keys are served with no credential at /.well-known/codespar-receipt-keys.json | A stock Ed25519 library, over codespar-receipt:v1:<receipt_id>:<chain> |
The Ed25519 signature is additional. The HMAC seal is unchanged and still means exactly what it always meant. What the asymmetric half attests is that CodeSpar sealed this receipt for a settlement CodeSpar executed — not that a bank moved the money.
Two limits before you build on it. The Ed25519 seal exists only on receipts sealed since the API gained it: an older receipt reads null in both fields and always will, because nothing is backfilled, and a null there is an absent signature rather than a failed one. And check a signature against the key document of the deployment that issued the receipt — every deployment publishes its own key under the same kid, so a sandbox receipt checked against the production keys fails as though somebody had tampered with it.
Every operation, from the spec
Generated from the published OpenAPI document, so it never drifts from what the API actually serves. The section above is written by hand and carries what a schema cannot: the object model, field rules, and the order to call things in.
GET /v1/consumers/receipts/{id}
https://api.codespar.dev/v1/consumers/receipts/{id}Read one agentic receipt
The same shape the list returns, by receipt id. Project-scoped for the same reason the list is: the id comes from the path and the row carries signed payment evidence, so organization alone would let a live project read a test project's receipts.
chain and receipt_sig are what make this evidence rather than a record: the four links are hash-chained and the result is signed. state moves from paid to delivered when a delivery proof is sealed onto it, which re-anchors the chain.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | string | yes | The receipt id. |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
404 | object | No receipt with this id in this project. A receipt in another project of the same organization answers the same way. |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
actor | object | object | null | yes | WHO triggered the spend, as the caller declared it at payment time. Null when they declared none, and null is the honest answer rather than a gap to fill: it is never inferred from mandate.scope or from the agent the mandate was signed for. A null here means nobody said, not that an agent did it. |
approval | object | no | Present ONLY when the spend carried an approval; absent otherwise, and absent on every receipt sealed before the field existed. The value is sealed into chain as its own link (chain version 4), so it is covered by receipt_sig and receipt_sig_ed25519. It is a SEALED CLAIM by the caller, NOT a server-side approval check: the server validated its shape and nothing else. It proves this payment was sealed together with this hash, not that anybody approved anything, and not who did — the approver's identity stays in the caller's own approval artifact. |
chain | string | yes | SHA-256 (64 lowercase hex) over the UTF-8 bytes of the RFC 8785 (JCS) canonical JSON of { "v": chain_version, "links": [...] }. Every value a link needs is in this read, so the chain can be recomputed from it and bound to receipt_sig_ed25519. The links, in order, each left out when it does not apply: 1. mandate — always: id, nonce, scope, currency, then sig (v1-v3) or sig_sha256 (v4), from mandate. sig is in the read only for a credential that can spend (see mandate.sig), so any other caller verifies a v1-v3 receipt at the signature level only. 2. quote — when quote is not null: seller, resource, price_minor, payee, session_id, sig, at, nulls included. 3. approval — v4 only: items_hash, batch_hash (null when absent). 4. payment — always: rail, provider, tx_id, amount_minor, attempt_id, money_moved, at, plus each of amount_atomic, amount_authorized, amount_charged, amount_refunded, metering, sandbox only when present and not null. 5. delivery — when delivery is not null: result, proof, kind, nfe_chave, at. Values are copied as read; timestamps are RFC 3339 UTC with milliseconds. The same recipe is served as data in chain_recipe on the unauthenticated /.well-known/codespar-receipt-keys.json. Recomputing needs quote.payee unmasked, and a v1-v3 chain needs mandate.sig, so only the tenant can recompute those. The Ed25519 signing string does not change with the version: it covers this digest. |
chain_version | integer | yes | The chain format chain was computed with: 4 when approval is present; else 3 when payment.amount_authorized is present; else 2 when payment.amount_atomic is not null; else 1. A receipt keeps the version it was sealed under. |
delivery | object,null | yes | Null until a delivery proof has been sealed onto the receipt. |
exceptions | array of object | yes | — |
mandate | object | yes | — |
payment | object | yes | — |
quote | object,null | yes | Null when the receipt carries neither a seller nor a resource. |
receipt_id | string | yes | — |
receipt_sig | string | yes | — |
receipt_sig_ed25519 | string,null | yes | base64url Ed25519 signature over codespar-receipt:v1:<receipt_id>:<chain>, made with the platform issuer key. Verifiable by anyone: fetch /.well-known/codespar-receipt-keys.json (no credential), take the key whose kid is the one below, and check the signature over that string. Null on a receipt sealed before this existed, and on one sealed while the key could not be resolved — receipt_sig is the HMAC seal either way and is unaffected. |
receipt_sig_kid | string,null | yes | Which published key signed it, <did:web:...>#<namespace>-<n>. Keys rotate and a retired key stays published, so an old receipt names its own key rather than leaving a verifier to try each one. The namespace names the DEPLOYMENT that minted the key (ent#1641), so a kid absent from the key document you fetched means the receipt came from another environment — unknown_key, not a tampered receipt. |
state | "paid" | "delivered" | "exception" | "voided" | yes | — |
curl -X GET https://api.codespar.dev/v1/consumers/receipts/{id} \
-H "Authorization: Bearer $CODESPAR_API_KEY"GET /v1/consumers/receipts/{id} HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEYimport os
import requests
res = requests.get(
"https://api.codespar.dev/v1/consumers/receipts/{id}",
headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
)
res.raise_for_status()
data = res.json()const res = await fetch("https://api.codespar.dev/v1/consumers/receipts/{id}", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
},
});
const data = await res.json();const result = await cs.api.get("/v1/consumers/receipts/{id}", {
path: {
id: "receipt_0000000000000000"
}
});{
"receipt_id": "receipt_0000000000000000",
"state": "paid",
"actor": {
"type": "agent",
"id": "obj_0000000000000000",
"on_behalf_of": "string"
},
"mandate": {
"id": "obj_0000000000000000",
"nonce": "string",
"scope": "string",
"currency": "BRL",
"sig": "string",
"sig_sha256": "string"
},
"quote": {
"seller": "string",
"resource": "string",
"price_minor": 1,
"payee": "string",
"session_id": "ses_0000000000000000",
"sig": "string",
"at": "string"
},
"approval": {
"items_hash": "string",
"batch_hash": "string"
},
"payment": {
"rail": "string",
"provider": "string",
"tx_id": "tx_0000000000000000",
"amount_minor": 1000,
"amount_atomic": "1000",
"amount_authorized": "1000",
"amount_charged": "1000",
"amount_refunded": "1000",
"metering": {
"basis": "string",
"units": "string",
"unit_price": "string"
},
"sandbox": true,
"attempt_id": "attempt_0000000000000000",
"money_moved": true,
"at": "string"
},
"delivery": {
"result": "confirmed",
"proof": "string",
"kind": "string",
"nfe_chave": "string",
"at": "string"
},
"chain": "string",
"chain_version": 0,
"receipt_sig": "string",
"receipt_sig_ed25519": "string",
"receipt_sig_kid": "string",
"exceptions": [
{
"code": "string",
"detail": "string",
"at": "string"
}
]
}POST /v1/consumers/receipts/{id}/delivery
https://api.codespar.dev/v1/consumers/receipts/{id}/deliverySeal a delivery proof onto a receipt
Folds evidence that the thing paid for actually arrived into a receipt that already exists, RE-SIGNS the receipt chain with the consumer's own secret, and advances the receipt to delivered. Nothing is created and no money moves; what changes is what the receipt can prove.
PROOFS ARE RANKED, AND THE SEAL ONLY EVER MOVES UP. Three classes, weakest first: rail evidence (pix_e2e, order_confirmation, tracking, resource), a third-party merchant document reference (merchant_ref, or any kind carrying an nfe_chave), and a fiscal document (nfe, nfse). A proof STRICTLY stronger than the one already sealed re-seals the receipt; a weaker or equal one is accepted and changes nothing, answering 200 with the receipt as it stands. That is what makes this route safe to call from a webhook that may fire twice, and it is why the restaurant's NFC-e that arrives hours after an iFood order can be attached without clobbering anything.
IT IS APPEND-ONLY UNDERNEATH. Every accepted delivery lands as its own receipt_delivered audit event, and an upgrade records what it replaced. The receipt row is the current projection of that history, not the history itself, so the chain reads as a sequence of evidence rather than as a mutation.
A VOIDED RECEIPT IS CLOSED AND SAYS SO GENTLY. It answers 200 with the receipt unchanged rather than an error: a void is a decision that already happened, and failing a late webhook over it would be noise.
SCOPE IS THE PROJECT, NOT JUST THE ORG. The receipt is resolved and re-sealed within the calling project, so a test key cannot read or rewrite a live receipt and the reverse. A receipt in another project answers receipt_not_found, the same as one that never existed.
proof IS FREE TEXT AND kind IS WHAT IT MEANS. Put the identifier a human or an auditor would follow in proof (the tracking number, the order id, the endToEndId), and use nfe_chave for the fiscal key itself when there is one — a chave present under any kind is what lifts the proof to the document class. at is when the delivery happened, not when you are telling us; omitting it stamps now.
A 409 consumer_secret_unavailable MEANS THE RECEIPT CANNOT BE RE-SIGNED, so it was left exactly as it was. Nothing partial was written. It is an operator's problem (the consumer's secret version is not resolvable from the vault), not a caller's, and the delivery can be re-sent once it is fixed.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | string | yes | The agentic receipt to seal onto. |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
at | string (date-time) | no | When the delivery happened, RFC 3339 with an offset, in UTC years 0001-9999. Sealed into the chain as that instant in UTC with milliseconds. Anything else is refused with invalid_receipt_timestamp. |
kind | "order_confirmation" | "nfe" | "nfse" | "tracking" | "resource" | "merchant_ref" | "pix_e2e" | yes | — |
nfe_chave | string | no | — |
proof | string | yes | — |
result | "confirmed" | "pending" | "failed" | yes | — |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
400 | object | The body did not match the schema (invalid_body): most often a kind outside the seven accepted values, or a proof longer than 500 characters, with Zod's issues in details.issues. Or at is not an RFC 3339 timestamp with an offset in UTC years 0001-9999 (invalid_receipt_timestamp). Nothing was re-sealed in either case. |
404 | object | No such receipt in this project. The same answer for a receipt that does not exist and one belonging to another project or organization. |
409 | object | The consumer's signing secret could not be resolved, so the chain could not be re-signed. The receipt is unchanged and the call can be repeated once the secret is available. |
500 | object | The audit append and the row update run in one transaction and it failed, so neither landed. The receipt is unchanged and the call is safe to repeat. |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
actor | object | object | null | yes | WHO triggered the spend, as the caller declared it at payment time. Null when they declared none, and null is the honest answer rather than a gap to fill: it is never inferred from mandate.scope or from the agent the mandate was signed for. A null here means nobody said, not that an agent did it. |
approval | object | no | Present ONLY when the spend carried an approval; absent otherwise, and absent on every receipt sealed before the field existed. The value is sealed into chain as its own link (chain version 4), so it is covered by receipt_sig and receipt_sig_ed25519. It is a SEALED CLAIM by the caller, NOT a server-side approval check: the server validated its shape and nothing else. It proves this payment was sealed together with this hash, not that anybody approved anything, and not who did — the approver's identity stays in the caller's own approval artifact. |
chain | string | yes | SHA-256 (64 lowercase hex) over the UTF-8 bytes of the RFC 8785 (JCS) canonical JSON of { "v": chain_version, "links": [...] }. Every value a link needs is in this read, so the chain can be recomputed from it and bound to receipt_sig_ed25519. The links, in order, each left out when it does not apply: 1. mandate — always: id, nonce, scope, currency, then sig (v1-v3) or sig_sha256 (v4), from mandate. sig is in the read only for a credential that can spend (see mandate.sig), so any other caller verifies a v1-v3 receipt at the signature level only. 2. quote — when quote is not null: seller, resource, price_minor, payee, session_id, sig, at, nulls included. 3. approval — v4 only: items_hash, batch_hash (null when absent). 4. payment — always: rail, provider, tx_id, amount_minor, attempt_id, money_moved, at, plus each of amount_atomic, amount_authorized, amount_charged, amount_refunded, metering, sandbox only when present and not null. 5. delivery — when delivery is not null: result, proof, kind, nfe_chave, at. Values are copied as read; timestamps are RFC 3339 UTC with milliseconds. The same recipe is served as data in chain_recipe on the unauthenticated /.well-known/codespar-receipt-keys.json. Recomputing needs quote.payee unmasked, and a v1-v3 chain needs mandate.sig, so only the tenant can recompute those. The Ed25519 signing string does not change with the version: it covers this digest. |
chain_version | integer | yes | The chain format chain was computed with: 4 when approval is present; else 3 when payment.amount_authorized is present; else 2 when payment.amount_atomic is not null; else 1. A receipt keeps the version it was sealed under. |
delivery | object,null | yes | Null until a delivery proof has been sealed onto the receipt. |
exceptions | array of object | yes | — |
mandate | object | yes | — |
payment | object | yes | — |
quote | object,null | yes | Null when the receipt carries neither a seller nor a resource. |
receipt_id | string | yes | — |
receipt_sig | string | yes | — |
receipt_sig_ed25519 | string,null | yes | base64url Ed25519 signature over codespar-receipt:v1:<receipt_id>:<chain>, made with the platform issuer key. Verifiable by anyone: fetch /.well-known/codespar-receipt-keys.json (no credential), take the key whose kid is the one below, and check the signature over that string. Null on a receipt sealed before this existed, and on one sealed while the key could not be resolved — receipt_sig is the HMAC seal either way and is unaffected. |
receipt_sig_kid | string,null | yes | Which published key signed it, <did:web:...>#<namespace>-<n>. Keys rotate and a retired key stays published, so an old receipt names its own key rather than leaving a verifier to try each one. The namespace names the DEPLOYMENT that minted the key (ent#1641), so a kid absent from the key document you fetched means the receipt came from another environment — unknown_key, not a tampered receipt. |
state | "paid" | "delivered" | "exception" | "voided" | yes | — |
curl -X POST https://api.codespar.dev/v1/consumers/receipts/{id}/delivery \
-H "Authorization: Bearer $CODESPAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"result": "confirmed",
"proof": "string",
"kind": "order_confirmation",
"nfe_chave": "string",
"at": "2026-01-15T12:00:00.000Z"
}'POST /v1/consumers/receipts/{id}/delivery HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json
{
"result": "confirmed",
"proof": "string",
"kind": "order_confirmation",
"nfe_chave": "string",
"at": "2026-01-15T12:00:00.000Z"
}import os
import requests
res = requests.post(
"https://api.codespar.dev/v1/consumers/receipts/{id}/delivery",
headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
json={
"result": "confirmed",
"proof": "string",
"kind": "order_confirmation",
"nfe_chave": "string",
"at": "2026-01-15T12:00:00.000Z"
},
)
res.raise_for_status()
data = res.json()const res = await fetch("https://api.codespar.dev/v1/consumers/receipts/{id}/delivery", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"result": "confirmed",
"proof": "string",
"kind": "order_confirmation",
"nfe_chave": "string",
"at": "2026-01-15T12:00:00.000Z"
}),
});
const data = await res.json();const result = await cs.api.post("/v1/consumers/receipts/{id}/delivery", {
path: {
id: "receipt_0000000000000000"
},
body: {
result: "confirmed",
proof: "string",
kind: "order_confirmation",
nfe_chave: "string",
at: "2026-01-15T12:00:00.000Z"
}
});{
"receipt_id": "receipt_0000000000000000",
"state": "paid",
"actor": {
"type": "agent",
"id": "obj_0000000000000000",
"on_behalf_of": "string"
},
"mandate": {
"id": "obj_0000000000000000",
"nonce": "string",
"scope": "string",
"currency": "BRL",
"sig": "string",
"sig_sha256": "string"
},
"quote": {
"seller": "string",
"resource": "string",
"price_minor": 1,
"payee": "string",
"session_id": "ses_0000000000000000",
"sig": "string",
"at": "string"
},
"approval": {
"items_hash": "string",
"batch_hash": "string"
},
"payment": {
"rail": "string",
"provider": "string",
"tx_id": "tx_0000000000000000",
"amount_minor": 1000,
"amount_atomic": "1000",
"amount_authorized": "1000",
"amount_charged": "1000",
"amount_refunded": "1000",
"metering": {
"basis": "string",
"units": "string",
"unit_price": "string"
},
"sandbox": true,
"attempt_id": "attempt_0000000000000000",
"money_moved": true,
"at": "string"
},
"delivery": {
"result": "confirmed",
"proof": "string",
"kind": "string",
"nfe_chave": "string",
"at": "string"
},
"chain": "string",
"chain_version": 0,
"receipt_sig": "string",
"receipt_sig_ed25519": "string",
"receipt_sig_kid": "string",
"exceptions": [
{
"code": "string",
"detail": "string",
"at": "string"
}
]
}GET /v1/consumers/{consumerId}/receipts
https://api.codespar.dev/v1/consumers/{consumerId}/receiptsA consumer's agentic receipts
Every sealed receipt for this consumer, newest first. The receipt is the control record of an agent payment: mandate, quote, payment and delivery, hash-chained and signed.
Scoped to the credential's PROJECT as well as its organization, because a project is the environment boundary and a receipt carries signed payment evidence. A live key does not see a test project's receipts, and the reverse.
Returns at most 50 receipts and takes no query parameters: there is no limit, no cursor and no filter on this route today. A consumer with no receipts, and a consumer this project cannot see, both answer 200 with an empty array.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
consumerId | string | yes | — |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
receipts | array of object | yes | — |
curl -X GET https://api.codespar.dev/v1/consumers/{consumerId}/receipts \
-H "Authorization: Bearer $CODESPAR_API_KEY"GET /v1/consumers/{consumerId}/receipts HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEYimport os
import requests
res = requests.get(
"https://api.codespar.dev/v1/consumers/{consumerId}/receipts",
headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
)
res.raise_for_status()
data = res.json()const res = await fetch("https://api.codespar.dev/v1/consumers/{consumerId}/receipts", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
},
});
const data = await res.json();const result = await cs.api.get("/v1/consumers/{consumerId}/receipts", {
path: {
consumerId: "csm_0000000000000000"
}
});{
"receipts": [
{
"receipt_id": "receipt_0000000000000000",
"state": "paid",
"actor": {
"type": "agent",
"id": "obj_0000000000000000",
"on_behalf_of": "string"
},
"mandate": {
"id": "obj_0000000000000000",
"nonce": "string",
"scope": "string",
"currency": "BRL",
"sig": "string",
"sig_sha256": "string"
},
"quote": {
"seller": "string",
"resource": "string",
"price_minor": 1,
"payee": "string",
"session_id": "ses_0000000000000000",
"sig": "string",
"at": "string"
},
"approval": {
"items_hash": "string",
"batch_hash": "string"
},
"payment": {
"rail": "string",
"provider": "string",
"tx_id": "tx_0000000000000000",
"amount_minor": 1000,
"amount_atomic": "1000",
"amount_authorized": "1000",
"amount_charged": "1000",
"amount_refunded": "1000",
"metering": {
"basis": "string",
"units": "string",
"unit_price": "string"
},
"sandbox": true,
"attempt_id": "attempt_0000000000000000",
"money_moved": true,
"at": "string"
},
"delivery": {
"result": "confirmed",
"proof": "string",
"kind": "string",
"nfe_chave": "string",
"at": "string"
},
"chain": "string",
"chain_version": 0,
"receipt_sig": "string",
"receipt_sig_ed25519": "string",
"receipt_sig_kid": "string",
"exceptions": [
{
"code": "string",
"detail": "string",
"at": "string"
}
]
}
]
}Pix in
2 operations under /v1/consumers/{consumerId}/pix (GET): parameters, status codes, refusal bodies, and the same request in curl, HTTP, Python and TypeScript.
Consumer funding
4 operations under /v1/consumers/{consumerId}/fund (POST GET): parameters, status codes, refusal bodies, and the same request in curl, HTTP, Python and TypeScript.