Skip to main content

Charges

5 operations under /v1/charges (GET POST): parameters, status codes, refusal bodies, and the same request in curl, HTTP, Python and TypeScript.

23 min read
View MarkdownEdit on GitHub

Base URL: https://api.codespar.dev

Every operation below requires a Bearer token. See Authentication.

GET /v1/charges

GEThttps://api.codespar.dev/v1/charges

List charges

The charges this project issued, newest first. Page with limit (1 to 200, default 50) and cursor, which is the created_at of the last row you saw; next_cursor comes back null when the page is the last one.

The settlement here is the LAST KNOWN, and the one on the by-id read is the one from now. The by-id read calls the issuer and compares; a list cannot do that per row, because 200 rows would be 200 provider calls and paging would become a flood. So every item carries settlement_as_of: when we last knew the issuer's state for that charge, or null if we never did.

The same charge can therefore answer a different settlement here and there. That is the age of the data, not a defect, which is why the freshness field is required rather than optional. A charge we never read carries settlement_as_of: null and can never be unconfirmable on this list, because there is nothing to conflict with.

settlement=none selects the charge with no settlement to report: nobody paid it, so there is no pending settlement, only an open receivable. It is not the same as omitting the filter.

Query parameters

NameTypeRequiredDescription
cursorstringno—
limitstringno—
settlement"confirmed" | "pending" | "unconfirmable" | "none"noFilters on the same three facts the field is derived from, in SQL.
statusstringnoOur own row status: pending, settled, expired.

Responses

StatusBodyDescription
200objectOK
400objectThe query is malformed, or the credential names no project.

Response 200

FieldTypeRequiredDescription
dataarray of objectyes—
next_cursorstring,nullyes—
Example request
curl -X GET https://api.codespar.dev/v1/charges \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
GET /v1/charges HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
import os
import requests

