Everything in the sandbox is deterministic: the result is decided by the document you send, a few amounts and the ids the sandbox itself returns to you. This page gathers those values so you can write automated tests that cover the happy path and every documented error. Remember that the sandbox doesn’t store state: state travels in the ids, and a write doesn’t change what later reads return (Sandbox).

Affiliates, credits and subsidies #

Five documents (type CC) with a fixed profile. The profile decides the result of each step, regardless of what you send in the decision.

DocumentProfileAffiliate lookup¹On submitOn decisionOn disbursement
1000000001EligibleNot simulated201 RECEIVEDAPPROVED200 DISBURSED
1000000002IneligibleNot simulated201 REJECTED (NOT_ELIGIBLE)REJECTED (NOT_ELIGIBLE)409 (not disbursable)
1000000003Manual reviewNot simulated201 RECEIVEDIN_REVIEW (MANUAL_REVIEW)409
1000000004With debtsNot simulated201 REJECTED (OUTSTANDING_DEBT)REJECTED (OUTSTANDING_DEBT)409
9999999999Doesn’t existNot simulated404——
Any otherEligible—Like 1000000001Like 1000000001Like 1000000001

¹ Affiliate lookup, cross-check and upload (affiliates.get, cross, upload) aren’t simulated: with any key they read and write the tenant’s real affiliate registry. To test the lookup, first load your test affiliates with affiliates.cross; a document that isn’t in your registry returns 404.

Amount threshold: 50,000,000. An application from the eligible profile for more than 50,000,000 is REJECTED with AMOUNT_EXCEEDS_LIMIT from submission onward, and stays rejected (and not disbursable) in the decision, the lookup and the disbursement. Use it for your “amount too high” case.

How to read the ids:

  • The application gets an id sbx-<code>-<hash>. The code is the profile letter (E eligible, I ineligible, M manual review, D with debts) plus an X if it exceeded the threshold (sbx-EX-…).
  • The same tenant and the same external_reference always produce the same id (without external_reference, the document is what counts).
  • Looking up an application (GET) returns the decided status (APPROVED, REJECTED, IN_REVIEW), even if the submission returned RECEIVED.
  • Disbursing an approved application returns a disbursement_id dsb-….
  • The credit and subsidy inboxes list 4 applications, one for each document from 1000000001 to 1000000004 (the nonexistent one doesn’t appear), and apply the status and search (document or name) filters and pagination.

Subsidies follow exactly the same rules, for any subsidy type in the route.

One test per profile #

Submit with each test document
import os
from compense_gateway import CompenseClient, NotFoundError

client = CompenseClient(api_key=os.environ["COMPENSE_API_KEY"], environment="sandbox")

ESPERADO = {"1000000001": "RECEIVED", "1000000002": "REJECTED",
            "1000000003": "RECEIVED", "1000000004": "REJECTED"}

for doc, estado in ESPERADO.items():
    tx = client.credits.submit_application(
        affiliate={"document_type": "CC", "document_number": doc},
        product_code="LIBRANZA_12M", amount="3500000", term_months=12,
        external_reference=f"HORIZONTE-TEST-{doc}",
    )
    assert tx.data["status"] == estado, (doc, tx.data)

try:
    client.affiliates.get("9999999999")
except NotFoundError:
    print("9999999999 doesn't exist, as expected")

Monetary allowance #

DocumentOn generationOn paying the generated allowance
1000000001 (or any other)201 GENERATED, eligible: true, amount 50,000200 DISBURSED
1000000003201 GENERATED, eligible: true, amount 50,000409 (the profile stays in review)
1000000002201 NOT_ELIGIBLE (NOT_ELIGIBLE), amount 0409
1000000004201 NOT_ELIGIBLE (OUTSTANDING_DEBT), amount 0409
9999999999404—
  • The allowance gets a disbursement_id mad-<code>-<hash>; the same affiliate and period produce the same id.
  • Without period, the current month (YYYY-MM) is used.
  • The list returns a single GENERATED row of 50,000 for the period.

Wallet #

Balance, P2P, Bre-B, bills and top-ups #

Currency: COP. The available balance depends on the holder’s profile (the request’s document):

