This guide is for the technical team at a fintech (such as Nova) or a family compensation fund (caja de compensación, such as Caja Horizonte) that operates its affiliates’ wallet through the API, with the same rules as the app: balance and transactions, P2P transfers to another affiliate, Bre-B to other banks, utility bill payments, mobile top-ups, sub-accounts (pockets, savings, goals), cards and currency exchange. Everything lives under the contract’s wallet tag and, in the SDKs, under client.wallet.

Before you continue, have your sandbox key at hand and review Idempotency and Amounts: almost everything here moves money.

Before you start #

The holder is identified by document #

There is no affiliate identifier in the paths: the key already identifies your entity and the holder is always identified by document (their ID document number). In GET requests it goes in the query string (?document=1000000001) and in POST requests inside the JSON body. A document that doesn’t exist in your entity returns 404.

Scopes #

ScopeWhat it allows
wallet:readBalance, transactions and their detail, Bre-B limits, billers, payment history, top-up operators, accounts, cards and their transactions
wallet:transferP2P (resolve and send) and Bre-B (resolve and send)
wallet:paymentsLook up and pay utility bills
wallet:topupTop up a mobile phone
wallet:accountsCreate pockets/savings/goals and transfer between the holder’s own accounts
wallet:cardsIssue cards and change their status (freeze, unfreeze, block)
wallet:fxQuote and execute currency exchange

Request only the ones your integration needs. wallet:transfer, wallet:payments and wallet:topup move money and are on the list of scopes that will require an HMAC request signature once that protection is turned on for your key (inactive today; you’ll be notified in advance). If your entity hasn’t contracted the module, every path returns 403 with «El módulo de billetera no está contratado para esta entidad.» (“The wallet module is not contracted for this entity.”).

Amounts and idempotency #

  • Amounts travel as decimal strings ("30000", "65000.00"), in Colombian pesos. The SDKs accept numbers or strings and convert them; see Amounts.
  • P2P, Bre-B, bill payment and top-up require Idempotency-Key: without it the gateway returns 400. The SDKs always send one (they generate a UUID if you don’t pass your own), but if your system may resend the operation, pass a key that is stable per business operation (for example nova-p2p-2026-11-02-000123). Repeating the same key returns the original result with idempotent_replay: true and never moves money twice.
  • Reads done via POST (p2p.resolve, breb.resolve, bills.inquire, fx.quote) carry the personal data in the body so it doesn’t end up in URLs or logs; they don’t move money, don’t take a key, and the SDKs never retry them.

client.wallet.accounts is client.accounts

client.wallet.accounts, client.wallet.cards and client.wallet.fx are the same objects as client.accounts, client.cards and client.fx (in .NET client.Wallet.Accounts and client.Accounts; in Go client.Wallet.Accounts and client.Accounts). Use whichever path you prefer: they call exactly the same operations.

Operations map #

