Skip to main content

Consumer Payments

2 operations under /v1/consumer-payments (POST): parameters, status codes, refusal bodies, and the same request in curl, HTTP, Python and TypeScript.

13 min read
View MarkdownEdit on GitHub

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

POSThttps://api.codespar.dev/v1/consumer-payments/execute
Moves money

attribution 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:

  1. attempt_id is 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.
  2. payee must 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.
  3. 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 is withdrawal_allowlist alone, and while the rollout flag is off a destination named explicitly in the signed merchant_allowlist is 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 against withdrawal_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. A retry of an attempt that already holds money is not evaluated against the organization's policy rules again, so a budget or a rate limit cannot refuse the retry that finds out what happened.

A settled attempt answers its recorded response. Presenting the attempt_id of an attempt that already settled, with the same amount, payee, mandate, rail, quote and approval, returns the ORIGINAL 200 body verbatim (same transactionId, same receipt) with idempotent_replay: true, and nothing is dispatched, held, sealed or published. This holds on every rail and in test mode alike, and while the organization is paused, since the answer moves no money. Presenting it with any of those parameters changed is attempt_id_conflict. Idempotency is opt-in: a request without an attempt_id is its own payment and is never replayed, so two spends under one mandate are two payments. Send an attempt_id to make a retry safe.

Request body

FieldTypeRequiredDescription
actorPaymentActornoWHO triggered this spend, recorded on the receipt and read back from it. Optional, and absence is a real answer: a spend that sends no actor records null, and nothing is ever inferred from agent_id — that field names the agent the mandate was SIGNED for, which is an authority, not an event. A person acting through WhatsApp under an agent's mandate and the agent acting unattended are the same row without this field.
agent_idstringyes—
amount_minorintegeryes—
approvalSpendApprovalnoThe hash of what the caller says a person approved: items_hash for this spend's lines, and for a batch batch_hash over the whole presented list. When present it is SEALED into the receipt's signed chain as its own link (chain version 4), so it cannot be stripped or altered afterwards without breaking both receipt_sig and receipt_sig_ed25519, and the receipt reads return it. It is a SEALED CLAIM by the caller, NOT a server-side approval check. The server only validates the shape; it never interprets the value, never compares it with anything and never refuses a spend because of it. It proves that this payment was sealed together with this hash, so whoever holds the approval artifact can recompute the hash and hold it against the receipt. It does not prove that anybody approved anything, or who. A malformed value, or a key other than these two, is refused with invalid_approval_hash before anything is read, held or sent. Part of what makes two requests with one attempt_id the same payment: a repeat with a different approval is attempt_id_conflict.
attempt_idstringno—
mandate—noThe canonical mandate, in the form it was signed in.
payeestringyes—
purposestringyes—
quoteSpendQuotenoThe 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.
session_idstringnoThe session this payment is made in: stamped on its hold and debit, so the session shows the amount it paid. Must name a session of this project, or 422 session_not_found before any money step. Not the quote's session_id, which is a store checkout session.
signaturestringyesHMAC in hexadecimal, 64 characters.
tedTedDestinationnoThe 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

StatusBodyDescription
200SpendOutcomeOK
400objectThe body did not match the schema (invalid_body), approval is malformed (invalid_approval_hash), quote.at is not an RFC 3339 timestamp with an offset in UTC years 0001-9999 (invalid_receipt_timestamp), or actor.on_behalf_of names a consumer other than the one this mandate belongs to (actor_consumer_mismatch). Nothing was read, held or sent in any of these cases. attempt_id_required: the body names no attempt_id and the deployment enforces one (ATTEMPT_ID_REQUIRED_ENFORCE); add a stable id and reuse it on every retry of that payment. It is checked AFTER the body is valid, so a request that is both malformed and id-less is answered invalid_approval_hash, invalid_receipt_timestamp or invalid_body first.
403objectThe destination is not on the signed withdrawal allowlist (withdrawal_pin), or a Pix is above the mandate's per-transaction cap and waits for a person (approval_required, same body as on the by-id route). Nothing was sent to the provider.
409objectLocal state refuses, on the same five conditions as the by-id route.
422objectThe mandate refuses, the consumer has no funding source for this rail, or the payee is a Pix copia-e-cola that fails its own check (the three carrier_* codes). per_tx_cap_exceeded carries details.amount_minor, details.per_tx_cap_minor and details.currency, as on the by-id route. psp_refused: the provider declined, nothing moved.
500objectThe cumulative cap was not resolved by the verifier. Nothing is debited.
502objectThe provider refused, or the outcome is unknown.