Holder’s documentBalanceBehavior
1000000001500,000Everything works.
1000000003500,000Everything works.
10000000020422 INSUFFICIENT_FUNDS on any send.
1000000004100422 INSUFFICIENT_FUNDS on almost any send.
9999999999—404 MEMBER_NOT_FOUND.
Any other500,000Like 1000000001.
CaseTest valueResult
Resolve a nonexistent P2P alias or Bre-B key9999999999200 with {"found": false}
P2P to yourselfalias equal to the payer’s document422 SELF_TRANSFER
Send without resolving firstno resolution_token400 RESOLUTION_REQUIRED
Token from another flowa Bre-B token in P2P or vice versa400 RESOLUTION_INVALID
Rejection by the destination bank (Bre-B)amount 11111422 TRANSFER_REJECTED (reason_code AC04)
Zero or negative amount0422 INVALID_AMOUNT
Bill already paidreference that ends in 0409 BILL_ALREADY_PAID
Unknown billerany other biller_code404 BILLER_NOT_FOUND
Paying an amount different from the one looked upany other amount422 AMOUNT_MISMATCH
Invalid mobile numbernot 10 digits or not starting with 3400 INVALID_PHONE
Unknown operatornot on the list404 OPERATOR_NOT_FOUND
Top-up out of rangeless than 1,000 or more than 200,000400 INVALID_AMOUNT
  • Simulated billers: EPM (electricity), ACUEDUCTO (water), VANTI (gas) and ETB (internet). Each bill’s amount is between 50,000 and 95,000 depending on the reference, it’s due on 2026-11-15 and belongs to period 2026-10: look it up before paying it to get the exact amount.
  • Top-up operators: CLARO, MOVISTAR, TIGO and WOM, from 1,000 to 200,000.
  • Transactions: 3 fixed ones per holder (October 1, 5 and 10, 2026: a subsidy credit of 150,000, a transfer of 20,000 and a top-up of 10,000), pageable and filterable by date; their id lets you look up the transaction’s details.
  • Transaction ids are wtx-… and repeat for the same request.

Pockets, cards and FX #

CaseResult
List accountsOne main wallet (with the profile’s balance) and one pocket at 0.
Default currencyDOP if you don’t send currency.
Issue a card to 1000000002422 NOT_ELIGIBLE.
Change a card’s statusACTIVE, FROZEN or BLOCKED; any other value → 422 INVALID_STATUS.
A card’s transactionsOne purchase (PURCHASE) of 1,000, SETTLED.
Quote or convert FXFixed rates: DOP→USD 0,017 · USD→DOP 58.5 · DOP→COP 68.0 · COP→DOP 0,0147. Any other pair → 422 UNSUPPORTED_CURRENCY_PAIR.
Convert more than the balance422 INSUFFICIENT_FUNDS.

Loan portfolio #

DocumentSimulated loan
10000000011 ACTIVE loan, up to date (installments 1 to 3 paid).
10000000031 IN_ARREARS loan: installment 4 is 12 days past due.
10000000041 DEFAULTED loan: installments 3 to 5 past due (95 days).
1000000002No loans (empty list).
9999999999404 (the affiliate doesn’t exist).
  • Every test loan is for 5,000,000 over 12 installments, at a rate of 1.8 % per month, with the first installment on July 15, 2026 (fixed dates).
  • The loan_id (lna-…) comes from listing the document’s loans.
  • Collections are validated like the engine does: channel in CASH, ACH_LIBRANZA, BANK_FILE or MANUAL (it also accepts aliases such as EFECTIVO, LIBRANZA, TRANSFERENCIA or CHEQUE; any other value → 422 INVALID_CHANNEL), required reference (422 REFERENCE_REQUIRED), positive amount (422 INVALID_AMOUNT) not greater than the debt (422 AMOUNT_EXCEEDS_DEBT), payoff amount equal to the quote (422 PAYOFF_AMOUNT_MISMATCH) and the partial prepayment’s mode in reduce_term or reduce_installment (422 INVALID_MODE).
  • The same reference returns the same receipt_id, and replayed is always false. Recording a payment doesn’t change what reads return.

Health #

