Skip to main content

Sandbox money

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

12 min read
View MarkdownEdit on GitHub

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

Every operation below requires a Bearer token. See Authentication.

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

POSThttps://api.codespar.dev/v1/test/charges/{chargeId}/pay
Moves money

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

Pay a charge in the sandbox

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/test/charges/{chargeId}/pay \
  -H "Authorization: Bearer $CODESPAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "amount_minor": 1000
     }'
POST /v1/test/charges/{chargeId}/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/test/charges/{chargeId}/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/test/charges/{chargeId}/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/test/charges/{chargeId}/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
}

POST /v1/test/charges/{chargeId}/scenarios

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

Run a controlled provider-event scenario on a test charge

Test projects only. Composes ONE issuer delivery the debtor-side sandbox payer cannot play, and runs it through the same settlement path the Celcoin webhook takes (translate, publish, reconcile, fund). late_after_cancel / late_after_expiry: a charge-in for a charge that is already cancelled / expired, which Collect classifies as a late payment with its refund obligation. The sandbox payer keeps refusing a closed charge (charge_closed); this is a separate surface. Every record carries simulated: true, settled_against: "sandbox_fixture" and simulated_provider_event: { scenario, requested_by }, and the audit chain gets test_scenario_injected. No money moves.

Path parameters

NameTypeRequiredDescription
chargeIdstringyes—

Request body

FieldTypeRequiredDescription
scenario"late_after_cancel" | "late_after_expiry"yes—

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.
409objectNothing was delivered: scenario_precondition_failed is a charge not in the state the scenario starts from (details.expected against details.charge_state: cancelled and expired are both local_status: "expired" on the row); charge_reference_ambiguous is a reference that is one charge's id and another's idempotency key; collect_simulated_settle_refused is a Collect link's charge, issued on a real rail and never settled by a simulation.

Response 200

FieldTypeRequiredDescription
charge_idstringyes—
event_idstringyes—
local_statusstringyes—
money_movedfalseyes—
scenariostringyes—
simulatedtrueyes—
simulated_provider_eventobjectyes—
Example request
curl -X POST https://api.codespar.dev/v1/test/charges/{chargeId}/scenarios \
  -H "Authorization: Bearer $CODESPAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "scenario": "late_after_cancel"
     }'
POST /v1/test/charges/{chargeId}/scenarios HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json

{
  "scenario": "late_after_cancel"
}
import os
import requests

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

const data = await res.json();
const r = await cs.api.response("post", "/v1/test/charges/{chargeId}/scenarios", {
  path: {
    chargeId: "charge_0000000000000000"
  },
  body: {
    scenario: "late_after_cancel"
  }
});
// 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
{
  "scenario": "string",
  "charge_id": "charge_0000000000000000",
  "event_id": "event_0000000000000000",
  "local_status": "string",
  "simulated": true,
  "simulated_provider_event": {
    "scenario": "string",
    "requested_by": "string"
  },
  "money_moved": false
}

POST /v1/test/fund

POSThttps://api.codespar.dev/v1/test/fund
Moves money

Credits the consumer's sandbox account directly, without going through a Pix.

Credit a consumer in the sandbox

Credits the consumer's sandbox account directly, without going through a Pix. It is the shortcut for putting a balance on a test wallet before exercising a spend flow.

Request body