Response 200

FieldTypeRequiredDescription
attempt_idstringyesThe attempt this payment settled under: the attempt_id sent, or the fresh one minted when none was. Presenting it again reads this outcome instead of paying again.
auditarray of —yesThe lifecycle steps, in order, each with a timestamp and an outcome.
idempotent_replaybooleanyesTrue when this attempt_id had already settled and this is its recorded outcome, answered again: every other field is the ORIGINAL response, verbatim, and nothing was sent, held, sealed or published by this call. False on the call that settled it.
mandateobjectyes—
paymentobjectyes—
receiptobject,nullyesThe sealed Control Record. Null when there was no consumer secret to sign it with.
requestIdstringyes—
status"completed"yes—
walletobjectyes—
Example request
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": "2026-01-15T12:00:00.000Z"
       },
       "actor": {
         "type": "agent",
         "id": "paymentactor_0000000000000000",
         "on_behalf_of": "string"
       },
       "approval": {
         "items_hash": "string",
         "batch_hash": "string"
       },
       "session_id": "ses_0000000000000000"
     }'
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": "2026-01-15T12:00:00.000Z"
  },
  "actor": {
    "type": "agent",
    "id": "paymentactor_0000000000000000",
    "on_behalf_of": "string"
  },
  "approval": {
    "items_hash": "string",
    "batch_hash": "string"
  },
  "session_id": "ses_0000000000000000"
}
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": "2026-01-15T12:00:00.000Z"
      },
      "actor": {
        "type": "agent",
        "id": "paymentactor_0000000000000000",
        "on_behalf_of": "string"
      },
      "approval": {
        "items_hash": "string",
        "batch_hash": "string"
      },
      "session_id": "ses_0000000000000000"
    },
)
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": "2026-01-15T12:00:00.000Z"
    },
    "actor": {
      "type": "agent",
      "id": "paymentactor_0000000000000000",
      "on_behalf_of": "string"
    },
    "approval": {
      "items_hash": "string",
      "batch_hash": "string"
    },
    "session_id": "ses_0000000000000000"
  }),
});

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: "2026-01-15T12:00:00.000Z"
    },
    actor: {
      type: "agent",
      id: "paymentactor_0000000000000000",
      on_behalf_of: "string"
    },
    approval: {
      items_hash: "string",
      batch_hash: "string"
    },
    session_id: "ses_0000000000000000"
  }
});
// 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);
}
Example response 200
application/json
{
  "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": [],
  "attempt_id": "attempt_0000000000000000",
  "idempotent_replay": true
}

POST /v1/consumer-payments/execute-stream

POSThttps://api.codespar.dev/v1/consumer-payments/execute-stream
Moves money

hold, 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. A retry of an attempt that already holds money is not evaluated against the organization's policy rules again, so a budget or a rate limit cannot refuse the retry that finds out what happened.

A settled attempt answers its recorded response. Presenting the attempt_id of an attempt that already settled, with the same amount, payee, mandate, rail, quote and approval, returns the ORIGINAL 200 body verbatim (same transactionId, same receipt) with idempotent_replay: true, and nothing is dispatched, held, sealed or published. This holds on every rail and in test mode alike, and while the organization is paused, since the answer moves no money. Presenting it with any of those parameters changed is attempt_id_conflict. Idempotency is opt-in: a request without an attempt_id is its own payment and is never replayed, so two spends under one mandate are two payments. Send an attempt_id to make a retry safe.

Request body