OperationPathScopeIdempotency-Key
BalanceGET/v1/gateway/wallet/balance/wallet:read—
TransactionsGET/v1/gateway/wallet/transactions/wallet:read—
Transaction detailGET/v1/gateway/wallet/transactions/{tx_id}/wallet:read—
Resolve P2P recipientPOST/v1/gateway/wallet/p2p/resolve/wallet:transfer—
Send P2PPOST/v1/gateway/wallet/p2p/transfers/wallet:transferrequired
Resolve Bre-B keyPOST/v1/gateway/wallet/breb/resolve/wallet:transfer—
Send Bre-BPOST/v1/gateway/wallet/breb/transfers/wallet:transferrequired
Bre-B limitsGET/v1/gateway/wallet/breb/limits/wallet:read—
BillersGET/v1/gateway/wallet/billers/wallet:read—
Look up billPOST/v1/gateway/wallet/bills/inquiry/wallet:payments—
Pay billPOST/v1/gateway/wallet/bills/pay/wallet:paymentsrequired
Payment historyGET/v1/gateway/wallet/bills/payments/wallet:read—
Payment detailGET/v1/gateway/wallet/bills/payments/{payment_id}/wallet:read—
Top-up operatorsGET/v1/gateway/wallet/topups/operators/wallet:read—
Top upPOST/v1/gateway/wallet/topups/wallet:topuprequired
AccountsGET/v1/gateway/wallet/accounts/wallet:read—
Create pocketPOST/v1/gateway/wallet/accounts/wallet:accountsrecommended
Transfer between accountsPOST/v1/gateway/wallet/accounts/transfer/wallet:accountsrecommended
CardsGET/v1/gateway/wallet/cards/wallet:read—
Issue cardPOST/v1/gateway/wallet/cards/wallet:cardsrecommended
Change card statusPOST/v1/gateway/wallet/cards/{card_id}/status/wallet:cardsrecommended
Card transactionsGET/v1/gateway/wallet/cards/{card_id}/transactions/wallet:read—
Quote currency exchangePOST/v1/gateway/wallet/fx/quote/wallet:fx—
Convert currencyPOST/v1/gateway/wallet/fx/convert/wallet:fxrecommended

Balance #

wallet.balance returns a typed Balance. available is what the holder can spend (the engine’s available_cash, net of holds); total includes held and pending funds.

import os
from compense_gateway import CompenseClient

client = CompenseClient(api_key=os.environ["COMPENSE_API_KEY"], environment="sandbox")
DOC = "1000000001"   # test holder: eligible profile, balance 500,000

saldo = client.wallet.balance(DOC)
print(saldo.available.amount, saldo.total.amount, saldo.currency)
if saldo.pending is not None:
    print("In transit:", saldo.pending.amount)
JSON
{"total": 500000.0, "available_cash": 500000.0, "restricted_amount": 0.0,
 "pending_amount": 0.0, "currency": "COP", "environment": "sandbox"}
Contract fieldIn the SDKWhat it is
totaltotalThe holder’s total balance
available_cashavailableAvailable to spend (net of holds)
restricted_amountrestrictedHeld (optional; null if the engine doesn’t send it)
pending_amountpendingIn transit (optional)
currencycurrencyThe entity’s currency (COP in Colombia)

The rest of the examples on this page reuse client and the document 1000000001; in Go they assume ctx, client and doc from the previous block.

Transactions #

GET/v1/gateway/wallet/transactions/ paginates with limit/offset and returns an OffsetPage: results, limit, offset, has_more and next_offset (details in Pagination). Filters: start_date and end_date (YYYY-MM-DD or ISO 8601; a bare date in end_date includes that whole day), category (TRANSFER, PAYMENT…), type (the engine’s type: TRANSFER_OUT, TOPUP…) and limit from 1 to 100 (50 by default). Transactions come from newest to oldest.

pagina = client.wallet.transactions.list(DOC, start_date="2026-10-01",
                                         end_date="2026-10-31", limit=20)
while True:
    for mov in pagina:
        print(mov["date"], mov["transaction_type"], mov["amount"], mov["status"])
    if not pagina.has_more:
        break
    pagina = client.wallet.transactions.list(DOC, start_date="2026-10-01",
                                             end_date="2026-10-31", limit=20,
                                             offset=pagina.next_offset)

detalle = client.wallet.transactions.get(mov["id"], document=DOC)
JSON
{"results": [{"id": "wtx-E-…", "type": "DEBIT", "category": "PAYMENT", "amount": 10000.0,
              "currency": "COP", "date": "2026-10-10T09:15:00+00:00",
              "description": "Recarga CLARO - ******4567", "status": "COMPLETED",
              "transaction_type": "TOPUP"}],
 "limit": 20, "offset": 0, "has_more": false, "next_offset": null}

The detail only shows transactions of the given holder: the id of another affiliate’s transaction returns 404, even if it exists.

P2P transfer to another affiliate #

A P2P transfer moves money between two affiliates of your own entity. It takes two steps, just like in the app: resolve (find out who you’re about to pay) and send.

