Consumer Payments
2 operations under /v1/consumer-payments (POST): parameters, status codes, refusal bodies, and the same request in curl, HTTP, Python and TypeScript.
Consumer Payments
Base URL: https://api.codespar.dev
Every operation below requires a Bearer token. See Authentication.
These three operations are how money leaves a consumer's account under a signed mandate, over plain HTTP. Two of them take the mandate in the request; the third addresses one already projected here by id. All three run the same lifecycle, and it is the same lifecycle the codespar_pay meta-tool runs internally, so a partner who does not use the SDK reaches the same place by the same rules.
TED, the Brazilian same-day bank wire, is here. Present a ted object and the provider leg becomes a real wire debited from the consumer's own sub-account; omit it and payee is a Pix key or a copy-and-paste string. TED binds three requirements the other methods do not, all of them before any money moves, and each operation below spells them out. Money arriving the other way is not a route at all: an inbound TED reaches you as the commerce.ted_in.succeeded event, described in the trigger event catalog.
There is no read route for an outcome. A spend that answers psp_dispatch_uncertain cannot be looked up afterwards; you re-present the same attempt_id and the lifecycle answers where that attempt stopped. That gap is a missing surface, not a missing page, and it is tracked as such. Until it closes, treat the attempt_id you sent as the only handle you have.
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.
POST /v1/consumer-payments/execute
https://api.codespar.dev/v1/consumer-payments/executeattribution of the spend belongs to the mandate
Spend by presenting the signed mandate
The same lifecycle as the by-id route, for a caller who carries the signed envelope instead of a mandate already projected here. Use this one when the mandate arrives from the consumer at call time; use /consumers/mandates/{id}/spend when it is already registered here.
signature is the 64-character hexadecimal HMAC over the canonical mandate. The body's agent_id is checked against the SIGNED one: attribution of the spend belongs to the mandate, not to the caller.
TED carries three requirements the other payment methods do not, and all three bind BEFORE any money step:
attempt_idis MANDATORY. A TED has no copy-and-paste string and no nonce to derive correlation from, and two TEDs under the same mandate must never collide on a derived id.payeemust EQUAL the destination's canonical string,ted:<ispb>:<branch>:<account>, alphanumerics only and upper-cased. The comparison normalizes whitespace and nothing else, so an entry carrying a dot or a hyphen does NOT match. The rule is what makes the signed-allowlist pin and the wire name the SAME destination: without it, an approved payee fronts for a different bank account.- The destination must be NAMED in a signed list of the mandate, and a merchant-list wildcard
"*"NEVER authorizes money out. Which list is accepted is mid-rollout, so read this one carefully: the target rule iswithdrawal_allowlistalone, and while the rollout flag is off a destination named explicitly in the signedmerchant_allowlistis ALSO accepted, with the stricter rule's refusal recorded rather than applied. Tolerance never widens what passes: a wildcard is refused on both paths, and a destination in neither list is refused with its own code. Build againstwithdrawal_allowlist: it is the list that keeps working when the flag turns on.
There is no read route for the outcome. An attempt_id that answered psp_dispatch_uncertain has no endpoint to ask about afterwards. Keep the attempt_id and present it again: the lifecycle is idempotent on it and answers the state that attempt stopped in, instead of firing a second one. Do NOT restart the same intent under a fresh attempt_id. That is how a payment gets made twice.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
agent_id | string | yes | — |
amount_minor | integer | yes | — |
attempt_id | string | no | — |
mandate | — | no | The canonical mandate, in the form it was signed in. |
payee | string | yes | — |
purpose | string | yes | — |
quote | SpendQuote | no | The offer the agent approved. When present it is signed into the receipt, and at close the price and the payee are compared against what actually settled. A divergence is RECORDED on the receipt; it does not abort settlement. |
signature | string | yes | HMAC in hexadecimal, 64 characters. |
ted | TedDestination | no | The destination of a TED, the Brazilian same-day bank wire. Present this object on a spend to send a wire instead of a Pix: its presence is what switches the rail, and what makes the three TED requirements on those operations bind. Money leaves the consumer's own sub-account, never a pooled one. |
Responses
| Status | Body | Description |
|---|---|---|
200 | SpendOutcome | OK |
400 | object | The body did not match the schema. |
403 | object | The destination is not on the signed withdrawal allowlist. Nothing was sent to the provider. |
409 | object | Local state refuses, on the same three conditions as the by-id route. |
422 | object | The mandate refuses, or the consumer has no funding source for this rail. |
500 | object | The cumulative cap was not resolved by the verifier. Nothing is debited. |
502 | object | The provider refused, or the outcome is unknown. |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
audit | array of — | yes | The lifecycle steps, in order, each with a timestamp and an outcome. |
mandate | object | yes | — |
payment | object | yes | — |
receipt | object,null | yes | The sealed Control Record. Null when there was no consumer secret to sign it with. |
requestId | string | yes | — |
status | "completed" | yes | — |
wallet | object | yes | — |
curl -X POST https://api.codespar.dev/v1/consumer-payments/execute \
-H "Authorization: Bearer $CODESPAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"signature": "string",
"amount_minor": 1000,
"purpose": "string",
"agent_id": "agt_0000000000000000",
"payee": "string",
"attempt_id": "attempt_0000000000000000",
"ted": {
"bank": "string",
"branch": "string",
"account": "string",
"tax_id": "tax_0000000000000000",
"name": "Example",
"account_type": "CC",
"person_type": "F",
"client_finality": "string"
},
"quote": {
"seller": "string",
"resource": "string",
"price_minor": 1,
"payee": "string",
"session_id": "ses_0000000000000000",
"at": "string"
}
}'POST /v1/consumer-payments/execute HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json
{
"signature": "string",
"amount_minor": 1000,
"purpose": "string",
"agent_id": "agt_0000000000000000",
"payee": "string",
"attempt_id": "attempt_0000000000000000",
"ted": {
"bank": "string",
"branch": "string",
"account": "string",
"tax_id": "tax_0000000000000000",
"name": "Example",
"account_type": "CC",
"person_type": "F",
"client_finality": "string"
},
"quote": {
"seller": "string",
"resource": "string",
"price_minor": 1,
"payee": "string",
"session_id": "ses_0000000000000000",
"at": "string"
}
}import os
import requests
res = requests.post(
"https://api.codespar.dev/v1/consumer-payments/execute",
headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
json={
"signature": "string",
"amount_minor": 1000,
"purpose": "string",
"agent_id": "agt_0000000000000000",
"payee": "string",
"attempt_id": "attempt_0000000000000000",
"ted": {
"bank": "string",
"branch": "string",
"account": "string",
"tax_id": "tax_0000000000000000",
"name": "Example",
"account_type": "CC",
"person_type": "F",
"client_finality": "string"
},
"quote": {
"seller": "string",
"resource": "string",
"price_minor": 1,
"payee": "string",
"session_id": "ses_0000000000000000",
"at": "string"
}
},
)
res.raise_for_status()
data = res.json()const res = await fetch("https://api.codespar.dev/v1/consumer-payments/execute", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"signature": "string",
"amount_minor": 1000,
"purpose": "string",
"agent_id": "agt_0000000000000000",
"payee": "string",
"attempt_id": "attempt_0000000000000000",
"ted": {
"bank": "string",
"branch": "string",
"account": "string",
"tax_id": "tax_0000000000000000",
"name": "Example",
"account_type": "CC",
"person_type": "F",
"client_finality": "string"
},
"quote": {
"seller": "string",
"resource": "string",
"price_minor": 1,
"payee": "string",
"session_id": "ses_0000000000000000",
"at": "string"
}
}),
});
const data = await res.json();const r = await cs.api.response("post", "/v1/consumer-payments/execute", {
body: {
signature: "string",
amount_minor: 1000,
purpose: "string",
agent_id: "agt_0000000000000000",
payee: "string",
attempt_id: "attempt_0000000000000000",
ted: {
bank: "string",
branch: "string",
account: "string",
tax_id: "tax_0000000000000000",
name: "Example",
account_type: "CC",
person_type: "F",
client_finality: "string"
},
quote: {
seller: "string",
resource: "string",
price_minor: 1,
payee: "string",
session_id: "ses_0000000000000000",
at: "string"
}
}
});
// r.status is one of the documented statuses (200, 403, 422),
// each with its own body shape in r.data; nothing here throws on 403.
if (r.ok) {
console.log(r.data);
}{
"status": "completed",
"requestId": "request_0000000000000000",
"mandate": {
"consumer_id": "csm_0000000000000000",
"purpose": "string",
"currency": "BRL"
},
"payment": {
"transactionId": "transaction_0000000000000000",
"endToEndId": "endtoend_0000000000000000",
"amountMinor": 1000,
"rail": "string",
"provider": "string",
"moneyMoved": true,
"adapter": "string"
},
"wallet": {
"walletId": "wlt_0000000000000000",
"holdEntryId": "holdentry_0000000000000000",
"fundEntryId": "fundentry_0000000000000000",
"debitEntryId": "debitentry_0000000000000000"
},
"receipt": {
"id": "spendoutcome_0000000000000000",
"state": "paid",
"chain": "string",
"exceptions": []
},
"audit": []
}POST /v1/consumer-payments/execute-stream
https://api.codespar.dev/v1/consumer-payments/execute-streamhold, provider debit, receipt seal
Spend by presenting the mandate, with the steps streamed
Same body and same gates as /v1/consumer-payments/execute. Only the response transport differs: the lifecycle steps (mandate verification, funding-source routing, hold, provider debit, receipt seal) arrive as text/event-stream events as they happen, instead of one JSON at the end.
The outcome is the same object the sibling route returns, carried on the last event. A client that does not need to follow progress should use /execute: streaming does not make settlement faster, it makes it observable while it runs.
There is no read route for the outcome. An attempt_id that answered psp_dispatch_uncertain has no endpoint to ask about afterwards. Keep the attempt_id and present it again: the lifecycle is idempotent on it and answers the state that attempt stopped in, instead of firing a second one. Do NOT restart the same intent under a fresh attempt_id. That is how a payment gets made twice.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
agent_id | string | yes | — |
amount_minor | integer | yes | — |
attempt_id | string | no | — |
mandate | — | no | — |
payee | string | yes | — |
purpose | string | yes | — |
quote | SpendQuote | no | The offer the agent approved. When present it is signed into the receipt, and at close the price and the payee are compared against what actually settled. A divergence is RECORDED on the receipt; it does not abort settlement. |
signature | string | yes | — |
ted | TedDestination | no | The destination of a TED, the Brazilian same-day bank wire. Present this object on a spend to send a wire instead of a Pix: its presence is what switches the rail, and what makes the three TED requirements on those operations bind. Money leaves the consumer's own sub-account, never a pooled one. |
Responses
| Status | Body | Description |
|---|---|---|
200 | — | An event stream. Each event is one lifecycle step; the last one carries the same outcome object /execute returns. |
400 | object | The body did not match the schema. |
403 | object | The destination is not on the signed withdrawal allowlist. |
422 | object | The mandate refuses, or there is no funding source for this rail. |
502 | object | The provider refused, or the outcome is unknown. |
curl -X POST https://api.codespar.dev/v1/consumer-payments/execute-stream \
-H "Authorization: Bearer $CODESPAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"signature": "string",
"amount_minor": 1000,
"purpose": "string",
"agent_id": "agt_0000000000000000",
"payee": "string",
"attempt_id": "attempt_0000000000000000",
"ted": {
"bank": "string",
"branch": "string",
"account": "string",
"tax_id": "tax_0000000000000000",
"name": "Example",
"account_type": "CC",
"person_type": "F",
"client_finality": "string"
},
"quote": {
"seller": "string",
"resource": "string",
"price_minor": 1,
"payee": "string",
"session_id": "ses_0000000000000000",
"at": "string"
}
}'POST /v1/consumer-payments/execute-stream HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json
{
"signature": "string",
"amount_minor": 1000,
"purpose": "string",
"agent_id": "agt_0000000000000000",
"payee": "string",
"attempt_id": "attempt_0000000000000000",
"ted": {
"bank": "string",
"branch": "string",
"account": "string",
"tax_id": "tax_0000000000000000",
"name": "Example",
"account_type": "CC",
"person_type": "F",
"client_finality": "string"
},
"quote": {
"seller": "string",
"resource": "string",
"price_minor": 1,
"payee": "string",
"session_id": "ses_0000000000000000",
"at": "string"
}
}import os
import requests
res = requests.post(
"https://api.codespar.dev/v1/consumer-payments/execute-stream",
headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
json={
"signature": "string",
"amount_minor": 1000,
"purpose": "string",
"agent_id": "agt_0000000000000000",
"payee": "string",
"attempt_id": "attempt_0000000000000000",
"ted": {
"bank": "string",
"branch": "string",
"account": "string",
"tax_id": "tax_0000000000000000",
"name": "Example",
"account_type": "CC",
"person_type": "F",
"client_finality": "string"
},
"quote": {
"seller": "string",
"resource": "string",
"price_minor": 1,
"payee": "string",
"session_id": "ses_0000000000000000",
"at": "string"
}
},
)
res.raise_for_status()
data = res.json()const res = await fetch("https://api.codespar.dev/v1/consumer-payments/execute-stream", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"signature": "string",
"amount_minor": 1000,
"purpose": "string",
"agent_id": "agt_0000000000000000",
"payee": "string",
"attempt_id": "attempt_0000000000000000",
"ted": {
"bank": "string",
"branch": "string",
"account": "string",
"tax_id": "tax_0000000000000000",
"name": "Example",
"account_type": "CC",
"person_type": "F",
"client_finality": "string"
},
"quote": {
"seller": "string",
"resource": "string",
"price_minor": 1,
"payee": "string",
"session_id": "ses_0000000000000000",
"at": "string"
}
}),
});
const data = await res.json();const r = await cs.api.response("post", "/v1/consumer-payments/execute-stream", {
body: {
signature: "string",
amount_minor: 1000,
purpose: "string",
agent_id: "agt_0000000000000000",
payee: "string",
attempt_id: "attempt_0000000000000000",
ted: {
bank: "string",
branch: "string",
account: "string",
tax_id: "tax_0000000000000000",
name: "Example",
account_type: "CC",
person_type: "F",
client_finality: "string"
},
quote: {
seller: "string",
resource: "string",
price_minor: 1,
payee: "string",
session_id: "ses_0000000000000000",
at: "string"
}
}
});
// r.status is one of the documented statuses (200, 403, 422),
// each with its own body shape in r.data; nothing here throws on 403.
if (r.ok) {
console.log(r.data);
}Wallets
15 operations under /v1/wallets (GET POST DELETE): parameters, status codes, refusal bodies, and the same request in curl, HTTP, Python and TypeScript.
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.