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.

10 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.

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).
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.

Response 201

FieldTypeRequiredDescription
accountstringyes—
amount_minorintegeryes—
attempt_idstringyes—
currency"BRL"yes—
deposit_idstringyes—
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"
     }'
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);
}
Example response 201
application/json
{
  "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

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.

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.

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.

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
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 settlement.
500objectThe 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

FieldTypeRequiredDescription
amount_minorintegeryes—
currency"BRL"yes—
deposit_idstringyes—
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 201
application/json
{
  "settled": true,
  "transaction_id": "transaction_0000000000000000",
  "deposit_id": "deposit_0000000000000000",
  "amount_minor": 1000,
  "currency": "BRL",
  "money_credited": true
}
Sandbox money | CodeSpar