The entity never sends a bare recipient id. resolve looks up the alias inside the engine and returns the masked name and a signed, short-lived (300 s) resolution_token, bound to your entity, to the paying wallet and to the receiving wallet. The send only accepts that token: you can’t confirm one person and pay another, or reuse the token with a different holder.

sequenceDiagram
    autonumber
    participant U as User (Nova app)
    participant N as Nova backend
    participant G as Compense Gateway
    U->>N: Send $30.000 to 300 123 4567
    N->>G: POST wallet/p2p/resolve/ {document, alias}
    G-->>N: found, display_name "Sa*** Re***", resolution_token (300 s)
    N->>U: Confirm sending to Sa*** Re***?
    U->>N: Yes
    N->>G: POST wallet/p2p/transfers/ + Idempotency-Key
    G-->>N: 201 {transaction_id, data.status: COMPLETED}
    G-)N: webhook wallets.transfer.created
r = client.wallet.p2p.resolve(document=DOC, alias="3001234567", key_type="PHONE")
if not r.found:
    raise SystemExit("No one in the entity has that alias")
if r.is_local is False:
    raise SystemExit("The key belongs to another entity: use Bre-B")

# Show r.display_name to the user and ask for confirmation within r.expires_in seconds
tx = client.wallet.p2p.transfer(
    document=DOC,
    resolution_token=r.resolution_token,
    amount="30000",
    description="Rent",
    idempotency_key="nova-p2p-2026-11-02-000123",
)
print(tx.transaction_id, tx.data["transaction_id"], tx.data["status"], tx.idempotent_replay)

Response from resolve and from transfer:

JSON
{"found": true, "is_local": true, "display_name": "Sa*** Re***",
 "resolution_token": "p2p-E-…", "expires_in": 300}
JSON
{"status": "success", "transaction_id": "gw-…", "module": "wallet",
 "data": {"transaction_id": "wtx-E-…", "status": "COMPLETED", "amount": 30000.0,
          "currency": "COP", "display_name": "Sa*** Re***"}}
  • alias accepts a mobile number, email, document or @alias; key_type (PHONE, EMAIL, DOCUMENT, ALIAS) is optional and disambiguates a string of digits.
  • found: false: no one has that alias. is_local: false: the key belongs to another entity, use Bre-B.
  • tx.transaction_id is the gateway id (for Transactions and support); tx.data["transaction_id"] is the wallet transaction (usable with wallet.transactions.get).
  • If the risk engine holds the transfer, the response includes review_status: EN_REVISION.

Bre-B to another bank #

Bre-B (Colombia’s instant payments) are 24/7 instant payments to accounts at other banks using a key (mobile number, document, email or alias). The flow is the same as P2P: resolve the key and send with the token. Your entity must have Bre-B enabled; otherwise the send returns 422 with «Esta entidad aún no tiene habilitadas las transferencias Bre-B.» (“This entity doesn’t have Bre-B transfers enabled yet.”).

limites = client.wallet.breb.limits(DOC)
print(limites["max_per_transaction"], limites["available_today"])

r = client.wallet.breb.resolve(document=DOC, key_type="PHONE", value="3109876543")
if r.found:
    # Show the user r.display_name, r.institution_name and r.key_display
    tx = client.wallet.breb.transfer(
        document=DOC, resolution_token=r.resolution_token, amount="25000",
        description="Payment", idempotency_key="nova-breb-2026-11-02-000045",
    )
    if tx.data["status"] == "PENDING":
        print("The destination bank hasn't confirmed yet: wait for wallets.transfer.settled")
JSON
{"found": true, "resolution_token": "brb-E-…", "expires_in": 300, "key_type": "PHONE",
 "key_display": "******6543", "display_name": "Ca*** Ro***",
 "institution_name": "Banco Sandbox", "participant_code": "SBXBANK", "is_local": false}
  • key_type: PHONE, DOCUMENT, EMAIL or ALIAS.
  • If the key turns out to belong to an affiliate of your own entity (is_local: true), the send is executed as an internal transfer with the same token.
  • data.status of the send: COMPLETED (settled) or PENDING (the destination bank hasn’t confirmed yet; the outcome arrives via event or lookup). A rejection by the destination bank is a 422 with the reason and no money is debited.