CaseTest valueResult
PractitionersD1 (general medicine, Sede Sandbox Centro) and D2 (pediatrics, Sede Sandbox Norte)In the catalog and availability
Slotsslt-<doctor>-<YYYYMMDD>-<HHMM>, times 08:00, 08:20, 08:40, 10:00, 10:20 and 10:40Availability doesn’t list the ones at minute 40
Slot already takenminute 40 (e.g. slt-D1-20261103-0840)409 SLOT_NOT_AVAILABLE when booking or rescheduling
Availability on a past dateyesterday or earlier400
Nonexistent patientdocument 9999999999404 HEALTH_PATIENT_NOT_FOUND; with new_patient it gets “registered”
Cancel without a reasonreason shorter than 5 characters422 HEALTH_REASON_REQUIRED
Attendancecompleted / no_showFuture appointment → 422 HEALTH_STATUS_NOT_ALLOWED; past → 200
Invoicesinv-R-120000-… (RADICADA, $120,000) and inv-C-53500-… (CUV_OK, $53,500)The two test invoices
Payer paymentmethod TRANSFERENCIA, CONSIGNACION, GIRO_DIRECTO or RETENCIONAny other method → 422 HEALTH_PAYMENT_METHOD
Payment without a referenceempty reference422 HEALTH_PAYMENT_REFERENCE
Payment greater than the net amountmore than $120,000 or $53,500422 PAYMENT_EXCEEDS_BALANCE

Appointments have ids aps-<doctor>-<YYYYMMDD>-<HHMM>-<hash>; rescheduling returns a different id.

Administrative #

CaseTest valueResult
Existing employeesCC 1000000001 (Laura Sandbox, Operaciones) and CC 1000000002 (Andrés Sandbox, Tesorería)An upsert returns created: false
New employeeany other documentIt gets “created”
Nonexistent employeeCC 9999999999404 on reads
Bank details in the upsertbank_name, bank_account_type, bank_account_number422
Open periodper-O-… (October 2026)Accepts payroll changes
Closed periodper-C-… (September 2026, approved)409 PERIOD_CLOSED when loading payroll changes
Items the engine calculatesSALARIO, SALUD, PENSION, PRIMA, CESANTIAS…422
Journal entriesanyAlways DRAFT; same external_id → same id; post: true → 422
Existing third partyNIT 900000001created: false
Treasurycollections, disbursements, collection accountsDRAFT; value <= 0 → 422; automatic matching returns matched: 0

PQRS #

CaseResult
Submit with a document from 1000000001 to 1000000004201; the same body produces the same id and filing number.
Submit with document 9999999999422 AFFILIATE_NOT_FOUND.
Answer PQRS PQRS-2026-999999 (closed)409.
Change the status of PQRS-2026-999999409, except to closed (200, no changes).
An id that is neither pqr-… nor a filing number404 PQRS_NOT_FOUND.
List and statisticsA fixed inbox of 5 PQRS from the test documents (applies filters and pagination).

The allowed transitions are the same as the engine’s (opened → in_progress, escalated, resolved or closed; resolved only → closed). A write returns the new status, but a later GET goes back to opened.

Webhooks #

CaseResult
Create an endpointId whe_sbx_<hash> (same URL → same id) and secret whsec_sbx_<hash>.
Rotate the secretAnother fixed whsec_sbx_….
Any whe_sbx_… idExists (details, edit, delete, rotate, test).
Any other endpoint id404 WEBHOOK_ENDPOINT_NOT_FOUND.
http://, localhost or private IP URL422 WEBHOOK_INVALID_URL.
Empty events or invalid format422 WEBHOOK_INVALID_EVENTS (only the resource.event format is validated).
Send a testdelivery_id whd_sbx_test_…, event_id test-sbx-…, "simulated": true.
Delivery whd_sbx_ok_…success, credits.decision, 2026-10-01; replay → 409.
Delivery whd_sbx_fail_…failed (HTTP 500), wallets.cashin.confirmed, 2026-10-02; replay → 201 (whd_sbx_retry_…).
Delivery whd_sbx_aban_…abandoned (HTTP 503), credits.disbursed, 2026-10-03; replay → 201 (whd_sbx_retry_…).
Catalog9 sample events with "sample": true.

There’s no real HTTP sending: to receive signed deliveries, use a production key in dev (Testing your webhooks).

Clocks and simulators #

ToolValues
Test clocksId tcl-…; advance from 0 to 63 days (5,443,200 s) per call; never backward.
SimulatorsAmount 22.22 → FAILED; 33.33 → RETURNED; 11.11 or any other → SETTLED / DISBURSED.

Sandbox id prefixes #

PrefixWhat it is
sbx-Credit or subsidy application
dsb-Credit disbursement
mad-Monetary allowance
wtx-Wallet transaction (P2P, Bre-B, top-up, account movement)
crd-Card
lna-Loan portfolio loan
slt- / aps- / inv-Health slot, appointment and invoice
per-O- / per-C-Open / closed payroll period
pqr-PQRS
whe_sbx_ / whd_sbx_Webhook endpoint and delivery
tcl-Test clock