res = requests.get(
    "https://api.codespar.dev/v1/charges",
    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/charges", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
const result = await cs.api.get("/v1/charges");
Example response 200
application/json
{
  "data": [
    {
      "id": "obj_0000000000000000",
      "status": "string",
      "local_status": "string",
      "status_conflict": true,
      "currency": "BRL",
      "amount_minor": 1000,
      "due_date": "string",
      "created_at": "string",
      "settlement": "confirmed",
      "settlement_as_of": "string"
    }
  ],
  "next_cursor": "string"
}

POST /v1/charges

POSThttps://api.codespar.dev/v1/charges
Can move money

Issues an inbound charge: the buyer pays the merchant.

Issue a charge

Issues an inbound charge: the buyer pays the merchant. The RAIL IS A FIELD, not a path. method: "pix" mints an immediate Pix that hands back its copy-and-paste string right away and expires in about an hour. method: "boleto" WITH a due_date issues a cobrança com vencimento: ONE receivable the payer settles either as a boleto (barcode and linha digitável) or by Pix. One debt, two payable legs, never two documents.

The cobrança com vencimento answers PROCESSING with payable: false and no document. The instrument registers with the clearing house first. Subscribe to commerce.charge.created rather than polling for a barcode that is not late, only not made yet. commerce.charge.paid is the event that means the funds arrived, by either leg.

idempotency_key is REQUIRED for that combination, and it is checked before anything reaches the issuer. A repeat with the same key returns the charge already issued, or its still-open reservation, instead of a second receivable the same debtor could pay twice. Send it as the Idempotency-Key header, as the body's idempotency_key, or as both with the SAME value; two different values are refused rather than one silently winning.

consumer_id is REQUIRED for a boleto too: it names the merchant-side consumer the receivable settles into, and its wallet is what the payment credits. Without it there is nothing to settle into and the call is refused with consumer_id_required before anything is sent. In a test project any stable id of your own works: the project's shared-sandbox receiver stands in for the consumer's account at the issuer (the consumer is never onboarded there), and POST /v1/test/charges/{chargeId}/pay settles the charge into that consumer's wallet.

An agreement in N instalments is N cobranças, one per parcela, each with its own due_date and its own key. There is no single instalment charge.

Each refusal names a reason, and the reason is what to branch on. The two error code values (invalid_args, provider_error) are too coarse: they cover six different situations, and reading them as one would turn not found into bad request.

reasonstatuswhat it means
charge_id_missing400the call named no charge
project_scope_missing400the credential is not project-scoped, and a charge belongs to one project
consumer_id_required400a boleto create named no consumer_id, so there is no wallet for the receivable to settle into
charge_not_found404no charge under that id FOR THIS TENANT
charge_reference_ambiguous409the reference is one charge's id AND another's idempotency key; query by the charge id
issuance_unconfirmed409the key holds a reservation whose create never got an answer
already_payment_notified409the clearing house already notified a payment
not_cancellable_in_this_state409the issuer accepts a cancellation only in some states
provider_status_unreadable502the issuer did not answer the read
provider_cancel_failed502the issuer refused the withdrawal

charge_not_found is deliberately the SAME answer for an id that belongs to another project and for an id that exists nowhere. Telling those apart would be an oracle about another tenant over a string anyone can guess.

Request body

FieldTypeRequiredDescription
amountnumberyesIn MAJOR units of currency, NOT minor: 12.5 is R$ 12,50 and comes back as amount_minor: 1250. Sending 1250 here issues a R$ 1.250,00 charge. This is the one field of the charge family that takes major units; every *_minor field, including the response's amount_minor, is in centavos.
buyerobjectno—
consumer_idstringnoThe merchant-side consumer the charge settles into: its wallet is credited when the debtor pays. REQUIRED for boleto (refused with consumer_id_required otherwise); optional for the other methods, where an absent value falls back to the session's user when there is one.
countrystringno—
currencystringyes—
descriptionstringno—
due_datestringnoYYYY-MM-DD. With method: "boleto" this is what makes it a cobrança com vencimento; an immediate Pix has no due date and refuses one.
idempotency_keystringnoRequired for a cobrança com vencimento. Stable per debt: it is what makes a retry the SAME charge instead of a second one.
metadataobjectno—
methodstringyespix, boleto, card or wallet.

Responses

StatusBodyDescription
200objectOK
400objectThe call is malformed, names no consumer for a boleto, or the two idempotency keys disagree.
409objectThe idempotency slot is held by a row this project cannot read.
502objectThe slot could not be reserved, or the issuer refused. Nothing was sent.

Response 200

FieldTypeRequiredDescription
amountnumberyesIn MAJOR units of currency: 12.5 is R$ 12,50. Always amount_minor / 100.
amount_minorintegeryesIn minor units (centavos): 1250 is R$ 12,50.
boleto_bank_linestring,nullyes—
boleto_bar_codestring,nullyes—
credit_correlation_armedbooleanyesWhether an inbound Pix on this charge's Pix leg can resolve a wallet on its own. False until registration hands us the leg's identifier.
currencystringyes—
due_datestring,nullyes—
idstring,nullyesThe issuer's charge id. Null while the issuance is unconfirmed.
issuance_unconfirmedbooleanyesTrue while the row is a reservation with no charge id: the issuer's answer to the create was lost, or a create is in flight elsewhere under the same key. Nothing is payable and nothing was duplicated.
local_statusstringyesWhat CodeSpar recorded, which is a different question from status.
methodstringyes—
payablebooleanyesTrue only when all four hold: the issuance is confirmed, our row is still open, the provider says PENDING, and at least one document exists. This is the field to branch on, not status.
payment_in_flightbooleanyesTrue when a payment was notified and the receivable is still open. Read this before you write anything off: the issuer can answer EXPIRED for a boleto it already told us was paid, because the baixa happened before the due date and the credit lands on the next business day.
pix_copy_pastestring,nullyesThe Pix leg of the same debt. One receivable, two payable legs.
settlement"confirmed" | "pending" | "unconfirmable"yesThe settlement tri-state (decision 10b of the canonical matrix). confirmed: the money is in the account. pending: the clearing house notified a payment and the credit has not landed, which is the D+1 window. unconfirmable: our terminal state disagrees with the issuer's, so settlement cannot be established. NULL MEANS NOT APPLICABLE, never pending. A charge nobody paid has no pending settlement: it has an open receivable. Reading null as pending would tell you money is on its way.
statusstringyesThe provider's state as far as we know it, normalized (PROCESSING, PENDING, CONFIRMED, CANCELLED, EXPIRED, ERROR). Forced to PROCESSING until the issuance is confirmed. ERROR is the issuer rejecting the charge: no boleto and no Pix were ever issued, so nobody can pay it. A charge settled by the sandbox payer answers CONFIRMED here, the same value a real payment leaves a live read answering — so a client that polls status sees the test payment exactly where it would see a live one. The issuer's homolog account never sees that money and keeps answering PENDING (EXPIRED after the due date); that answer stays in raw and is not reported as status or as a status_conflict, because for a test charge the fixture is the issuer's truth. settlement: "confirmed" and local_status: "settled" say the same thing from our side. Any other issuer answer (ERROR, CANCELLED, PROCESSING) is reported as the issuer said it, with status_conflict: true.
status_conflictbooleanyesTrue whenever the issuer's known state and ours disagree, instead of one of them silently winning. They agree only as follows: our pending beside the issuer's PENDING or PROCESSING; our settled beside CONFIRMED or PENDING (the issuer's read can lag a payment); our expired beside CANCELLED or PENDING (lag after a cancel). Every other pair is a conflict, including an issuer ERROR, an issuer terminal state on a charge we still hold open, and a status spelling we do not recognise. False when no issuer state is known at all. settlement: "unconfirmable" is the narrower case of both sides terminal and different.
Example request
curl -X POST https://api.codespar.dev/v1/charges \
  -H "Authorization: Bearer $CODESPAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "amount": 1000,
       "currency": "BRL",
       "method": "string",
       "consumer_id": "csm_0000000000000000",
       "description": "string",
       "buyer": {},
       "due_date": "string",
       "idempotency_key": "string",
       "country": "string",
       "metadata": {}
     }'
POST /v1/charges HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json

{
  "amount": 1000,
  "currency": "BRL",
  "method": "string",
  "consumer_id": "csm_0000000000000000",
  "description": "string",
  "buyer": {},
  "due_date": "string",
  "idempotency_key": "string",
  "country": "string",
  "metadata": {}
}
import os
import requests

res = requests.post(
    "https://api.codespar.dev/v1/charges",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
    json={
      "amount": 1000,
      "currency": "BRL",
      "method": "string",
      "consumer_id": "csm_0000000000000000",
      "description": "string",
      "buyer": {},
      "due_date": "string",
      "idempotency_key": "string",
      "country": "string",
      "metadata": {}
    },
)
res.raise_for_status()
data = res.json()
const res = await fetch("https://api.codespar.dev/v1/charges", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "amount": 1000,
    "currency": "BRL",
    "method": "string",
    "consumer_id": "csm_0000000000000000",
    "description": "string",
    "buyer": {},
    "due_date": "string",
    "idempotency_key": "string",
    "country": "string",
    "metadata": {}
  }),
});