Limits depend on the holder’s tier:

FieldWhat it is
tierThe holder’s tier (TIER_1…)
max_per_transactionCap per transfer
max_daily_amount / max_monthly_amountDaily and monthly caps
used_today / used_monthAmount already used
available_todayWhat they can still send today

Check the limits before offering the transfer: an amount above them returns 422.

Utility bills #

Bill payment has three steps: choose the biller (agreement), look up the bill to get the exact amount, and pay it. For a complete flow, with user confirmation, retries and receipt, follow the recipe Collect a bill from the wallet.

convenios = client.wallet.bills.billers(category="POWER", q="epm")   # doesn't need document

factura = client.wallet.bills.inquire(document=DOC, biller_code="EPM", reference="123456789")
print(factura["holder_name"], factura["amount"], factura["due_date"], factura["status"])

tx = client.wallet.bills.pay(
    document=DOC, biller_code="EPM", reference="123456789",
    amount=factura["amount"],                       # the amount from the lookup
    idempotency_key="nova-bill-EPM-123456789-2026-10",
)
print(tx.data["status"], tx.data.get("approval_number"))

historial = client.wallet.bills.list_payments(DOC, kind="BILL", limit=20)
pago = client.wallet.bills.get_payment(historial.results[0]["id"], document=DOC)

Bill lookup (in the sandbox, EPM / 123456789 is worth 90,000):

JSON
{"biller": {"code": "EPM", "name": "EPM Energía", "category": "POWER"}, "reference": "123456789",
 "holder_name": "Sa*** Ti***", "amount": "90000.00", "currency": "COP", "due_date": "2026-11-15",
 "status": "PENDING", "allows_partial": false, "min_amount": "90000.00", "max_amount": "90000.00",
 "period": "2026-10"}
  • Biller categories: UTILITIES, TELCO, TV, WATER, POWER, GAS, TAX, INTERNET, EDUCATION, OTHER.
  • Instead of biller_code + reference you can look up with barcode (the bill’s GS1 code).
  • The amount to pay must be the one from the lookup, except for billers that allow partial payment (allows_partial: true), which accept any value between min_amount and max_amount.
  • data.status of the payment: APPROVED (HTTP 201, with approval_number) or PENDING (HTTP 202: the collector hasn’t confirmed yet). A PENDING payment is reconciled with the collector every time you fetch its detail with bills.get_payment.
  • History kind: BILL (bills) or TOPUP (top-ups).

Mobile top-ups #

GET/v1/gateway/wallet/topups/operators/ returns exactly what a top-up accepts: operators, amount range, suggested amounts and bundles. source: catalog means your entity has an aggregator catalog; default means the country’s operators with the default range.

catalogo = client.wallet.topups.operators()
claro = next(o for o in catalogo["operators"] if o["code"] == "CLARO")
print(claro["min_amount"], claro["max_amount"], claro["suggested_amounts"])

tx = client.wallet.topups.create(
    document=DOC, phone_number="3001234567", carrier="CLARO", amount="20000",
    idempotency_key="nova-topup-2026-11-02-000123",
)
print(tx.data["status"], tx.data["phone_number_masked"])

Validations: a 10-digit Colombian mobile number starting with 3 (INVALID_PHONE), an enabled operator (OPERATOR_NOT_FOUND), an amount within the operator’s range (INVALID_AMOUNT), plus the holder’s balance and limits.

Accounts and pockets #

Each holder has their main wallet and can have sub-accounts: pockets (pocket), savings (savings) or goals (goal). Creating sub-accounts and moving money between them requires the wallet:accounts scope; listing them, wallet:read.

