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. |
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 | 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. |
Response 201
| Field | Type | Required | Description |
|---|---|---|---|
account | string | yes | — |
amount_minor | integer | yes | — |
attempt_id | string | yes | — |
currency | "BRL" | yes | — |
deposit_id | string | yes | — |
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"
}'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"
}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"
},
)
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"
}),
});
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"
}
});
// 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
}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.
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. |
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.
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 |
|---|---|---|
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 settlement. |
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. |
Response 201
| Field | Type | Required | Description |
|---|---|---|---|
amount_minor | integer | yes | — |
currency | "BRL" | yes | — |
deposit_id | string | yes | — |
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",
"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.