const data = await res.json();
const result = await cs.api.post("/v1/charges", {
  body: {
    amount: 1000,
    currency: "BRL",
    method: "string",
    consumer_id: "csm_0000000000000000",
    description: "string",
    buyer: {},
    due_date: "string",
    idempotency_key: "string",
    country: "string",
    metadata: {}
  }
});
Example response 200
application/json
{
  "id": "obj_0000000000000000",
  "status": "string",
  "local_status": "string",
  "status_conflict": true,
  "method": "string",
  "currency": "BRL",
  "amount": 1000,
  "amount_minor": 1000,
  "due_date": "string",
  "payable": true,
  "boleto_bar_code": "string",
  "boleto_bank_line": "string",
  "pix_copy_paste": "string",
  "credit_correlation_armed": true,
  "payment_in_flight": true,
  "settlement": "confirmed",
  "issuance_unconfirmed": true
}

GET /v1/charges/{chargeId}

GEThttps://api.codespar.dev/v1/charges/{chargeId}

Read a charge

Reads a charge this project issued. chargeId accepts EITHER the id the create returned (the issuer's charge transaction id) OR the caller's own idempotency_key — the row is resolved under this credential's org and project, which is what makes another tenant's id simply not found. A value that is one charge's id AND another charge's key is refused with charge_reference_ambiguous rather than answered for whichever came first; query by the charge id in that case. The Pix leg's own transaction id is not a reference here.

A charge the sandbox payer settled answers status: "CONFIRMED" with settlement: "confirmed" and local_status: "settled" — see status on the response for why the issuer's own PENDING is not reported.

Branch on payable, not on status. payable is true only when the issuance is confirmed, our row is still open, the provider says PENDING, and at least one payable document exists. A cobrança com vencimento answers PROCESSING with payable: false and no document at first, because the instrument registers with the clearing house before it can be paid.

This read also ARMS the Pix leg's correlation when the provider's answer carries it, which is why credit_correlation_armed can flip from a read. Nothing else about it writes: no ledger row, no money.

Each refusal names a reason, and the reason is what to branch on. The two error code values (invalid_args, provider_error) are too coarse: they cover six different situations, and reading them as one would turn not found into bad request.

reasonstatuswhat it means
charge_id_missing400the call named no charge
project_scope_missing400the credential is not project-scoped, and a charge belongs to one project
consumer_id_required400a boleto create named no consumer_id, so there is no wallet for the receivable to settle into
charge_not_found404no charge under that id FOR THIS TENANT
charge_reference_ambiguous409the reference is one charge's id AND another's idempotency key; query by the charge id
issuance_unconfirmed409the key holds a reservation whose create never got an answer
already_payment_notified409the clearing house already notified a payment
not_cancellable_in_this_state409the issuer accepts a cancellation only in some states
provider_status_unreadable502the issuer did not answer the read
provider_cancel_failed502the issuer refused the withdrawal

charge_not_found is deliberately the SAME answer for an id that belongs to another project and for an id that exists nowhere. Telling those apart would be an oracle about another tenant over a string anyone can guess.

Path parameters

NameTypeRequiredDescription
chargeIdstringyes—

Responses

StatusBodyDescription
200objectOK
400objectThe call is malformed. reason says which half.
404objectNo charge under that id for this tenant.
409objectThe charge exists and is not readable in this state, or the reference names two charges.
502objectThe issuer did not answer.

Response 200

FieldTypeRequiredDescription
amountnumberyesIn MAJOR units of currency: 12.5 is R$ 12,50. Always amount_minor / 100.
amount_minorintegeryesIn minor units (centavos): 1250 is R$ 12,50.
boleto_bank_linestring,nullyes—
boleto_bar_codestring,nullyes—
credit_correlation_armedbooleanyesWhether an inbound Pix on this charge's Pix leg can resolve a wallet on its own. False until registration hands us the leg's identifier.
currencystringyes—
due_datestring,nullyes—
idstring,nullyesThe issuer's charge id. Null while the issuance is unconfirmed.
issuance_unconfirmedbooleanyesTrue while the row is a reservation with no charge id: the issuer's answer to the create was lost, or a create is in flight elsewhere under the same key. Nothing is payable and nothing was duplicated.
local_statusstringyesWhat CodeSpar recorded, which is a different question from status.
methodstringyes—
payablebooleanyesTrue only when all four hold: the issuance is confirmed, our row is still open, the provider says PENDING, and at least one document exists. This is the field to branch on, not status.
payment_in_flightbooleanyesTrue when a payment was notified and the receivable is still open. Read this before you write anything off: the issuer can answer EXPIRED for a boleto it already told us was paid, because the baixa happened before the due date and the credit lands on the next business day.
pix_copy_pastestring,nullyesThe Pix leg of the same debt. One receivable, two payable legs.
settlement"confirmed" | "pending" | "unconfirmable"yesThe settlement tri-state (decision 10b of the canonical matrix). confirmed: the money is in the account. pending: the clearing house notified a payment and the credit has not landed, which is the D+1 window. unconfirmable: our terminal state disagrees with the issuer's, so settlement cannot be established. NULL MEANS NOT APPLICABLE, never pending. A charge nobody paid has no pending settlement: it has an open receivable. Reading null as pending would tell you money is on its way.
statusstringyesThe provider's state as far as we know it, normalized (PROCESSING, PENDING, CONFIRMED, CANCELLED, EXPIRED, ERROR). Forced to PROCESSING until the issuance is confirmed. ERROR is the issuer rejecting the charge: no boleto and no Pix were ever issued, so nobody can pay it. A charge settled by the sandbox payer answers CONFIRMED here, the same value a real payment leaves a live read answering — so a client that polls status sees the test payment exactly where it would see a live one. The issuer's homolog account never sees that money and keeps answering PENDING (EXPIRED after the due date); that answer stays in raw and is not reported as status or as a status_conflict, because for a test charge the fixture is the issuer's truth. settlement: "confirmed" and local_status: "settled" say the same thing from our side. Any other issuer answer (ERROR, CANCELLED, PROCESSING) is reported as the issuer said it, with status_conflict: true.
status_conflictbooleanyesTrue whenever the issuer's known state and ours disagree, instead of one of them silently winning. They agree only as follows: our pending beside the issuer's PENDING or PROCESSING; our settled beside CONFIRMED or PENDING (the issuer's read can lag a payment); our expired beside CANCELLED or PENDING (lag after a cancel). Every other pair is a conflict, including an issuer ERROR, an issuer terminal state on a charge we still hold open, and a status spelling we do not recognise. False when no issuer state is known at all. settlement: "unconfirmable" is the narrower case of both sides terminal and different.
Example request
curl -X GET https://api.codespar.dev/v1/charges/{chargeId} \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
GET /v1/charges/{chargeId} HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
import os
import requests

res = requests.get(
    "https://api.codespar.dev/v1/charges/{chargeId}",
    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/charges/{chargeId}", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
const result = await cs.api.get("/v1/charges/{chargeId}", {
  path: {
    chargeId: "charge_0000000000000000"
  }
});
Example response 200
application/json
{
  "id": "obj_0000000000000000",
  "status": "string",
  "local_status": "string",
  "status_conflict": true,
  "method": "string",
  "currency": "BRL",
  "amount": 1000,
  "amount_minor": 1000,
  "due_date": "string",
  "payable": true,
  "boleto_bar_code": "string",
  "boleto_bank_line": "string",
  "pix_copy_paste": "string",
  "credit_correlation_armed": true,
  "payment_in_flight": true,
  "settlement": "confirmed",
  "issuance_unconfirmed": true
}

POST /v1/charges/{chargeId}/cancel

POSThttps://api.codespar.dev/v1/charges/{chargeId}/cancel

Withdraw a charge

Withdraws a charge that has not been paid. Takes no body: the id is the whole request, and it accepts the same two forms the read does.

This never moves money. It is not a refund: an unpaid receivable is withdrawn, and a charge whose payment the clearing house already notified is REFUSED with already_payment_notified rather than withdrawn. That refusal is the D+1 guard: a boleto paid after the convênio's cut-off is notified on one day and credited on the next business day, so the due date can fall between the two, and withdrawing there would write off a receivable somebody paid.

A charge the issuer already reports as cancelled is a no-op that closes our row, not a second withdrawal.

Each refusal names a reason, and the reason is what to branch on. The two error code values (invalid_args, provider_error) are too coarse: they cover six different situations, and reading them as one would turn not found into bad request.

reasonstatuswhat it means
charge_id_missing400the call named no charge
project_scope_missing400the credential is not project-scoped, and a charge belongs to one project
consumer_id_required400a boleto create named no consumer_id, so there is no wallet for the receivable to settle into
charge_not_found404no charge under that id FOR THIS TENANT
charge_reference_ambiguous409the reference is one charge's id AND another's idempotency key; query by the charge id
issuance_unconfirmed409the key holds a reservation whose create never got an answer
already_payment_notified409the clearing house already notified a payment
not_cancellable_in_this_state409the issuer accepts a cancellation only in some states
provider_status_unreadable502the issuer did not answer the read
provider_cancel_failed502the issuer refused the withdrawal

charge_not_found is deliberately the SAME answer for an id that belongs to another project and for an id that exists nowhere. Telling those apart would be an oracle about another tenant over a string anyone can guess.

Path parameters

NameTypeRequiredDescription
chargeIdstringyes—

Responses

StatusBodyDescription
200objectOK
400objectThe call is malformed.
404objectNo charge under that id for this tenant.
409objectThe charge exists and this state does not accept a withdrawal. already_payment_notified is the D+1 guard and is never a retry case.
502objectThe issuer refused the withdrawal, or did not answer.

Response 200

FieldTypeRequiredDescription
amountnumberyesIn MAJOR units of currency: 12.5 is R$ 12,50. Always amount_minor / 100.
amount_minorintegeryesIn minor units (centavos): 1250 is R$ 12,50.
boleto_bank_linestring,nullyes—
boleto_bar_codestring,nullyes—
credit_correlation_armedbooleanyesWhether an inbound Pix on this charge's Pix leg can resolve a wallet on its own. False until registration hands us the leg's identifier.
currencystringyes—
due_datestring,nullyes—
idstring,nullyesThe issuer's charge id. Null while the issuance is unconfirmed.
issuance_unconfirmedbooleanyesTrue while the row is a reservation with no charge id: the issuer's answer to the create was lost, or a create is in flight elsewhere under the same key. Nothing is payable and nothing was duplicated.
local_statusstringyesWhat CodeSpar recorded, which is a different question from status.
methodstringyes—
payablebooleanyesTrue only when all four hold: the issuance is confirmed, our row is still open, the provider says PENDING, and at least one document exists. This is the field to branch on, not status.
payment_in_flightbooleanyesTrue when a payment was notified and the receivable is still open. Read this before you write anything off: the issuer can answer EXPIRED for a boleto it already told us was paid, because the baixa happened before the due date and the credit lands on the next business day.
pix_copy_pastestring,nullyesThe Pix leg of the same debt. One receivable, two payable legs.
settlement"confirmed" | "pending" | "unconfirmable"yesThe settlement tri-state (decision 10b of the canonical matrix). confirmed: the money is in the account. pending: the clearing house notified a payment and the credit has not landed, which is the D+1 window. unconfirmable: our terminal state disagrees with the issuer's, so settlement cannot be established. NULL MEANS NOT APPLICABLE, never pending. A charge nobody paid has no pending settlement: it has an open receivable. Reading null as pending would tell you money is on its way.
statusstringyesThe provider's state as far as we know it, normalized (PROCESSING, PENDING, CONFIRMED, CANCELLED, EXPIRED, ERROR). Forced to PROCESSING until the issuance is confirmed. ERROR is the issuer rejecting the charge: no boleto and no Pix were ever issued, so nobody can pay it. A charge settled by the sandbox payer answers CONFIRMED here, the same value a real payment leaves a live read answering — so a client that polls status sees the test payment exactly where it would see a live one. The issuer's homolog account never sees that money and keeps answering PENDING (EXPIRED after the due date); that answer stays in raw and is not reported as status or as a status_conflict, because for a test charge the fixture is the issuer's truth. settlement: "confirmed" and local_status: "settled" say the same thing from our side. Any other issuer answer (ERROR, CANCELLED, PROCESSING) is reported as the issuer said it, with status_conflict: true.
status_conflictbooleanyesTrue whenever the issuer's known state and ours disagree, instead of one of them silently winning. They agree only as follows: our pending beside the issuer's PENDING or PROCESSING; our settled beside CONFIRMED or PENDING (the issuer's read can lag a payment); our expired beside CANCELLED or PENDING (lag after a cancel). Every other pair is a conflict, including an issuer ERROR, an issuer terminal state on a charge we still hold open, and a status spelling we do not recognise. False when no issuer state is known at all. settlement: "unconfirmable" is the narrower case of both sides terminal and different.
Example request
curl -X POST https://api.codespar.dev/v1/charges/{chargeId}/cancel \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
POST /v1/charges/{chargeId}/cancel HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
import os
import requests

res = requests.post(
    "https://api.codespar.dev/v1/charges/{chargeId}/cancel",
    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/charges/{chargeId}/cancel", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
const result = await cs.api.post("/v1/charges/{chargeId}/cancel", {
  path: {
    chargeId: "charge_0000000000000000"
  }
});
Example response 200
application/json
{
  "id": "obj_0000000000000000",
  "status": "string",
  "local_status": "string",
  "status_conflict": true,
  "method": "string",
  "currency": "BRL",
  "amount": 1000,
  "amount_minor": 1000,
  "due_date": "string",
  "payable": true,
  "boleto_bar_code": "string",
  "boleto_bank_line": "string",
  "pix_copy_paste": "string",
  "credit_correlation_armed": true,
  "payment_in_flight": true,
  "settlement": "confirmed",
  "issuance_unconfirmed": true
}

POST /v1/charges/{chargeId}/sandbox/pay

POSThttps://api.codespar.dev/v1/charges/{chargeId}/sandbox/pay
Moves moneyDeprecated

ledger `fund()` with the `pending -> settled` claim

Pay a charge in the sandbox (alias path)

Alias of POST /v1/test/charges/{chargeId}/pay (ent#979 contract: one handler, two paths, one scope). Same body, same answers.

Plays the DEBTOR of a test charge: settles it through the SAME path a Celcoin charge-in webhook takes (translate, publish commerce.charge.paid, reconcile, ledger fund() with the pending -> settled claim). It is not a status write, so the event fans out to your triggers exactly as a real payment would, and the ledger and receipt side effects are the ones production produces.

Test-mode only, fail-closed. A live-environment key or project answers 403 sandbox_pay_not_permitted before the body is read or any query runs.

Nothing claims money moved. The event payload, the ledger entry and the charge row all carry simulated: true and settled_against: "sandbox_fixture"; a real charge-in never carries either.

Only a payable charge settles. The debtor can only pay a document the issuer handed over: the charge must be open, a boleto or Pix recorded, and the issuer's last known status PENDING (or CONFIRMED, the issuer already calling it paid) — the conditions of payable: true on the read. Anything else is refused with 409 charge_not_payable before anything is written, exactly as a live debtor would find nothing to pay.

Idempotent. The provider event id is the one the real delivery would carry, so a second pay dedupes at the event (no second trigger fire), the ledger dedupes on the charge, and a charge already settled answers its recorded state with idempotent_replay: true whatever amount the second call names.

chargeId is the id create returned, or your own idempotency key, resolved under the caller's org and project: another tenant's charge is charge_not_found, never forbidden.

Path parameters

NameTypeRequiredDescription
chargeIdstringyes—

Request body

FieldTypeRequiredDescription
amount_minorintegernoOverrides the quoted amount, in minor units, to simulate a partial or divergent payment. The funding bridge credits what arrived and closes the charge, exactly as it does for a real charge-in that disagrees with the quote. Omitted: the quoted amount is paid in full.

Responses

StatusBodyDescription
200objectOK
400objectThe body is outside the schema, or the credential names no project.
403objectNot a test-environment key/project. Terminal for that credential.
404objectNo charge under that id for this tenant.
409objectThe charge exists and cannot be paid, and nothing was settled, credited or published: issuance_unconfirmed is a reservation whose create never got an answer; charge_not_payable is a charge with no document a debtor could pay, and details.reason says which: issuer_rejected (the issuer left it in ERROR, no boleto or Pix ever existed — terminal, create a new charge), charge_closed (expired or cancelled, ours or the issuer's — terminal, create a new charge), instrument_not_registered (the issuer has not handed over a boleto or Pix yet — read the charge and pay once it answers payable: true); charge_reference_ambiguous is a reference that is one charge's id and another's idempotency key — pay by the charge id; collect_simulated_settle_refused is a Collect link's charge, issued on a real rail: in Test it is paid with POST /v1/collect/attempts/{attemptId}/test-pay (details.next_action), never by this simulation.
502objectThe payment event was published but the ledger credit did not land; the charge is still open. Retrying is safe — the event dedupes and the credit is keyed on the charge.

Response 200

FieldTypeRequiredDescription
charge_idstringyes—
currencystringyes—
eventobject,nullyesThe one commerce.charge.paid this charge has. A second pay never mints another.
idempotent_replaybooleanyesTrue when the charge was already settled and this call answered from the record.
ledger_entry_idstring,nullyesThe fund entry keyed bolepix:<charge_id>. Null only for a charge the other credit path settled.
local_statusstringyesWhat CodeSpar recorded: settled.
money_movedfalseyesNothing moved anywhere, including at the provider's sandbox.
paid_minorintegeryesWhat the ledger credited.
paid_viastringyesThe leg the simulated debtor used. Always Pix today.
payment"full" | "partial" | "over"yes—
quoted_minorintegeryes—
settled_against"sandbox_fixture"yes—
simulatedbooleanyesTrue when a sandbox fixture settled it. Read off the records, never off the request.
status"paid"yes—
wallet_idstringyes—
Example request
curl -X POST https://api.codespar.dev/v1/charges/{chargeId}/sandbox/pay \
  -H "Authorization: Bearer $CODESPAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "amount_minor": 1000
     }'
POST /v1/charges/{chargeId}/sandbox/pay HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json

{
  "amount_minor": 1000
}
import os
import requests

res = requests.post(
    "https://api.codespar.dev/v1/charges/{chargeId}/sandbox/pay",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
    json={
      "amount_minor": 1000
    },
)
res.raise_for_status()
data = res.json()
const res = await fetch("https://api.codespar.dev/v1/charges/{chargeId}/sandbox/pay", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "amount_minor": 1000
  }),
});

const data = await res.json();
const r = await cs.api.response("post", "/v1/charges/{chargeId}/sandbox/pay", {
  path: {
    chargeId: "charge_0000000000000000"
  },
  body: {
    amount_minor: 1000
  }
});
// r.status is one of the documented statuses (200, 403),
// 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
{
  "charge_id": "charge_0000000000000000",
  "status": "paid",
  "local_status": "string",
  "currency": "BRL",
  "quoted_minor": 1,
  "paid_minor": 1,
  "payment": "full",
  "paid_via": "string",
  "wallet_id": "wlt_0000000000000000",
  "ledger_entry_id": "ledgerentry_0000000000000000",
  "event": {
    "id": "obj_0000000000000000",
    "type": "commerce.charge.paid"
  },
  "simulated": true,
  "settled_against": "sandbox_fixture",
  "money_moved": false,
  "idempotent_replay": true
}
Charges | CodeSpar