cuentas = client.wallet.accounts.list(DOC)          # == client.accounts.list(DOC)
principal = next(c for c in cuentas if c.is_primary or c.kind.lower() == "wallet")

ahorro = client.wallet.accounts.create_pocket(
    document=DOC, name="Vacation", kind="savings",
    target_amount="500000", target_date="2026-12-31",
    idempotency_key="nova-pocket-1000000001-vacaciones",
)

envelope = client.wallet.accounts.transfer(
    document=DOC, from_account=principal.id, to_account=ahorro.id, amount="20000",
    idempotency_key="nova-acc-tx-2026-11-02-0001",
)
print(envelope["transaction_id"], envelope["data"])
  • kind is lowercase (pocket, savings, goal); the SDKs normalize it.
  • The SDKs send target_amount and target_date (savings goal) when you pass them; the contract accepts them as additional fields.
  • accounts.transfer returns the gateway envelope as is (a dictionary/object), not a typed Transaction.

Cards #

Cards are debit cards, virtual or physical. The gateway only exposes non-sensitive metadata (last_four, brand, status): the PAN and CVV never leave the processor.

stateDiagram-v2
    [*] --> ACTIVE: issue
    ACTIVE --> FROZEN: set_status(freeze)
    FROZEN --> ACTIVE: set_status(unfreeze)
    ACTIVE --> BLOCKED: set_status(block)
    FROZEN --> BLOCKED: set_status(block)
    BLOCKED --> [*]

block is terminal: a blocked card can’t be reactivated; issue a new one.

tarjeta = client.wallet.cards.issue(document=DOC, form_factor="virtual",
                                    idempotency_key="nova-card-1000000001-v1")
print(tarjeta.id, tarjeta.last_four, tarjeta.status)

client.wallet.cards.set_status(tarjeta.id, document=DOC, status="freeze")
client.wallet.cards.set_status(tarjeta.id, document=DOC, status="unfreeze")

tarjetas = client.wallet.cards.list(DOC)
movs = client.wallet.cards.transactions(tarjeta.id, document=DOC)

form_factor is virtual (default) or physical; status is freeze, unfreeze or block. The SDKs send them in lowercase even if you write them in uppercase. Issuing a card emits the wallets.card.issued event.

Currency exchange (FX) #

First you quote (a pure read: it doesn’t move money and isn’t billed) and then you execute the conversion, passing the quote’s quote_id. The rate is indicative: the engine resolves it again at execution time.

cotizacion = client.wallet.fx.quote(document=DOC, sell_currency="COP",
                                    buy_currency="USD", amount="1000000")
print(cotizacion["rate"], cotizacion["converted_amount"])

envelope = client.wallet.fx.convert(
    document=DOC, sell_currency="COP", buy_currency="USD", amount="1000000",
    quote_id=cotizacion.get("quote_id"),
    idempotency_key="nova-fx-2026-11-02-0007",
)

The contract names are sell_currency and buy_currency (the SDKs send them in uppercase). fx.convert returns the gateway envelope as is. A currency pair without a published rate returns 422.

Business errors #

