Testing in Sandbox
The Koywe sandbox environment allows you to test all payment flows with simulated transactions before going live.
Sandbox Environment
Base URL
All sandbox API requests use:
https://api-sandbox.koywe.comGetting Test Credentials
Contact soporte@koywe.com to receive:
- Sandbox API Key
- Sandbox Secret
- Organization ID
- Merchant ID
Sandbox vs Production: Sandbox credentials are completely separate from production. No real money is involved in sandbox testing.
Sandbox behavior differences
Sandbox mirrors production closely, but a handful of behaviors differ in ways that trip up first-time integrators. Budget time for these up front:
| Behavior | What actually happens in sandbox |
|---|---|
PAYOUT orders | Create as PENDING, then transition to FAILED. USD is deducted and then refunded. The payout service itself does not execute — use this to test the state-machine, not the external wire. |
KHIPU PAYIN | Auto-completes on the Koywe side without you visiting the Khipu link. |
QRI payment method (CLP) | Broken in sandbox — use KHIPU instead. |
BALANCE_TRANSFER without a policy | Blocked with POL00002. Create a policy and an ALLOW rule first (see below). |
BALANCE_TRANSFER with a policy | Completes instantly — no intermediate PROCESSING state. |
| Invalid document numbers | Always enforced, including in sandbox (DC00010). Tests need valid document numbers per country (see the seed list below). |
| Payout destination currency mismatch | BAA00008 — the destination account’s currency must exactly match the order’s destination currency. |
Wrong organizationId in config | GETs silently return empty; POSTs fail with MC00015 (“Merchant does not belong to the organization”). If reads work but writes fail on a fresh setup, check this first. |
| MFA-gated operations | Orders transition into ON_HOLD and wait (POL00007). Approve via the dashboard or pass --mfa-token / use flow order --wait. |
Seed test document numbers
Document validation (DC00010) is enforced in sandbox. The values below pass the country-specific format checks and are safe for tests:
| Country | Document type | Example |
|---|---|---|
| Chile | RUT | 11111111-1 |
| Brazil (individual) | CPF | 11144477735 |
| Brazil (company) | CNPJ | 11222333000181 |
| Colombia | CC | 1020304050 |
| Mexico | RFC | XAXX010101000 |
| Argentina | CUIT | 20123456780 |
| Peru | DNI | 12345678 |
Minimum viable policy for BALANCE_TRANSFER
Before you can create a BALANCE_TRANSFER or any MFA-gated order in sandbox, the organization needs an active policy with a matching rule:
npx @koyweforest/cli policy create --data '{"name":"default"}'
npx @koyweforest/cli policy rules create --data '{
"name": "allow-balance-transfer",
"scope": "ORDER",
"match": { "orderType": ["BALANCE_TRANSFER"] },
"decision": { "action": "ALLOW" }
}'See Passkeys & Approvals for the full policy model and Error Code Catalog for POL* codes.
Test Payment Methods by Country
Colombia (COP) 🇨🇴
PSE (Pagos Seguros en Línea)
Test Banks:
BANCOLOMBIADAVIVIENDABOGOTAOCCIDENTE
Usage:
{
"method": "PSE",
"extra": { "bankAccount": { "name": "BANCOLOMBIA" } } // Any test bank
}Behavior:
- Payment link is fully functional in sandbox
- Follow the simulated payment process
- You can choose to succeed or fail the payment during the flow
- Allows testing of complete user experience
Nequi
Usage:
{
"method": "NEQUI"
}Behavior:
- Generates a test QR code
- After scanning the QR code, you’ll be given options to succeed or fail the payment
- Simulates the complete Nequi payment experience
Brazil (BRL) 🇧🇷
PIX Static
Usage:
{
"method": "PIX_STATIC"
}Behavior:
- Generates a test QR code
- After scanning the QR code, you’ll be given options to succeed or fail the payment
- Simulates the complete PIX payment experience
PIX Dynamic
Usage:
{
"method": "PIX_DYNAMIC"
}Behavior:
- Generates a test QR code
- After scanning the QR code, you’ll be given options to succeed or fail the payment
- Similar to PIX_STATIC with interactive testing options
Mexico (MXN) 🇲🇽
SPEI (Instant Settlement ⚡)
Usage:
{
"method": "SPEI"
}Behavior:
- Provides test bank account details
- Auto-completes after order creation
- Simulates bank transfer confirmation
Cards
Test Card Numbers:
- Success:
4242424242424242 - Decline:
4000000000000002 - Insufficient Funds:
4000000000009995
Usage:
{
"method": "CARD"
}Chile (CLP) 🇨🇱
Khipu
Usage:
{
"method": "KHIPU"
}Behavior:
- Payment link is fully functional in sandbox
- Follow the simulated payment process
- You can choose to succeed or fail the payment during the flow
- Allows testing of complete user experience
Argentina (ARS) 🇦🇷
Multiple Local Methods
Usage:
{
"method": "KHIPU" // Also works for Argentina
}Behavior:
- Payment link is fully functional in sandbox
- You can choose to succeed or fail the payment during the flow
Test Scenarios
Interactive Testing: Most payment methods (PSE, Khipu) provide functional payment links where you can follow the simulated payment process and choose to succeed or fail. QR-based methods (PIX, Nequi) give you options after scanning the code.
Successful Payment Flow
Test a successful end-to-end payment:
async function testSuccessfulPayment() {
// Any amount will succeed in sandbox
const order = await createPayinOrder(token, orgId, merchantId, {
amount: 50000,
currency: 'COP',
paymentMethod: 'PSE',
bank: 'BANCOLOMBIA'
});
console.log('Order created:', order.id);
console.log('Status:', order.status); // "PENDING"
console.log('Payment URL:', order.paymentUrl);
// In sandbox, order auto-completes
// Wait a few seconds and check status
await sleep(5000);
const updated = await getOrderStatus(token, orgId, merchantId, order.id);
console.log('Updated status:', updated.status); // "COMPLETED"
}Expected flow:
PENDING → PROCESSING → PAID → COMPLETEDFailed Payment Scenario
Test payment failure handling:
Use the special test amount 666 to simulate a failed payment:
async function testFailedPayment() {
const order = await createPayinOrder(token, orgId, merchantId, {
amount: 666, // Special test amount for failure
currency: 'COP',
paymentMethod: 'PSE',
bank: 'BANCOLOMBIA'
});
console.log('Order created:', order.id);
// Wait for processing
await sleep(3000);
const updated = await getOrderStatus(token, orgId, merchantId, order.id);
console.log('Status:', updated.status); // "FAILED"
console.log('Error:', updated.errorMessage);
}Expected flow:
PENDING → PROCESSING → FAILEDExpired Payment
Test order expiration:
async function testExpiredPayment() {
// Set dueDate in the past
const pastDate = new Date();
pastDate.setHours(pastDate.getHours() - 1);
const order = await createPayinOrder(token, orgId, merchantId, {
amount: 50000,
currency: 'COP',
paymentMethod: 'PSE',
bank: 'BANCOLOMBIA',
dueDate: pastDate.toISOString()
});
console.log('Status:', order.status); // "EXPIRED"
}Payment Link Flow (PAYMENT_LINK)
Test the complete Koywe-branded checkout experience:
PAYMENT_LINK is perfect for e-commerce! No contact or payment method needed - just create a link and share it. The customer enters their own data and selects their payment method in the Koywe checkout.
async function testPaymentLink() {
// Create payment link - no contact or payment method needed!
const order = await axios.post(
`https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/orders`,
{
type: 'PAYMENT_LINK',
originCurrencySymbol: 'COP',
destinationCurrencySymbol: 'COP',
amountIn: 50000,
description: 'Test Invoice #123 - Web Design',
externalId: `test-invoice-${Date.now()}`
},
{ headers: { 'Authorization': `Bearer ${token}` } }
);
console.log('✓ Payment link created:', order.data.id);
console.log('📧 Share this link:', order.data.paymentUrl);
console.log('');
console.log('Customer will:');
console.log(' 1. Open the link');
console.log(' 2. See Koywe-branded checkout');
console.log(' 3. Enter their personal information');
console.log(' 4. Select payment method (PSE, PIX, Nequi, etc.)');
console.log(' 5. Complete payment');
// Simulate sharing via email, WhatsApp, or QR code
return order.data;
}
// Usage
const link = await testPaymentLink();
// Open the payment URL in a browser to test the full checkout flow
console.log('\n🌐 Open this URL to test:', link.paymentUrl);def test_payment_link():
# Create payment link - minimal fields!
response = requests.post(
f'https://api-sandbox.koywe.com/api/v1/organizations/{org_id}/merchants/{merchant_id}/orders',
json={
'type': 'PAYMENT_LINK',
'originCurrencySymbol': 'COP',
'destinationCurrencySymbol': 'COP',
'amountIn': 50000,
'description': 'Test Invoice #123 - Web Design'
},
headers={'Authorization': f'Bearer {token}'}
)
order = response.json()
print(f"✓ Payment link created: {order['id']}")
print(f"📧 Share this link: {order['paymentUrl']}")
return order
# Usage
link = test_payment_link()
print(f"\n🌐 Open this URL to test: {link['paymentUrl']}")Testing the checkout flow:
- Create the payment link using the code above
- Copy the
paymentUrl - Open it in your browser
- You’ll see the Koywe-branded checkout page
- Fill in test customer information
- Select a payment method
- Complete the simulated payment (choose success or fail)
- Verify webhook notification is received
Use Case: PAYMENT_LINK is ideal for:
- E-commerce checkout pages
- Invoice payment requests
- Email/WhatsApp payment links
- QR code payments
- Quick payment collections without storing customer data
Testing Webhooks
Setup Webhook Endpoint
Use webhook testing tools:
- webhook.site - Instant webhook URL
- ngrok - Tunnel to localhost
- RequestBin - Webhook inspector
Using webhook.site
Get webhook URL
Visit webhook.site and copy your unique URL
Create order with webhook
const order = await createPayinOrder(token, orgId, merchantId, {
amount: 50000,
webhookUrl: 'https://webhook.site/your-unique-id'
});View webhooks
Return to webhook.site to see webhook events in real-time
Expected Webhook Events
For a successful PAYIN:
order.created- Order is createdorder.pending- Waiting for paymentorder.processing- Payment being processedorder.paid- Payment confirmedorder.completed- Funds credited
Webhook Payload Example
{
"id": "evt_abc123",
"type": "order.completed",
"version": "v1",
"occurred_at": "2025-11-13T15:30:00Z",
"source": "koywe.api",
"environment": "sandbox",
"organization_id": "org_xyz",
"merchant_id": "mrc_abc",
"data": {
"orderId": "ord_123456",
"type": "PAYIN",
"status": "COMPLETED",
"amountIn": 50000,
"currencySymbol": "COP"
}
}Test Data
Test Contacts
Use these test document numbers (all valid in sandbox):
Colombia
{
"documentType": "CC",
"documentNumber": "1234567890",
"fullName": "Juan Pérez Test"
}Brazil
{
"documentType": "CPF",
"documentNumber": "12345678900",
"fullName": "Maria Silva Test"
}Mexico
{
"documentType": "RFC",
"documentNumber": "XAXX010101000",
"fullName": "Pedro García Test"
}Chile
{
"documentType": "RUT",
"documentNumber": "11111111-1",
"fullName": "Ana López Test"
}Test Bank Accounts
For PAYOUT testing:
Colombia
{
"bankCode": "BANCOLOMBIA",
"accountNumber": "1234567890",
"accountType": "savings",
"currencySymbol": "COP"
}Brazil
{
"bankCode": "BANCO_DO_BRASIL",
"accountNumber": "12345678",
"accountType": "checking",
"currencySymbol": "BRL"
}Mexico
{
"bankCode": "BBVA_MEXICO",
"accountNumber": "012345678901234567",
"accountType": "checking",
"currencySymbol": "MXN"
}Chile
{
"bankCode": "BANCO_CHILE",
"accountNumber": "12345678",
"accountType": "checking",
"currencySymbol": "CLP"
}Testing Crypto Operations
Automatic Test Network Selection
Production Network Names in Sandbox: When using production network names like ETHEREUM, POLYGON, or BSC in sandbox, the system automatically routes to test networks (Sepolia, Amoy, BSC Testnet respectively). You don’t need to specify testnet names explicitly.
Test Networks
Sandbox automatically uses these testnets:
| Production Network | Sandbox Testnet | Supported Assets |
|---|---|---|
| ETHEREUM | Sepolia | ETH, USDC, USDT |
| POLYGON | Amoy | MATIC, USDC |
| BSC | BSC Testnet | BNB, USDC |
Example: Specify "network": "POLYGON" in your request, and sandbox will use Amoy automatically.
Test Wallet Address
Use this address for receiving test crypto:
0x0000000000000000000000000000000000000000Never use production addresses in sandbox - Always use test/burn addresses like the zero address above.
ONRAMP Test
async function testOnramp() {
// Create crypto wallet (or use existing)
const wallet = await createCryptoWallet(token, orgId, merchantId, {
address: '0x0000000000000000000000000000000000000000',
network: 'ETHEREUM' // Automatically uses Sepolia in sandbox
});
// Buy USDC with COP
const order = await createOrder(token, orgId, merchantId, {
type: 'ONRAMP',
originCurrencySymbol: 'COP',
destinationCurrencySymbol: 'USDC',
amountIn: 50000,
destinationAccountId: wallet.id
});
console.log('ONRAMP order:', order.id);
// In sandbox, crypto is "sent" to test address
}OFFRAMP Test
async function testOfframp() {
// Sell USDC for COP
const order = await createOrder(token, orgId, merchantId, {
type: 'OFFRAMP',
originCurrencySymbol: 'USDC',
destinationCurrencySymbol: 'COP',
amountIn: 10 // 10 USDC
});
console.log('OFFRAMP order:', order.id);
// In sandbox, order auto-completes
// Fiat is credited to virtual account
}Rate Limits
Sandbox environment has the following limits:
| Limit Type | Value |
|---|---|
| Requests per minute | 100 |
| Requests per hour | 1,000 |
| Orders per day | 10,000 |
Rate limit headers are included in all responses:
X-RateLimit-Limit: Total requests allowedX-RateLimit-Remaining: Requests remainingX-RateLimit-Reset: Unix timestamp when limit resets
Testing Best Practices
Test All Order Types
Test each order type:
- ✅ PAYIN with different payment methods
- ✅ PAYOUT to different countries
- ✅ BALANCE_TRANSFER between currencies
- ✅ ONRAMP for crypto purchases
- ✅ OFFRAMP for crypto sales
- ✅ PAYMENT_LINK with expiry
Test Error Scenarios
Test error handling:
- ✅ Invalid API credentials
- ✅ Insufficient balance for PAYOUT
- ✅ Invalid bank account details
- ✅ Expired quotes
- ✅ Failed payments (amount: 666)
- ✅ Webhook signature verification
Test Idempotency
async function testIdempotency() {
const externalId = `test-order-${Date.now()}`;
// Create order
const order1 = await createPayinOrder(token, orgId, merchantId, {
amount: 50000,
externalId: externalId
});
// Try to create same order again
const order2 = await createPayinOrder(token, orgId, merchantId, {
amount: 50000,
externalId: externalId // Same externalId
});
// Should return same order
console.log(order1.id === order2.id); // true
}Common Test Workflows
E-Commerce Checkout Flow
async function testCheckoutFlow() {
console.log('1. Customer adds items to cart...');
const cartTotal = 50000; // 50,000 COP
console.log('2. Customer proceeds to checkout...');
const contact = await createContact(token, orgId, merchantId, {
email: 'test@example.com',
fullName: 'Test Customer',
country: 'CO',
documentType: 'CC',
documentNumber: '1234567890'
});
console.log('3. Get payment methods...');
const methods = await getPaymentMethods('CO', 'COP');
console.log('4. Create payment order...');
const order = await createPayinOrder(token, orgId, merchantId, {
amount: cartTotal,
currency: 'COP',
contactId: contact.id,
paymentMethod: 'PSE',
bank: 'BANCOLOMBIA',
externalId: `cart-${Date.now()}`
});
console.log('5. Redirect customer to payment...');
console.log('Payment URL:', order.paymentUrl);
console.log('6. Wait for webhook confirmation...');
// In production, webhook handler processes this
await sleep(5000);
console.log('7. Verify order completed...');
const completed = await getOrderStatus(token, orgId, merchantId, order.id);
console.log('Final status:', completed.status); // "COMPLETED"
console.log('8. Fulfill order...');
console.log('✅ Test checkout flow complete!');
}Provider Payout Flow
async function testPayoutFlow() {
console.log('1. Create provider contact...');
const provider = await createContact(token, orgId, merchantId, {
email: 'provider@example.com',
fullName: 'Test Provider',
country: 'CO',
documentType: 'NIT',
documentNumber: '900123456-1',
businessType: 'COMPANY'
});
console.log('2. Add provider bank account...');
const bankAccount = await addBankAccountToContact(token, orgId, merchantId, provider.id, {
country: 'CO',
currency: 'COP',
bankCode: 'BANCOLOMBIA',
accountNumber: '1234567890',
accountType: 'checking'
});
console.log('3. Check virtual balance...');
const balance = await getBalance(token, orgId, merchantId, 'COP');
console.log('Available:', balance.availableBalance, 'COP');
console.log('4. Create payout order...');
const payout = await createPayoutOrder(token, orgId, merchantId, {
amount: 100000,
currency: 'COP',
contactId: provider.id,
destinationAccountId: bankAccount.id,
externalId: `payout-${Date.now()}`
});
console.log('5. Monitor payout status...');
await sleep(3000);
const completed = await getOrderStatus(token, orgId, merchantId, payout.id);
console.log('Final status:', completed.status); // "COMPLETED"
console.log('✅ Test payout flow complete!');
}Simulating Bank Income (Sandbox Only)
Some flows — SPEI deposits, direct bank transfers, certain ARS/MXN rails — work by receiving funds into a virtual bank account provisioned for your merchant. In sandbox, there’s no real bank wire to trigger these. Use the bank-income simulator to register a fake incoming deposit that runs through the normal bank-income processor, exactly as a real wire would.
POST /api/v1/organizations/{organizationId}/merchants/{merchantId}/sandbox/bank-income/simulateThis endpoint is sandbox-only. It returns an error in production.
Request
{
"currency": "ARS",
"amount": 500000,
"documentType": "CUIT",
"documentNumber": "30712345678",
"customerReference": "customer-test-001"
}| Field | Required | Description |
|---|---|---|
currency | ✓ | ARS, MXN, or USD. Selects which fake virtual account is used. |
amount | ✓ | Amount to credit. |
documentType | Conditional | Depositor document type. Required for ARS and MXN unless validation is bypassed by merchant flags. |
documentNumber | Conditional | Depositor document number. Same conditions as documentType. |
customerReference | — | Free-form reference to make the simulated movement easy to trace in reports. |
Response
{
"referenceId": "bm_1234567890",
"virtualAccount": { /* PayInVirtualBankAccount */ },
"virtualAccountProvisioned": true
}referenceId— ID of the registered bank income, usable for reconciliation.virtualAccount— The virtual account the deposit landed in. If the merchant didn’t already have one for this currency, the simulator provisions one on the fly.virtualAccountProvisioned—truethe first time a virtual account had to be created for the currency;falseon subsequent calls.
Example
async function simulateSandboxDeposit() {
const response = await axios.post(
`https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/sandbox/bank-income/simulate`,
{
currency: 'ARS',
amount: 500000,
documentType: 'CUIT',
documentNumber: '30712345678',
customerReference: `test-${Date.now()}`
},
{ headers: { Authorization: `Bearer ${token}` } }
);
console.log('Simulated deposit:', response.data.referenceId);
console.log('Credited to VA:', response.data.virtualAccount.accountNumber);
}Use cases:
- Fund a sandbox merchant so you can test PAYOUT, BALANCE_TRANSFER, or OFFRAMP without going through a full PAYIN flow first.
- Reproduce bank-income edge cases (unknown depositor, document mismatch) by varying the document fields.
- Smoke-test reconciliation pipelines that consume bank-income events.
Moving to Production
When you’re ready to go live:
Request Production Credentials
Contact soporte@koywe.com for production API key and secret
Update Base URL
Change from sandbox to production:
// Sandbox
const BASE_URL = 'https://api-sandbox.koywe.com';
// Production
const BASE_URL = 'https://api.koywe.com';Update Credentials
Replace sandbox credentials with production credentials in your environment variables
Update Webhook URLs
Change webhook endpoints from test URLs to production URLs
Test with Small Amounts
Start with small real transactions to verify everything works
Monitor Closely
Watch your first production transactions carefully
Setup Monitoring
Implement logging, alerts, and monitoring for production
Production Checklist
- Production API credentials obtained
- Base URL updated to production
- Webhook URLs pointing to production servers
- Webhook signature verification implemented
- Error handling tested
- Logging and monitoring setup
- Small test transaction successful
- Team trained on production processes
Troubleshooting
Order Stuck in PENDING
Issue: Order created but stays in PENDING status
Solution:
- In sandbox, wait 30-60 seconds for auto-completion
- Check if you used the failure test amount (666)
- Verify payment method is correct for country
Webhooks Not Received
Issue: No webhook events arriving
Solution:
- Verify webhook URL is publicly accessible
- Check firewall/security settings
- Use webhook.site to test
- Verify webhook endpoint is configured
Insufficient Balance Error
Issue: PAYOUT fails with insufficient balance
Solution:
- Check virtual account balance
- Create PAYIN orders to add funds to sandbox account
- Verify you’re checking the correct currency balance