FieldTypeRequiredDescription
actorPaymentActornoWHO triggered this spend, recorded on the receipt and read back from it. Optional, and absence is a real answer: a spend that sends no actor records null, and nothing is ever inferred from agent_id — that field names the agent the mandate was SIGNED for, which is an authority, not an event. A person acting through WhatsApp under an agent's mandate and the agent acting unattended are the same row without this field.
agent_idstringyes—
amount_minorintegeryes—
approvalSpendApprovalnoThe hash of what the caller says a person approved: items_hash for this spend's lines, and for a batch batch_hash over the whole presented list. When present it is SEALED into the receipt's signed chain as its own link (chain version 4), so it cannot be stripped or altered afterwards without breaking both receipt_sig and receipt_sig_ed25519, and the receipt reads return it. It is a SEALED CLAIM by the caller, NOT a server-side approval check. The server only validates the shape; it never interprets the value, never compares it with anything and never refuses a spend because of it. It proves that this payment was sealed together with this hash, so whoever holds the approval artifact can recompute the hash and hold it against the receipt. It does not prove that anybody approved anything, or who. A malformed value, or a key other than these two, is refused with invalid_approval_hash before anything is read, held or sent. Part of what makes two requests with one attempt_id the same payment: a repeat with a different approval is attempt_id_conflict.
attempt_idstringno—
mandate—no—
payeestringyes—
purposestringyes—
quoteSpendQuotenoThe 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.
session_idstringnoThe session this payment is made in: stamped on its hold and debit, so the session shows the amount it paid. Must name a session of this project, or 422 session_not_found before any money step. Not the quote's session_id, which is a store checkout session.
signaturestringyes—
tedTedDestinationnoThe 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

StatusBodyDescription
200—An event stream. Each event is one lifecycle step; the last one carries the same outcome object /execute returns.
400objectThe body did not match the schema (invalid_body), approval is malformed (invalid_approval_hash), quote.at is not an RFC 3339 timestamp with an offset in UTC years 0001-9999 (invalid_receipt_timestamp), or actor.on_behalf_of names a consumer other than the one this mandate belongs to (actor_consumer_mismatch). Nothing was read, held or sent in any of these cases. attempt_id_required: the body names no attempt_id and the deployment enforces one (ATTEMPT_ID_REQUIRED_ENFORCE); add a stable id and reuse it on every retry of that payment. It is checked AFTER the body is valid, so a request that is both malformed and id-less is answered invalid_approval_hash, invalid_receipt_timestamp or invalid_body first.
403objectThe destination is not on the signed withdrawal allowlist (withdrawal_pin), or a Pix is above the per-transaction cap and waits for a person (approval_required, sent as the error frame with approval_id and payment_state).
422objectThe mandate refuses, or there is no funding source for this rail. per_tx_cap_exceeded arrives as the error frame with amount_minor, per_tx_cap_minor and currency. psp_refused: the provider declined, nothing moved.
502objectThe provider refused, or the outcome is unknown.
Example request
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": "2026-01-15T12:00:00.000Z"
       },
       "actor": {
         "type": "agent",
         "id": "paymentactor_0000000000000000",
         "on_behalf_of": "string"
       },
       "approval": {
         "items_hash": "string",
         "batch_hash": "string"
       },
       "session_id": "ses_0000000000000000"
     }'
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": "2026-01-15T12:00:00.000Z"
  },
  "actor": {
    "type": "agent",
    "id": "paymentactor_0000000000000000",
    "on_behalf_of": "string"
  },
  "approval": {
    "items_hash": "string",
    "batch_hash": "string"
  },
  "session_id": "ses_0000000000000000"
}
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": "2026-01-15T12:00:00.000Z"
      },
      "actor": {
        "type": "agent",
        "id": "paymentactor_0000000000000000",
        "on_behalf_of": "string"
      },
      "approval": {
        "items_hash": "string",
        "batch_hash": "string"
      },
      "session_id": "ses_0000000000000000"
    },
)
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": "2026-01-15T12:00:00.000Z"
    },
    "actor": {
      "type": "agent",
      "id": "paymentactor_0000000000000000",
      "on_behalf_of": "string"
    },
    "approval": {
      "items_hash": "string",
      "batch_hash": "string"
    },
    "session_id": "ses_0000000000000000"
  }),
});

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: "2026-01-15T12:00:00.000Z"
    },
    actor: {
      type: "agent",
      id: "paymentactor_0000000000000000",
      on_behalf_of: "string"
    },
    approval: {
      items_hash: "string",
      batch_hash: "string"
    },
    session_id: "ses_0000000000000000"
  }
});
// 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);
}
Consumer Payments | CodeSpar