FieldTypeRequiredDescription
accountstringnoThe consumer's own sandbox account. Accepted only when it is the consumer's active pix-celcoin account in this project (otherwise 403 funding_account_not_owned); the default is that same account.
amount_minorintegeryesBRL cents.
consumer_idstringnoRequired on the /v1/test/* path, where the route has no consumer segment. Ignored on the /v1/consumers/{consumerId}/... alias, where the segment wins.
idempotency_keystringnoRetry key. In a project of the controlled or the restricted integrated proof, the same key answers the credit it already made (200, idempotent_replay: true) instead of crediting again.

Responses

StatusBodyDescription
200objectOK
201objectOK
400objectThe body is outside the schema, or consumer_id is missing on the /v1/test/* path (where it is required, because the route has no consumer segment).
403objectsandbox_funding_not_permitted: a LIVE environment key (these routes exist only in a test environment; details.environment carries what was read). funding_account_not_owned: the body names an account that is not the consumer's active Celcoin account in this project; only the consumer's own account is credited, and nothing was sent.
422objectThe consumer has no Celcoin account to credit, or the sandbox provider refused. sandbox_funding_refused: a bound account's credit was refused by a platform guard before it was sent (details.reason); nothing was sent.

Response 200

FieldTypeRequiredDescription
accountstringyes—
amount_minorintegeryes—
attempt_idstringyes—
currency"BRL"yes—
deposit_idstringyes—
idempotent_replaytruenoPresent on a rerun answered from the credit already made.
money_creditedtrueyes—
statusstringyes—
Example request
curl -X POST https://api.codespar.dev/v1/test/fund \
  -H "Authorization: Bearer $CODESPAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "consumer_id": "csm_0000000000000000",
       "amount_minor": 1000,
       "account": "string",
       "idempotency_key": "string"
     }'
POST /v1/test/fund HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json

{
  "consumer_id": "csm_0000000000000000",
  "amount_minor": 1000,
  "account": "string",
  "idempotency_key": "string"
}
import os
import requests

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

const data = await res.json();
const r = await cs.api.response("post", "/v1/test/fund", {
  body: {
    consumer_id: "csm_0000000000000000",
    amount_minor: 1000,
    account: "string",
    idempotency_key: "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);
}
Example response 200
application/json
{
  "attempt_id": "attempt_0000000000000000",
  "deposit_id": "deposit_0000000000000000",
  "status": "string",
  "account": "string",
  "amount_minor": 1000,
  "currency": "BRL",
  "money_credited": true,
  "idempotent_replay": true
}

POST /v1/test/pix-in

POSThttps://api.codespar.dev/v1/test/pix-in

Mint a sandbox Pix charge to fund a consumer

Mints a sandbox Pix charge and returns the copia-e-cola string. Nothing is credited here: this step produces the code, and settlement is the next step.

Keep the transaction_id: it is what settlement uses as the idempotency key.

A consumer whose account is bound to this Test project (the project's open account binding, its collect capability active) gets the charge on that account's OWN Pix key, through the default provider; the call is counted in the project's provider call budget first. The charge is recorded like any charge of the consumer's, so its settlement, its webhook and the reconciler credit it once.

Request body

FieldTypeRequiredDescription
amount_minorintegeryesBRL cents.
consumer_idstringnoRequired on the /v1/test/* path, where the route has no consumer segment. Ignored on the /v1/consumers/{consumerId}/... alias, where the segment wins.
descriptionstringno—

Responses

StatusBodyDescription
201objectOK
400objectThe body is outside the schema, or consumer_id is missing on the /v1/test/* path (where it is required, because the route has no consumer segment).
403objectA LIVE environment key. These routes credit fake money and exist only in a test environment: use a csk_test_* key on a test project. The environment that was read comes in details.environment.
422objectThe sandbox provider refused the mint.
429objectproject_access_call_cap: the project's provider call budget for the window is used up (Test bound account only). details names the window, the scope and the cap. Nothing was sent.
503objectproject_access_calls_unavailable: the project's provider call budget could not be read (Test bound account only). Nothing was sent; retry.

Response 201

FieldTypeRequiredDescription
amount_minorintegeryes—
currency"BRL"yes—
pix_copia_e_colastringyesThe code that gets paid, the way a real Pix is paid.
pix_keystringyes—
rail"pix-celcoin"yes—
transaction_idstringyesThe idempotency key of the settlement step.
Example request
curl -X POST https://api.codespar.dev/v1/test/pix-in \
  -H "Authorization: Bearer $CODESPAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "consumer_id": "csm_0000000000000000",
       "amount_minor": 1000,
       "description": "string"
     }'
POST /v1/test/pix-in HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json

{
  "consumer_id": "csm_0000000000000000",
  "amount_minor": 1000,
  "description": "string"
}
import os
import requests

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

const data = await res.json();
const r = await cs.api.response("post", "/v1/test/pix-in", {
  body: {
    consumer_id: "csm_0000000000000000",
    amount_minor: 1000,
    description: "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);
}
Example response 201
application/json
{
  "pix_copia_e_cola": "string",
  "transaction_id": "transaction_0000000000000000",
  "pix_key": "string",
  "amount_minor": 1000,
  "currency": "BRL",
  "rail": "pix-celcoin"
}

POST /v1/test/settle-pix-in

POSThttps://api.codespar.dev/v1/test/settle-pix-in
Moves money

Settles the charge the mint produced and credits the consumer.

Settle a sandbox Pix charge and credit the consumer

Settles the charge the mint produced and credits the consumer.

The transaction_id is the idempotency key: re-sending the same settlement does not credit twice.

For a consumer whose account is bound to this Test project, the settlement credits THAT account, for a charge the mint recorded for this consumer: transaction_id is required and must be the one the mint returned, and amount_minor must be the charge's amount. The credit is claimed before it is sent, so a retry answers from the claim (200, idempotent_replay), a charge already settled by its webhook or the reconciler answers the same way, and a credit whose outcome is unknown stays open (409 sandbox_funding_open_claim) and is never resent. It needs the project's payments capability, and it is counted in the provider call budget.

Request body

FieldTypeRequiredDescription
amount_minorintegeryes—
consumer_idstringnoRequired on the /v1/test/* path, where the route has no consumer segment. Ignored on the /v1/consumers/{consumerId}/... alias, where the segment wins.
transaction_idstringyesThe transaction_id the mint returned. It is the IDEMPOTENCY KEY of the credit and of the wallet mirror: re-sending the same settlement does not credit twice.

Responses

StatusBodyDescription
200objectOK
201objectOK
400objectThe body is outside the schema, or consumer_id is missing on the /v1/test/* path (where it is required, because the route has no consumer segment).
403objectA LIVE environment key. These routes credit fake money and exist only in a test environment: use a csk_test_* key on a test project. The environment that was read comes in details.environment.
404objectcharge_not_found: no single charge minted for this consumer in this project carries this transaction_id (Test bound account only). Nothing was sent.
409objectTest bound account only. charge_not_pending: the charge is no longer open. capability_inactive: the project's payments capability is not active. sandbox_funding_open_claim: an earlier credit of the project is still open (unanswered, or credited without its ledger copy); it is never resent. Nothing was sent.
422objectThe sandbox provider refused the settlement (sandbox_funding_failed), or, for a Test bound account, amount_minor is not the charge's amount (settle_amount_mismatch).
429objectproject_access_call_cap: the project's provider call budget for the window is used up (Test bound account only). details names the window, the scope and the cap. Nothing was sent.
500objectThe sandbox account was credited and the mirror in the wallet ledger failed: split state, and details.deposit_id is where to reconcile from.
503objectproject_access_calls_unavailable: the project's provider call budget could not be read (Test bound account only). Nothing was sent; retry.

Response 200

FieldTypeRequiredDescription
amount_minorintegeryes—
currency"BRL"yes—
deposit_idstring,nullyesThe provider's credit. Null only on a replay of a charge its webhook or the reconciler settled.
idempotent_replaybooleannoTrue when this answer is a replay: nothing was sent again.
money_creditedtrueyes—
settledtrueyes—
transaction_idstringyes—
Example request
curl -X POST https://api.codespar.dev/v1/test/settle-pix-in \
  -H "Authorization: Bearer $CODESPAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "consumer_id": "csm_0000000000000000",
       "amount_minor": 1000,
       "transaction_id": "transaction_0000000000000000"
     }'
POST /v1/test/settle-pix-in HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json

{
  "consumer_id": "csm_0000000000000000",
  "amount_minor": 1000,
  "transaction_id": "transaction_0000000000000000"
}
import os
import requests

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

const data = await res.json();
const r = await cs.api.response("post", "/v1/test/settle-pix-in", {
  body: {
    consumer_id: "csm_0000000000000000",
    amount_minor: 1000,
    transaction_id: "transaction_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
{
  "settled": true,
  "transaction_id": "transaction_0000000000000000",
  "deposit_id": "deposit_0000000000000000",
  "idempotent_replay": true,
  "amount_minor": 1000,
  "currency": "BRL",
  "money_credited": true
}
Sandbox money | CodeSpar