Sandbox money
5 operations under /v1/test (POST): parameters, status codes, refusal bodies, and the same request in curl, HTTP, Python and TypeScript.
Base URL: https://api.codespar.dev
Every operation below requires a Bearer token. See Authentication.
POST /v1/test/charges/{chargeId}/pay
https://api.codespar.dev/v1/test/charges/{chargeId}/payledger `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
| Name | Type | Required | Description |
|---|---|---|---|
chargeId | string | yes | — |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
amount_minor | integer | no | Overrides 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
| Status | Body | Description |
|---|---|---|
200 | object | OK |
400 | object | The body is outside the schema, or the credential names no project. |
403 | object | Not a test-environment key/project. Terminal for that credential. |
404 | object | No charge under that id for this tenant. |
409 | object | The 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. |
502 | object | The 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
| Field | Type | Required | Description |
|---|---|---|---|
charge_id | string | yes | — |
currency | string | yes | — |
event | object,null | yes | The one commerce.charge.paid this charge has. A second pay never mints another. |
idempotent_replay | boolean | yes | True when the charge was already settled and this call answered from the record. |
ledger_entry_id | string,null | yes | The fund entry keyed bolepix:<charge_id>. Null only for a charge the other credit path settled. |
local_status | string | yes | What CodeSpar recorded: settled. |
money_moved | false | yes | Nothing moved anywhere, including at the provider's sandbox. |
paid_minor | integer | yes | What the ledger credited. |
paid_via | string | yes | The leg the simulated debtor used. Always Pix today. |
payment | "full" | "partial" | "over" | yes | — |
quoted_minor | integer | yes | — |
settled_against | "sandbox_fixture" | yes | — |
simulated | boolean | yes | True when a sandbox fixture settled it. Read off the records, never off the request. |
status | "paid" | yes | — |
wallet_id | string | yes | — |
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);
}{
"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
https://api.codespar.dev/v1/test/charges/{chargeId}/scenariosRun 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
| Name | Type | Required | Description |
|---|---|---|---|
chargeId | string | yes | — |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
scenario | "late_after_cancel" | "late_after_expiry" | yes | — |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
400 | object | The body is outside the schema, or the credential names no project. |
403 | object | Not a test-environment key/project. Terminal for that credential. |
404 | object | No charge under that id for this tenant. |
409 | object | Nothing 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
| Field | Type | Required | Description |
|---|---|---|---|
charge_id | string | yes | — |
event_id | string | yes | — |
local_status | string | yes | — |
money_moved | false | yes | — |
scenario | string | yes | — |
simulated | true | yes | — |
simulated_provider_event | object | yes | — |
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);
}{
"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
https://api.codespar.dev/v1/test/fundCredits 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
| Field | Type | Required | Description |
|---|---|---|---|
account | string | no | The 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_minor | integer | yes | BRL cents. |
consumer_id | string | no | Required on the /v1/test/* path, where the route has no consumer segment. Ignored on the /v1/consumers/{consumerId}/... alias, where the segment wins. |
idempotency_key | string | no | Retry 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
| Status | Body | Description |
|---|---|---|
200 | object | OK |
201 | object | OK |
400 | object | The 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). |
403 | object | sandbox_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. |
422 | object | The 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
| Field | Type | Required | Description |
|---|---|---|---|
account | string | yes | — |
amount_minor | integer | yes | — |
attempt_id | string | yes | — |
currency | "BRL" | yes | — |
deposit_id | string | yes | — |
idempotent_replay | true | no | Present on a rerun answered from the credit already made. |
money_credited | true | yes | — |
status | string | yes | — |
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);
}{
"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
https://api.codespar.dev/v1/test/pix-inMint 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
| Field | Type | Required | Description |
|---|---|---|---|
amount_minor | integer | yes | BRL cents. |
consumer_id | string | no | Required on the /v1/test/* path, where the route has no consumer segment. Ignored on the /v1/consumers/{consumerId}/... alias, where the segment wins. |
description | string | no | — |
Responses
| Status | Body | Description |
|---|---|---|
201 | object | OK |
400 | object | The 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). |
403 | object | A 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. |
422 | object | The sandbox provider refused the mint. |
429 | object | project_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. |
503 | object | project_access_calls_unavailable: the project's provider call budget could not be read (Test bound account only). Nothing was sent; retry. |
Response 201
| Field | Type | Required | Description |
|---|---|---|---|
amount_minor | integer | yes | — |
currency | "BRL" | yes | — |
pix_copia_e_cola | string | yes | The code that gets paid, the way a real Pix is paid. |
pix_key | string | yes | — |
rail | "pix-celcoin" | yes | — |
transaction_id | string | yes | The idempotency key of the settlement step. |
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);
}{
"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
https://api.codespar.dev/v1/test/settle-pix-inSettles 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
| Field | Type | Required | Description |
|---|---|---|---|
amount_minor | integer | yes | — |
consumer_id | string | no | Required on the /v1/test/* path, where the route has no consumer segment. Ignored on the /v1/consumers/{consumerId}/... alias, where the segment wins. |
transaction_id | string | yes | The 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
| Status | Body | Description |
|---|---|---|
200 | object | OK |
201 | object | OK |
400 | object | The 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). |
403 | object | A 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. |
404 | object | charge_not_found: no single charge minted for this consumer in this project carries this transaction_id (Test bound account only). Nothing was sent. |
409 | object | Test 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. |
422 | object | The 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). |
429 | object | project_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. |
500 | object | The sandbox account was credited and the mirror in the wallet ledger failed: split state, and details.deposit_id is where to reconcile from. |
503 | object | project_access_calls_unavailable: the project's provider call budget could not be read (Test bound account only). Nothing was sent; retry. |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
amount_minor | integer | yes | — |
currency | "BRL" | yes | — |
deposit_id | string,null | yes | The provider's credit. Null only on a replay of a charge its webhook or the reconciler settled. |
idempotent_replay | boolean | no | True when this answer is a replay: nothing was sent again. |
money_credited | true | yes | — |
settled | true | yes | — |
transaction_id | string | yes | — |
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);
}{
"settled": true,
"transaction_id": "transaction_0000000000000000",
"deposit_id": "deposit_0000000000000000",
"idempotent_replay": true,
"amount_minor": 1000,
"currency": "BRL",
"money_credited": true
}Meter Events
1 operation under /v1/meter-events (POST): parameters, status codes, refusal bodies, and the same request in curl, HTTP, Python and TypeScript.
Wallets
15 operations under /v1/wallets (GET POST DELETE): parameters, status codes, refusal bodies, and the same request in curl, HTTP, Python and TypeScript.