Errors arrive as application/problem+json (see Errors). When the engine rejects a request, detail carries the reason the engine gave—its message in Spanish, for example «Esta factura ya está pagada.» (“This bill is already paid.")—and, if the engine sends no message, the code: The request was rejected by the processing engine (code: BILL_ALREADY_PAID). The “Engine code” column is the engine code behind each case: use it to recognize the case in logs and when talking to support (always quote the trace_id).

HTTPEngine codeWhenWhat to do
400—Idempotency-Key missing on P2P, Bre-B, bill or top-upSend the header
400RESOLUTION_REQUIRED / RESOLUTION_INVALIDSend without a token, or with a token from another flow (Bre-B on P2P or vice versa)Resolve again with the correct operation
400INVALID_PHONE / INVALID_AMOUNTTop-up phone number or amount breaks the rulesFix the value
404MEMBER_NOT_FOUNDThe document isn’t an active affiliate of your entitySync the affiliate
404BILLER_NOT_FOUND / OPERATOR_NOT_FOUNDBiller or operator not enabledList the catalog again
409BILL_ALREADY_PAIDThe bill (or that period) is already paidShow the existing payment; don’t retry
409—The same Idempotency-Key is still in progressWait and retry with the same key
422INSUFFICIENT_FUNDSInsufficient available balanceAsk for a top-up or a smaller amount
422RESOLUTION_EXPIREDMore than 300 s since resolveResolve again and confirm again
422SELF_TRANSFERThe recipient is the same holder—
422AMOUNT_MISMATCHYou’re paying an amount different from the bill’sUse the amount from the lookup
422TRANSFER_REJECTED / PAYMENT_REJECTEDThe destination bank or the collector rejected it; no money was debitedInform the user
422—Bre-B limit exceeded, Bre-B not enabledCheck breb.limits
502 / 504—The engine failed or didn’t respondRetry with the same Idempotency-Key

A 409 isn’t always an idempotency conflict. The SDKs raise the same exception for both; tell them apart by the problem’s type: …/idempotency-conflict when it’s your key still in progress and …/upstream-error when the engine rejected it on a business rule.

from compense_gateway import CompenseAPIError, IdempotencyConflictError, NotFoundError, ValidationError

try:
    tx = client.wallet.p2p.transfer(document=DOC, resolution_token=r.resolution_token,
                                    amount="30000", idempotency_key="nova-p2p-2026-11-02-000123")
except ValidationError as e:                 # 422: balance, limits, expired token…
    print("Rejected:", e.detail, e.trace_id)
except IdempotencyConflictError as e:        # 409
    if e.problem.type.endswith("/idempotency-conflict"):
        print("The same key is still in progress: retry later with the SAME key")
    else:
        print("Business conflict:", e.detail)
except NotFoundError as e:                   # 404: holder doesn't exist
    print("Not found:", e.trace_id)
except CompenseAPIError as e:                # 400 and the rest
    print(e.status, e.detail, e.trace_id)

Events #

Wallet events are emitted in the same transaction as the money movement: if the money didn’t move, there’s no success event, and a retry with the same Idempotency-Key doesn’t duplicate it. Subscribe to them as explained in Subscription and verify the signature as described in Verification.

Wallet #

EventSinceWhen it's emitted
wallets.cashin.initiated2026-07-21A cash-in intent was recorded; no money has moved yet.
wallets.cashin.confirmed2026-07-21The source confirmed the cash-in and e-money was issued to the wallet.
wallets.transfer.created2026-07-21A transfer (P2P, Bre-B or between pockets) was created and posted to the ledger.
wallets.transfer.settled2026-07-21The transfer was settled.
wallets.transfer.failed2026-07-21The rail rejected the transfer; no money moved.
wallets.disbursement.credited2026-07-21A subsidy or credit was paid directly into the wallet.
wallets.withdrawal.authorized2026-07-21An ATM withdrawal was authorized (single-use token and funds on hold).
wallets.bill_payment.completed2026-07-21The holder paid a utility bill and the biller accepted it.
wallets.qr_payment.completed2026-07-21The holder paid an interoperable QR code (EMVCo).
wallets.transaction.reversed2026-07-21A transaction was reversed (refund).
wallets.dispute.opened2026-07-21The holder opened a dispute on a transaction.
wallets.dispute.resolved2026-07-21An operator resolved the dispute (in the holder’s favor, with a reversal, or rejected).
wallets.card.issued2026-07-21A card was issued (only the last 4 digits are sent).
wallets.topup.completed2026-10-27A mobile top-up was posted (carrier, masked phone number, amount).

resource fields of the events this guide triggers:

Eventresource fields
wallets.transfer.createdtransaction_id, source_wallet_id, recipient_wallet_id (null if it goes to another bank), amount, currency, rail_reference, is_internal, status; Bre-B adds rail: "breb"
wallets.transfer.failedtransaction_id, source_wallet_id, amount, currency, rail_reference, reason, status
wallets.transfer.settledThe in-flight transfer has settled (see the catalog)
wallets.bill_payment.completedtransaction_id, wallet_id, service_id, amount, currency, provider, provider_reference, status
wallets.topup.completedtransaction_id, wallet_id, carrier, phone_number_masked, amount, currency, status
JSON
{"event": "wallets.topup.completed", "version": "1", "tenant_id": "nova",
 "occurred_at": "2026-11-02T15:00:00Z",
 "resource": {"transaction_id": "wtx-E-…", "wallet_id": "…", "carrier": "CLARO",
              "phone_number_masked": "******4567", "amount": "20000.00", "currency": "COP",
              "status": "completed"}}

Reconcile by resource.transaction_id, which is the same tx.data["transaction_id"] the write returned to you.

Sandbox #

With a sandbox key nothing reaches the real engine: the gateway returns deterministic data (same request, same response) with "environment": "sandbox". The full table is in Test data.

Holder documentProfileWallet behavior
1000000001EligibleBalance 500,000; everything works
1000000002IneligibleBalance 0 → INSUFFICIENT_FUNDS on any send; can’t issue a card
1000000004With debtsBalance 100 → INSUFFICIENT_FUNDS
9999999999Doesn’t exist404 MEMBER_NOT_FOUND
any otherEligibleSame as 1000000001

Test cases:

  • Alias or key 9999999999 → {"found": false}.
  • P2P with an alias equal to the payer’s document → 422 SELF_TRANSFER.
  • Send with a token from another flow (Bre-B on P2P or vice versa) → 400 RESOLUTION_INVALID.
  • Bre-B for 11111 → 422 TRANSFER_REJECTED (rejection by the destination bank).
  • Bill with a reference ending in 0 → 409 BILL_ALREADY_PAID; unknown biller → 404; paying an amount different from the lookup → 422 AMOUNT_MISMATCH. Each bill’s amount is fixed per biller and reference (between 50,000 and 95,000): look it up first.
  • Simulated billers: EPM, ACUEDUCTO, VANTI, ETB. Operators: CLARO, MOVISTAR, TIGO, WOM (1,000–200,000).
  • Transactions: three fixed ones per holder (October 1, 5 and 10, 2026), paginated and filterable by date; their id works in the detail endpoint.

Magic amounts: settle, fail or return #

A real transfer can stay PENDING and end up settled, failed or returned days later. To test those outcomes without waiting, the transfer simulator advances one status per call (PENDING → PROCESSING → …) and the amount decides the ending:

AmountOutcome
22.22FAILED (doesn’t settle; reason_code: TRANSFER_FAILED)
33.33RETURNED (settles and is then returned; reason_code: TRANSFER_RETURNED)
11.11 or any otherSETTLED
estado = {"status": "PENDING", "amount": "22.22", "transaction_id": "wtx-E-3f1c9a0b2d4e"}
while True:
    paso = client.sandbox.simulate.wallet_transfer(**estado)
    print(paso["previous_status"], "→", paso["status"])
    if paso["terminal"]:
        break
    estado["status"] = paso["status"]
# PENDING → PROCESSING, PROCESSING → FAILED

The simulator is a pure transformation: it doesn’t move balances or emit webhooks. Use it in your tests to walk through every branch of your reconciliation (wallets.transfer.settled and wallets.transfer.failed).

Accounts, cards and FX in the sandbox

The simulated responses for accounts/pockets, cards and FX come from the previous wallet surface: amounts are simulated in DOP and their shape doesn’t fully match the contract yet (the account and card lists arrive in results, the card status change expects ACTIVE/FROZEN/BLOCKED, and the quote ignores sell_currency/buy_currency). Use them to test the flow and error handling, not to validate values; balance, transactions, P2P, Bre-B, bills and top-ups do follow the contract.

What’s next #