Sandbox
Test data
The sandbox’s deterministic documents, amounts, ids and error cases, module by module: affiliates, credits and subsidies (1000000001–1000000004 and 9999999999, 50.000.000 threshold), monetary allowance, wallet, loan portfolio, health, administrative, PQRS and webhooks.
On this page
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.
| Document | Profile | Affiliate lookup¹ | On submit | On decision | On disbursement |
|---|---|---|---|---|---|
1000000001 | Eligible | Not simulated | 201 RECEIVED | APPROVED | 200 DISBURSED |
1000000002 | Ineligible | Not simulated | 201 REJECTED (NOT_ELIGIBLE) | REJECTED (NOT_ELIGIBLE) | 409 (not disbursable) |
1000000003 | Manual review | Not simulated | 201 RECEIVED | IN_REVIEW (MANUAL_REVIEW) | 409 |
1000000004 | With debts | Not simulated | 201 REJECTED (OUTSTANDING_DEBT) | REJECTED (OUTSTANDING_DEBT) | 409 |
9999999999 | Doesn’t exist | Not simulated | 404 | — | — |
| Any other | Eligible | — | Like 1000000001 | Like 1000000001 | Like 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 (Eeligible,Iineligible,Mmanual review,Dwith debts) plus anXif it exceeded the threshold (sbx-EX-…). - The same tenant and the same
external_referencealways produce the same id (withoutexternal_reference, the document is what counts). - Looking up an application (
GET) returns the decided status (APPROVED,REJECTED,IN_REVIEW), even if the submission returnedRECEIVED. - Disbursing an approved application returns a
disbursement_iddsb-…. - The credit and subsidy inboxes list 4 applications, one for each document from
1000000001to1000000004(the nonexistent one doesn’t appear), and apply thestatusandsearch(document or name) filters and pagination.
Subsidies follow exactly the same rules, for any subsidy type in the route.
One test per profile #
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")import { CompenseClient, NotFoundError } from "@compense/sdk";
const client = new CompenseClient({ apiKey: process.env.COMPENSE_API_KEY!, environment: "sandbox" });
const esperado: Record<string, string> = {
"1000000001": "RECEIVED", "1000000002": "REJECTED",
"1000000003": "RECEIVED", "1000000004": "REJECTED",
};
for (const [doc, estado] of Object.entries(esperado)) {
const tx = await client.credits.submitApplication({
affiliate: { document_type: "CC", document_number: doc },
productCode: "LIBRANZA_12M", amount: "3500000", termMonths: 12,
externalReference: `HORIZONTE-TEST-${doc}`,
});
if (tx.data.status !== estado) throw new Error(`${doc}: ${JSON.stringify(tx.data)}`);
}
try {
await client.affiliates.get("9999999999");
} catch (err) {
if (!(err instanceof NotFoundError)) throw err;
console.log("9999999999 doesn't exist, as expected");
}<?php
require __DIR__ . '/vendor/autoload.php';
use Compense\Sdk\CompenseClient;
use Compense\Sdk\Exception\NotFoundException;
use Compense\Sdk\Model\AffiliateRef;
$client = new CompenseClient(apiKey: getenv('COMPENSE_API_KEY'), environment: 'sandbox');
$esperado = ['1000000001' => 'RECEIVED', '1000000002' => 'REJECTED',
'1000000003' => 'RECEIVED', '1000000004' => 'REJECTED'];
foreach ($esperado as $doc => $estado) {
$tx = $client->credits->submitApplication(
affiliate: new AffiliateRef('CC', (string) $doc),
productCode: 'LIBRANZA_12M', amount: '3500000', termMonths: 12,
externalReference: "HORIZONTE-TEST-{$doc}",
);
assert($tx->data['status'] === $estado);
}
try {
$client->affiliates->get('9999999999');
} catch (NotFoundException) {
echo "9999999999 doesn't exist, as expected\n";
}using Compense.Sdk;
using Compense.Sdk.Models;
using var client = new CompenseClient(
Environment.GetEnvironmentVariable("COMPENSE_API_KEY")!, CompenseEnvironment.Sandbox);
var esperado = new Dictionary<string, string>
{
["1000000001"] = "RECEIVED", ["1000000002"] = "REJECTED",
["1000000003"] = "RECEIVED", ["1000000004"] = "REJECTED",
};
foreach (var (doc, estado) in esperado)
{
Transaction tx = await client.Credits.SubmitApplicationAsync(
affiliate: new AffiliateRef("CC", doc),
productCode: "LIBRANZA_12M", amount: "3500000", termMonths: 12,
externalReference: $"HORIZONTE-TEST-{doc}");
if (tx.Data["status"]?.GetValue<string>() != estado) throw new Exception($"{doc}: {tx.Data}");
}
try
{
await client.Affiliates.GetAsync("9999999999");
}
catch (NotFoundException)
{
Console.WriteLine("9999999999 doesn't exist, as expected");
}esperado := map[string]string{
"1000000001": "RECEIVED", "1000000002": "REJECTED",
"1000000003": "RECEIVED", "1000000004": "REJECTED",
}
for doc, estado := range esperado {
tx, err := client.Credits.SubmitApplication(ctx, &compense.CreditApplicationParams{
Affiliate: compense.AffiliateRef{DocumentType: "CC", DocumentNumber: doc},
ProductCode: "LIBRANZA_12M",
Amount: "3500000",
TermMonths: 12,
ExternalReference: "HORIZONTE-TEST-" + doc,
})
if err != nil {
log.Fatal(err)
}
if tx.Data["status"] != estado {
log.Fatalf("%s: %v", doc, tx.Data)
}
}
if _, err := client.Affiliates.Get(ctx, "9999999999"); compense.IsNotFound(err) {
fmt.Println("9999999999 doesn't exist, as expected")
}for DOC in 1000000001 1000000002 1000000003 1000000004; do
curl -s -X POST https://sandbox.dev.compense.co/gw/v1/gateway/credits/application/ \
-H "X-API-Key: $COMPENSE_API_KEY" \
-H "Idempotency-Key: horizonte-test-$DOC" \
-H "Content-Type: application/json" \
-d "{\"afiliado\": {\"document_type\": \"CC\", \"document_number\": \"$DOC\"},
\"product_code\": \"LIBRANZA_12M\", \"amount\": \"3500000\", \"term_months\": 12,
\"external_reference\": \"HORIZONTE-TEST-$DOC\"}" | jq -r --arg d "$DOC" '"\($d) \(.data.status)"'
doneMonetary allowance #
| Document | On generation | On paying the generated allowance |
|---|---|---|
1000000001 (or any other) | 201 GENERATED, eligible: true, amount 50,000 | 200 DISBURSED |
1000000003 | 201 GENERATED, eligible: true, amount 50,000 | 409 (the profile stays in review) |
1000000002 | 201 NOT_ELIGIBLE (NOT_ELIGIBLE), amount 0 | 409 |
1000000004 | 201 NOT_ELIGIBLE (OUTSTANDING_DEBT), amount 0 | 409 |
9999999999 | 404 | — |
- The allowance gets a
disbursement_idmad-<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
GENERATEDrow 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 document | Balance | Behavior |
|---|---|---|
1000000001 | 500,000 | Everything works. |
1000000003 | 500,000 | Everything works. |
1000000002 | 0 | 422 INSUFFICIENT_FUNDS on any send. |
1000000004 | 100 | 422 INSUFFICIENT_FUNDS on almost any send. |
9999999999 | — | 404 MEMBER_NOT_FOUND. |
| Any other | 500,000 | Like 1000000001. |
| Case | Test value | Result |
|---|---|---|
| Resolve a nonexistent P2P alias or Bre-B key | 9999999999 | 200 with {"found": false} |
| P2P to yourself | alias equal to the payer’s document | 422 SELF_TRANSFER |
| Send without resolving first | no resolution_token | 400 RESOLUTION_REQUIRED |
| Token from another flow | a Bre-B token in P2P or vice versa | 400 RESOLUTION_INVALID |
| Rejection by the destination bank (Bre-B) | amount 11111 | 422 TRANSFER_REJECTED (reason_code AC04) |
| Zero or negative amount | 0 | 422 INVALID_AMOUNT |
| Bill already paid | reference that ends in 0 | 409 BILL_ALREADY_PAID |
| Unknown biller | any other biller_code | 404 BILLER_NOT_FOUND |
| Paying an amount different from the one looked up | any other amount | 422 AMOUNT_MISMATCH |
| Invalid mobile number | not 10 digits or not starting with 3 | 400 INVALID_PHONE |
| Unknown operator | not on the list | 404 OPERATOR_NOT_FOUND |
| Top-up out of range | less than 1,000 or more than 200,000 | 400 INVALID_AMOUNT |
- Simulated billers:
EPM(electricity),ACUEDUCTO(water),VANTI(gas) andETB(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 period2026-10: look it up before paying it to get the exact amount. - Top-up operators:
CLARO,MOVISTAR,TIGOandWOM, 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
idlets you look up the transaction’s details. - Transaction ids are
wtx-…and repeat for the same request.
Pockets, cards and FX #
| Case | Result |
|---|---|
| List accounts | One main wallet (with the profile’s balance) and one pocket at 0. |
| Default currency | DOP if you don’t send currency. |
Issue a card to 1000000002 | 422 NOT_ELIGIBLE. |
| Change a card’s status | ACTIVE, FROZEN or BLOCKED; any other value → 422 INVALID_STATUS. |
| A card’s transactions | One purchase (PURCHASE) of 1,000, SETTLED. |
| Quote or convert FX | Fixed 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 balance | 422 INSUFFICIENT_FUNDS. |
Loan portfolio #
| Document | Simulated loan |
|---|---|
1000000001 | 1 ACTIVE loan, up to date (installments 1 to 3 paid). |
1000000003 | 1 IN_ARREARS loan: installment 4 is 12 days past due. |
1000000004 | 1 DEFAULTED loan: installments 3 to 5 past due (95 days). |
1000000002 | No loans (empty list). |
9999999999 | 404 (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:
channelinCASH,ACH_LIBRANZA,BANK_FILEorMANUAL(it also accepts aliases such asEFECTIVO,LIBRANZA,TRANSFERENCIAorCHEQUE; any other value →422 INVALID_CHANNEL), requiredreference(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’smodeinreduce_termorreduce_installment(422 INVALID_MODE). - The same
referencereturns the samereceipt_id, andreplayedis alwaysfalse. Recording a payment doesn’t change what reads return.
Health #
| Case | Test value | Result |
|---|---|---|
| Practitioners | D1 (general medicine, Sede Sandbox Centro) and D2 (pediatrics, Sede Sandbox Norte) | In the catalog and availability |
| Slots | slt-<doctor>-<YYYYMMDD>-<HHMM>, times 08:00, 08:20, 08:40, 10:00, 10:20 and 10:40 | Availability doesn’t list the ones at minute 40 |
| Slot already taken | minute 40 (e.g. slt-D1-20261103-0840) | 409 SLOT_NOT_AVAILABLE when booking or rescheduling |
| Availability on a past date | yesterday or earlier | 400 |
| Nonexistent patient | document 9999999999 | 404 HEALTH_PATIENT_NOT_FOUND; with new_patient it gets “registered” |
| Cancel without a reason | reason shorter than 5 characters | 422 HEALTH_REASON_REQUIRED |
| Attendance | completed / no_show | Future appointment → 422 HEALTH_STATUS_NOT_ALLOWED; past → 200 |
| Invoices | inv-R-120000-… (RADICADA, $120,000) and inv-C-53500-… (CUV_OK, $53,500) | The two test invoices |
| Payer payment | method TRANSFERENCIA, CONSIGNACION, GIRO_DIRECTO or RETENCION | Any other method → 422 HEALTH_PAYMENT_METHOD |
| Payment without a reference | empty reference | 422 HEALTH_PAYMENT_REFERENCE |
| Payment greater than the net amount | more than $120,000 or $53,500 | 422 PAYMENT_EXCEEDS_BALANCE |
Appointments have ids aps-<doctor>-<YYYYMMDD>-<HHMM>-<hash>; rescheduling returns a different id.
Administrative #
| Case | Test value | Result |
|---|---|---|
| Existing employees | CC 1000000001 (Laura Sandbox, Operaciones) and CC 1000000002 (Andrés Sandbox, Tesorería) | An upsert returns created: false |
| New employee | any other document | It gets “created” |
| Nonexistent employee | CC 9999999999 | 404 on reads |
| Bank details in the upsert | bank_name, bank_account_type, bank_account_number | 422 |
| Open period | per-O-… (October 2026) | Accepts payroll changes |
| Closed period | per-C-… (September 2026, approved) | 409 PERIOD_CLOSED when loading payroll changes |
| Items the engine calculates | SALARIO, SALUD, PENSION, PRIMA, CESANTIAS… | 422 |
| Journal entries | any | Always DRAFT; same external_id → same id; post: true → 422 |
| Existing third party | NIT 900000001 | created: false |
| Treasury | collections, disbursements, collection accounts | DRAFT; value <= 0 → 422; automatic matching returns matched: 0 |
PQRS #
| Case | Result |
|---|---|
Submit with a document from 1000000001 to 1000000004 | 201; the same body produces the same id and filing number. |
Submit with document 9999999999 | 422 AFFILIATE_NOT_FOUND. |
Answer PQRS PQRS-2026-999999 (closed) | 409. |
Change the status of PQRS-2026-999999 | 409, except to closed (200, no changes). |
An id that is neither pqr-… nor a filing number | 404 PQRS_NOT_FOUND. |
| List and statistics | A 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 #
| Case | Result |
|---|---|
| Create an endpoint | Id whe_sbx_<hash> (same URL → same id) and secret whsec_sbx_<hash>. |
| Rotate the secret | Another fixed whsec_sbx_…. |
Any whe_sbx_… id | Exists (details, edit, delete, rotate, test). |
| Any other endpoint id | 404 WEBHOOK_ENDPOINT_NOT_FOUND. |
http://, localhost or private IP URL | 422 WEBHOOK_INVALID_URL. |
Empty events or invalid format | 422 WEBHOOK_INVALID_EVENTS (only the resource.event format is validated). |
| Send a test | delivery_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_…). |
| Catalog | 9 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 #
| Tool | Values |
|---|---|
| Test clocks | Id tcl-…; advance from 0 to 63 days (5,443,200 s) per call; never backward. |
| Simulators | Amount 22.22 → FAILED; 33.33 → RETURNED; 11.11 or any other → SETTLED / DISBURSED. |
Sandbox id prefixes #
| Prefix | What 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 |