# Koywe Platform Docs β€” Full Content > Accept payments, pay providers, and manage crypto globally Source: https://docs.koywe.com --- # πŸ‘‹ Welcome to Koywe Platform _Accept payments, pay providers, and manage crypto globally_ Source: https://docs.koywe.com/en # Welcome to Koywe Platform API Koywe Platform enables businesses to collect payments, pay providers, and manage multi-currency operations across Latin America and beyond. ## What You Can Do Collect payments from customers via PSE, PIX, SPEI, and other local payment methods Send payments to vendors and contractors in multiple currencies Transfer funds between currencies instantly within your virtual accounts Buy and sell cryptocurrency with fiat currencies seamlessly --- ## Popular Use Cases ### Accept Customer Payments (PAYIN) Collect payments from your customers across Latin America using local payment methods: - **Colombia**: PSE, Nequi - **Brazil**: PIX - **Mexico**: SPEI (instant settlement) - **Chile**: Khipu - **Argentina**: Multiple options [Quick Guide β†’](/en/accepting-payments) ### Payout to Providers (PAYOUT) Send payments to vendors, contractors, and partners: - Direct bank transfers - Multi-currency support - Automated reconciliation [Quick Guide β†’](/en/paying-providers) ### Currency Exchange (BALANCE_TRANSFER) Transfer funds between currencies instantly within your virtual accounts: - Real-time exchange rates - No external bank transfers needed - Instant settlement [Quick Guide β†’](/en/balance-management) ### Crypto On/Off Ramp Convert between fiat and cryptocurrency: - **Onramp**: Buy crypto (USDC, ETH, BTC, etc.) with fiat - **Offramp**: Sell crypto for fiat - 8 networks supported β€” see [Supported Networks](/en/crypto-operations/onramp#supported-networks) [Onramp Guide β†’](/en/crypto-operations/onramp) | [Offramp Guide β†’](/en/crypto-operations/offramp) --- ## Getting Started ### Get Credentials Contact soporte@koywe.com to receive your sandbox API key, secret, organization ID, and merchant ID. ### Make Your First API Call Follow our 5-minute quickstart to create your first payment order. ### Integrate into Your Application Use our comprehensive guides to build a production-ready integration. ### Go Live Switch to production credentials and start processing real payments. Make your first API call and create a payment order in minutes Understand the fundamentals of the Koywe Payments system Learn how to authenticate and manage your API credentials --- ## How It Works |Create Order| B[Koywe API] B -->|Payment URL| A A -->|Redirect| C[Customer] C -->|Completes Payment| D[Payment Provider] D -->|Confirms| B B -->|Credits Funds| E[Virtual Account] B -->|Webhook| A`} /> **The Flow:** 1. Your application creates a payment order via the Koywe API 2. Koywe returns a payment URL 3. Customer completes payment through their preferred method 4. Funds are credited to your virtual balance 5. You receive real-time webhook notifications 6. Funds can be used for payouts, transfers, or crypto operations --- ## Key Features ### Multi-Currency Virtual Accounts Hold balances in multiple currencies (COP, BRL, MXN, CLP, USD, EUR, and more) without opening multiple bank accounts. ### Real-Time Notifications Receive webhook events for all order status changes, enabling instant order fulfillment and reconciliation. ### Comprehensive API RESTful API with detailed documentation, code examples in multiple languages, and extensive testing tools. ### Sandbox Environment Test all payment flows with simulated transactions before going live. ### Security First Bank-grade security with encrypted communications, webhook signature verification, and secure credential management. --- ## Supported Regions **Payment Methods**: PSE, Nequi **Currency**: COP (Colombian Peso) **Payouts**: Bank transfers to Colombian banks **Payment Methods**: PIX (Static & Dynamic) **Currency**: BRL (Brazilian Real) **Payouts**: PIX transfers **Payment Methods**: SPEI (instant settlement) **Currency**: MXN (Mexican Peso) **Payouts**: SPEI bank transfers (instant) **Payment Methods**: Khipu, Bank transfers **Currency**: CLP (Chilean Peso) **Payouts**: Bank transfers **Payment Methods**: Multiple local options **Currency**: ARS (Argentine Peso) **Payouts**: Bank transfers **Payment Methods**: Local bank transfers **Currency**: PEN (Peruvian Sol) **Payouts**: Bank transfers --- ## Need Help? soporte@koywe.com We typically respond within 24 hours Step-by-step integration guide with authentication, setup, and configuration Learn how to test all payment scenarios in sandbox Common issues and solutions --- ## Ready to Start? Follow our 5-minute quickstart guide to create your first payment order and see the API in action. --- # πŸ”‘ Sandbox Credentials Source: https://docs.koywe.com/en/getting-started/credentials ## Sandbox Authentication To access the sandbox environment and make API calls, you'll need to authenticate using your credentials. Here's how to get started: ## Authentication Flow B[Get Credentials] B --> C[Make Auth Request] C --> D[Receive Token] D --> E[Use Token in API Calls] E --> F[End]`} /> ## Required Credentials Before making any API calls, you'll need the following credentials: - Api Key - Secret ## Getting an Authentication Token To get an authentication token, make a POST request to the authentication endpoint: ```bash curl -X POST 'https://api-sandbox.koywe.com/api/v1/auth/sign-in' \ -H 'Content-Type: application/json' \ -d '{ "apiKey": "your_apiKey", "secret": "your_secret" }' ``` ### Request Parameters | Parameter | Type | Required | Description | |-----------|--------|----------|--------------------------------| | apiKey | string | Yes | Your api key | | secret | string | Yes | Your api key secret | ### Example Response ```json { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", } ``` ## Using the Token Once you have received the token, include it in the Authorization header of all subsequent API requests: ```bash curl -X POST 'https://api-sandbox.koywe.com/api/v1/organizations/YOUR_ORG_ID/merchants/YOUR_MERCHANT_ID/orders' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer your_token_here' \ -d '{ // Your request payload }' ``` ## Token Expiration The authentication token expires after a certain period (typically 1 hour). When the token expires, you'll need to make a new authentication request to get a fresh token. ## Error Responses If the authentication fails, you'll receive an error response: ```json { "error": "Invalid credentials", "message": "The provided username or password is incorrect" } ``` Common error scenarios: - Invalid api-key or secret - Missing required fields - Malformed request ## Best Practices 1. **Secure Storage**: Never store credentials in plain text or commit them to version control 2. **Token Management**: Implement proper token storage and refresh mechanisms 3. **Error Handling**: Always handle authentication errors gracefully 4. **Environment Variables**: Use environment variables for sensitive information ## Need Help? If you encounter any issues with authentication or need help with your credentials, please contact our support team. --- # Onboarding & KYB _Registration drafts, onboarding status, and merchant KYB_ Source: https://docs.koywe.com/en/getting-started/onboarding-kyb # Onboarding & KYB Koywe exposes a public onboarding surface for user registration flows and a public KYB surface for merchant verification. These are separate but related workflows. ## Two Public Flows ### User Pre-Onboarding Use these endpoints to create and submit registration drafts: - `POST /api/v1/pre-onboarding/registration-draft` - `PUT /api/v1/pre-onboarding/registration-draft/{draftId}` - `GET /api/v1/pre-onboarding/registration-draft/{draftId}` - `POST /api/v1/pre-onboarding/registration-form/{draftId}` - `GET /api/v1/pre-onboarding/registrations` - `GET /api/v1/pre-onboarding/registrations/{registrationId}/status` These endpoints are useful when your team needs to complete setup data incrementally before final submission. ### Merchant KYB Use these endpoints once a merchant exists and is ready for verification: - `POST /api/v1/organizations/{organizationId}/merchants/{merchantId}/onboarding/kyb` - `GET /api/v1/organizations/{organizationId}/merchants/{merchantId}/onboarding/kyb` - `GET /api/v1/organizations/{organizationId}/merchants/{merchantId}/onboarding/kyb/{kybId}` ## Recommended Flow >API: Create or update registration draft U->>API: Submit registration form API-->>U: Registration ID + status U->>API: Poll onboarding status M->>API: Trigger merchant KYB API->>KYB: Submit merchant data KYB-->>API: KYB status / form URL API-->>M: KYB process status`} /> ## Check Current Onboarding State The authenticated user can retrieve onboarding evaluations with: - `GET /api/v1/onboarding/me` This endpoint is useful for dashboards and internal setup tooling because it shows where a user is blocked. ## Trigger Merchant KYB ```bash curl -X POST 'https://api-sandbox.koywe.com/api/v1/organizations/YOUR_ORG_ID/merchants/YOUR_MERCHANT_ID/onboarding/kyb' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "forceRetrigger": false }' ``` After triggering, poll the KYB status endpoint until the provider form link or final state is available. ## When You Need This - Use pre-onboarding when the person or team is still preparing registration data. - Use `onboarding/me` when you need a read-only status summary for the current user. - Use merchant KYB after the merchant has been created and basic entity data is ready. ## Best Practices - Save draft IDs so users can resume registration instead of starting over. - Treat KYB as asynchronous and expect polling or dashboard follow-up. - Separate β€œuser registration complete” from β€œmerchant KYB approved” in your internal state model. ## Next Steps - [Organization Setup & Invitations](/en/getting-started/organization-setup) - [Passkeys & Approvals](/en/advanced/passkeys-and-approvals) - [API Reference](/api-reference) --- # Organization Setup & Invitations _Set up organizations, merchants, users, and API credentials_ Source: https://docs.koywe.com/en/getting-started/organization-setup # Organization Setup & Invitations This guide covers the public setup endpoints used before you start processing payments: organization invitations, merchant creation, user invitations, role assignment, and API credential generation. ## Typical Setup Flow B[List organizations] B --> C[Create or review merchants] C --> D[Invite organization or merchant users] D --> E[Create API credentials] E --> F[Start payment integration]`} /> --- ## 1. Accept Organization Access These endpoints are typically used when a new user is joining an existing organization: - `POST /api/v1/auth/organization-invitation/redeem` - `POST /api/v1/auth/invitation/redeem` - `POST /api/v1/auth/invitation/check-status` Use the organization invitation flow for organization-wide access and the merchant invitation flow for merchant-scoped access. ## 2. List Organizations After authentication, list the organizations the current user can access: ```bash curl -X GET 'https://api-sandbox.koywe.com/api/v1/organizations' \ -H 'Authorization: Bearer YOUR_TOKEN' ``` This response gives you the `organizationId` needed for all organization-scoped operations. ## 3. Create and Manage Merchants Merchant lifecycle endpoints: - `POST /api/v1/organizations/{organizationId}/merchants` - `GET /api/v1/organizations/{organizationId}/merchants` - `GET /api/v1/organizations/{organizationId}/merchants/{merchantId}` - `PUT /api/v1/organizations/{organizationId}/merchants/{merchantId}` - `DELETE /api/v1/organizations/{organizationId}/merchants/{merchantId}` When a merchant is created, Koywe automatically provisions default resources such as contacts and virtual accounts. See [Organizations & Merchants](/en/core-concepts/organizations-and-merchants) for the resource model. ### Minimal Merchant Creation Example ```bash curl -X POST 'https://api-sandbox.koywe.com/api/v1/organizations/YOUR_ORG_ID/merchants' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "name": "Acme Colombia", "countrySymbol": "CO" }' ``` ## 4. Invite Users and Assign Roles Public invitation endpoints: - `POST /api/v1/users/organizations/{organizationId}/invitations` - `GET /api/v1/users/organizations/{organizationId}/invitations` - `POST /api/v1/users/organizations/{organizationId}/merchants/{merchantId}/invitations` - `GET /api/v1/users/organizations/{organizationId}/merchants/{merchantId}/invitations` Public user-management endpoints: - `GET /api/v1/organizations/{organizationId}/users` - `PUT /api/v1/organizations/{organizationId}/users` - `GET /api/v1/organizations/{organizationId}/merchants/{merchantId}/users` - `PUT /api/v1/organizations/{organizationId}/merchants/{merchantId}/users` Use organization invitations for company-wide administrators and merchant invitations for operational users limited to a specific merchant. ## 5. Create API Credentials Credential endpoints: - `POST /api/v1/auth/organizations/{organizationId}/credentials` - `POST /api/v1/auth/organizations/{organizationId}/merchants/{merchantId}/credentials` - `POST /api/v1/auth/organizations/{organizationId}/merchants/{merchantId}/pos/credentials/create` Use organization credentials when one integration needs broad access across merchants. Use merchant credentials when you want stricter isolation. ### Merchant Credential Example ```bash curl -X POST 'https://api-sandbox.koywe.com/api/v1/auth/organizations/YOUR_ORG_ID/merchants/YOUR_MERCHANT_ID/credentials' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "name": "backend-service", "roles": ["merchant_root"] }' ``` ## 6. Review Merchant Resources Useful follow-up endpoints after setup: - `GET /api/v1/organizations/{organizationId}/merchants/{merchantId}/features` - `GET /api/v1/organizations/{organizationId}/merchants/{merchantId}/virtual-accounts` - `GET /api/v1/organizations/{organizationId}/merchants/{merchantId}/accounts/balances` - `GET /api/v1/organizations/{organizationId}/balances` These help confirm the merchant is ready for PAYIN, PAYOUT, and balance-management flows. ## Best Practices - Keep sandbox and production organizations and credentials separate. - Prefer merchant-scoped credentials unless your use case truly needs organization-wide access. - Use invitations for ongoing access management instead of sharing credentials. - Capture and store the returned IDs for organizations, merchants, users, and credentials in your internal config store. ## Next Steps - [Onboarding & KYB](/en/getting-started/onboarding-kyb) - [Deposit Accounts](/en/balance-management/deposit-accounts) - [Quickstart](/en/getting-started/quickstart) - [API Reference](/api-reference) --- # ⚑ 5-Minute Quickstart _Make your first API call in minutes_ Source: https://docs.koywe.com/en/getting-started/quickstart # 5-Minute Quickstart ## What You'll Build Create a simple PAYIN order to accept a payment from a customer. This quickstart gets you up and running without diving into complex concepts. By the end of this guide, you'll be able to: - Authenticate with the Koywe API - Create a payment order - Track the payment status ## Prerequisites Before you begin, make sure you have: - [ ] API Key and Secret (contact soporte@koywe.com if you don't have these) - [ ] Organization ID - [ ] Merchant ID - [ ] A tool to make HTTP requests (cURL, Postman, or your preferred programming language) This quickstart uses the sandbox environment. All payments are simulated and no real money is involved. --- ## Fast lane: the CLI (β‰ˆ 30 seconds) If you're an AI agent or you just want to see an order move end-to-end, skip the HTTP walkthrough and use the [Koywe CLI](/en/cli). Three commands β€” browser sign-in, pick an org, run a guided order flow: ```bash npx @koyweforest/cli init # browser login + auto-create credentials npx @koyweforest/cli config set organizationId npx @koyweforest/cli flow order # interactive: quote β†’ create β†’ wait ``` `flow order` prompts for the type, currency, amount, and payment method, then chains quote + order creation + status polling into one command. 26 of the 142 commands (the `create` and `update` ones) accept `--schema` to print the full request-body JSON schema β€” run that before constructing a payload instead of guessing fields. If you prefer to hit the API directly, keep reading β€” the rest of this page walks through the same flow with curl, Node, and Python. --- ## Step 1: Authenticate First, obtain an access token using your API credentials: {/* Multi-language code examples */} {/* cURL */} ```bash curl -X POST 'https://api-sandbox.koywe.com/api/v1/auth/sign-in' \ -H 'Content-Type: application/json' \ -d '{ "apiKey": "your_api_key", "secret": "your_secret" }' ``` {/* Node.js */} ```javascript const axios = require('axios'); async function authenticate() { const response = await axios.post( 'https://api-sandbox.koywe.com/api/v1/auth/sign-in', { apiKey: process.env.KOYWE_API_KEY, secret: process.env.KOYWE_SECRET } ); return response.data.token; } // Usage const token = await authenticate(); console.log('Token:', token); ``` {/* Python */} ```python def authenticate(): response = requests.post( 'https://api-sandbox.koywe.com/api/v1/auth/sign-in', json={ 'apiKey': os.environ['KOYWE_API_KEY'], 'secret': os.environ['KOYWE_SECRET'] } ) return response.json()['token'] # Usage token = authenticate() print(f'Token: {token}') ``` **Response:** ```json { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." } ``` Store this token securely. You'll need to include it in all subsequent requests as: `Authorization: Bearer YOUR_TOKEN` The token expires after 1 hour. In production, implement token refresh logic. --- ## Step 2: Create a Contact (Optional) While optional, creating a contact helps you track which customer made the payment: {/* Multi-language code examples */} {/* Node.js */} ```javascript async function createContact(token, orgId, merchantId) { const response = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/contacts`, { firstName: 'Juan', lastName: 'PΓ©rez', countrySymbol: 'CO', businessType: 'PERSON', type: 'PERSON', email: 'customer@example.com', phone: '+573001234567', documentType: 'CC', documentNumber: '1234567890' }, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } } ); return response.data; } // Usage const contact = await createContact(token, 'your_org_id', 'your_merchant_id'); console.log('Contact ID:', contact.id); ``` {/* Python */} ```python def create_contact(token, org_id, merchant_id): response = requests.post( f'https://api-sandbox.koywe.com/api/v1/organizations/{org_id}/merchants/{merchant_id}/contacts', json={ 'email': 'customer@example.com', 'phone': '+573001234567', 'firstName': 'Juan', 'lastName': 'PΓ©rez', 'countrySymbol': 'CO', 'documentType': 'CC', 'documentNumber': '1234567890', 'businessType': 'PERSON', 'type': 'PERSON' }, headers={ 'Authorization': f'Bearer {token}', 'Content-Type': 'application/json' } ) return response.json() # Usage contact = create_contact(token, 'your_org_id', 'your_merchant_id') print(f"Contact ID: {contact['id']}") ``` {/* cURL */} ```bash curl -X POST 'https://api-sandbox.koywe.com/api/v1/organizations/YOUR_ORG_ID/merchants/YOUR_MERCHANT_ID/contacts' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "email": "customer@example.com", "phone": "+573001234567", "firstName": "Juan", "lastName": "PΓ©rez", "countrySymbol": "CO", "documentType": "CC", "documentNumber": "1234567890", "businessType": "PERSON", "type": "PERSON" }' ``` --- ## Step 3: Get a Quote Get an exchange rate quote for the payment (optional but recommended for transparency): {/* Multi-language code examples */} {/* Node.js */} ```javascript async function getQuote(token, orgId, merchantId) { const response = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/quotes`, { originCurrencySymbol: 'COP', destinationCurrencySymbol: 'COP', amountIn: 50000, // 50,000 COP orderType: 'PAYIN', executable: true }, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } } ); return response.data; } // Usage const quote = await getQuote(token, 'your_org_id', 'your_merchant_id'); console.log('Quote:', quote); ``` {/* Python */} ```python def get_quote(token, org_id, merchant_id): response = requests.post( f'https://api-sandbox.koywe.com/api/v1/organizations/{org_id}/merchants/{merchant_id}/quotes', json={ 'originCurrencySymbol': 'COP', 'destinationCurrencySymbol': 'COP', 'amountIn': 50000, # 50,000 COP 'orderType': 'PAYIN', 'executable': True }, headers={ 'Authorization': f'Bearer {token}', 'Content-Type': 'application/json' } ) return response.json() # Usage quote = get_quote(token, 'your_org_id', 'your_merchant_id') print(f"Quote: {quote}") ``` {/* cURL */} ```bash curl -X POST 'https://api-sandbox.koywe.com/api/v1/organizations/YOUR_ORG_ID/merchants/YOUR_MERCHANT_ID/quotes' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "originCurrencySymbol": "COP", "destinationCurrencySymbol": "COP", "amountIn": 50000, "orderType": "PAYIN", "executable": true }' ``` --- ## Step 4: Create Your First PAYIN Order Now create the payment order. This generates a payment URL where your customer can complete the payment: {/* Multi-language code examples */} {/* Node.js */} ```javascript async function createPayinOrder(token, orgId, merchantId, contactId) { const response = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/orders`, { type: 'PAYIN', // Order type: receiving payment originCurrencySymbol: 'COP', // Currency: Colombian Pesos destinationCurrencySymbol: 'COP', // Same currency (no conversion) amountIn: 50000, // Amount: 50,000 COP description: 'Payment for Order #12345', // Description for customer externalId: `order-${Date.now()}`, // Your internal reference contactId: contactId, // Customer who is paying paymentMethods: [ { method: 'PSE', // Payment method: PSE (Colombian bank transfer) extra: { bankAccount: { name: 'BANCOLOMBIA' } } // Per-method extras } ], successUrl: 'https://yoursite.com/payment/success', // Redirect after success failedUrl: 'https://yoursite.com/payment/failed' // Redirect after failure }, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } } ); return response.data; } // Usage const order = await createPayinOrder(token, 'your_org_id', 'your_merchant_id', contact.id); console.log('Order created:', order.id); console.log('Payment URL:', order.providedAction); ``` {/* Python */} ```python def create_payin_order(token, org_id, merchant_id, contact_id): import time response = requests.post( f'https://api-sandbox.koywe.com/api/v1/organizations/{org_id}/merchants/{merchant_id}/orders', json={ 'type': 'PAYIN', # Order type: receiving payment 'originCurrencySymbol': 'COP', # Currency: Colombian Pesos 'destinationCurrencySymbol': 'COP', # Same currency (no conversion) 'amountIn': 50000, # Amount: 50,000 COP 'description': 'Payment for Order #12345', # Description for customer 'externalId': f'order-{int(time.time())}', # Your internal reference 'contactId': contact_id, # Customer who is paying 'paymentMethods': [ { 'method': 'PSE', # Payment method: PSE 'extra': 'BANCOLOMBIA' # Specific bank } ], 'successUrl': 'https://yoursite.com/payment/success', 'failedUrl': 'https://yoursite.com/payment/failed' }, headers={ 'Authorization': f'Bearer {token}', 'Content-Type': 'application/json' } ) return response.json() # Usage order = create_payin_order(token, 'your_org_id', 'your_merchant_id', contact['id']) print(f"Order created: {order['id']}") print(f"Payment URL: {order['providedAction']}") ``` {/* cURL */} ```bash curl -X POST 'https://api-sandbox.koywe.com/api/v1/organizations/YOUR_ORG_ID/merchants/YOUR_MERCHANT_ID/orders' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "type": "PAYIN", "originCurrencySymbol": "COP", "destinationCurrencySymbol": "COP", "amountIn": 50000, "description": "Payment for Order #12345", "externalId": "order-1699999999", "contactId": "CONTACT_ID_FROM_STEP_2", "paymentMethods": [ { "method": "PSE", "extra": { "bankAccount": { "name": "BANCOLOMBIA" } } } ], "successUrl": "https://yoursite.com/payment/success", "failedUrl": "https://yoursite.com/payment/failed" }' ``` **Response:** ```json { "id": "ord_abc123xyz", "type": "PAYIN", "status": "PENDING", "amountIn": 50000, "originCurrencySymbol": "COP", "destinationCurrencySymbol": "COP", "providedAction": "https://checkout.koywe.com/pay/ord_abc123xyz", "externalId": "order-1699999999", "description": "Payment for Order #12345", "createdAt": "2025-11-13T10:00:00Z" } ``` **Success!** You've created your first payment order. The `providedAction` is where you should redirect your customer to complete the payment. --- ## Step 5: Track the Order Status You can check the order status at any time: {/* Multi-language code examples */} {/* Node.js */} ```javascript async function getOrderStatus(token, orgId, merchantId, orderId) { const response = await axios.get( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/orders/${orderId}`, { headers: { 'Authorization': `Bearer ${token}` } } ); return response.data; } // Usage const orderStatus = await getOrderStatus(token, 'your_org_id', 'your_merchant_id', order.id); console.log('Order status:', orderStatus.status); // Possible statuses: PENDING, PROCESSING, PAID, COMPLETED, FAILED, CANCELLED, EXPIRED ``` {/* Python */} ```python def get_order_status(token, org_id, merchant_id, order_id): response = requests.get( f'https://api-sandbox.koywe.com/api/v1/organizations/{org_id}/merchants/{merchant_id}/orders/{order_id}', headers={'Authorization': f'Bearer {token}'} ) return response.json() # Usage order_status = get_order_status(token, 'your_org_id', 'your_merchant_id', order['id']) print(f"Order status: {order_status['status']}") # Possible statuses: PENDING, PROCESSING, PAID, COMPLETED, FAILED, CANCELLED, EXPIRED ``` {/* cURL */} ```bash curl -X GET 'https://api-sandbox.koywe.com/api/v1/organizations/YOUR_ORG_ID/merchants/YOUR_MERCHANT_ID/orders/ORDER_ID' \ -H 'Authorization: Bearer YOUR_TOKEN' ``` **Order Status Flow:** ``` PENDING β†’ PROCESSING β†’ PAID β†’ COMPLETED ``` - **PENDING**: Order created, waiting for customer payment - **PROCESSING**: Payment is being processed - **PAID**: Payment confirmed - **COMPLETED**: Funds credited to your virtual balance --- ## Complete End-to-End Example Here's a complete working example that puts it all together: {/* Multi-language code examples */} {/* Node.js */} ```javascript const axios = require('axios'); const BASE_URL = 'https://api-sandbox.koywe.com/api/v1'; const ORG_ID = process.env.KOYWE_ORG_ID; const MERCHANT_ID = process.env.KOYWE_MERCHANT_ID; const API_KEY = process.env.KOYWE_API_KEY; const SECRET = process.env.KOYWE_SECRET; async function main() { try { // 1. Authenticate console.log('1. Authenticating...'); const authResponse = await axios.post(`${BASE_URL}/auth/sign-in`, { apiKey: API_KEY, secret: SECRET }); const token = authResponse.data.token; console.log('βœ“ Authenticated'); // 2. Create contact console.log('\n2. Creating contact...'); const contactResponse = await axios.post( `${BASE_URL}/organizations/${ORG_ID}/merchants/${MERCHANT_ID}/contacts`, { email: 'customer@example.com', phone: '+573001234567', firstName: 'Juan', lastName: 'PΓ©rez', countrySymbol: 'CO', documentType: 'CC', documentNumber: '1234567890', businessType: 'PERSON', type: 'PERSON' }, { headers: { Authorization: `Bearer ${token}` } } ); const contactId = contactResponse.data.id; console.log('βœ“ Contact created:', contactId); // 3. Create PAYIN order console.log('\n3. Creating payment order...'); const orderResponse = await axios.post( `${BASE_URL}/organizations/${ORG_ID}/merchants/${MERCHANT_ID}/orders`, { type: 'PAYIN', originCurrencySymbol: 'COP', destinationCurrencySymbol: 'COP', amountIn: 50000, description: 'Payment for Order #12345', externalId: `order-${Date.now()}`, contactId: contactId, paymentMethods: [{ method: 'PSE', extra: { bankAccount: { name: 'BANCOLOMBIA' } } }], successUrl: 'https://yoursite.com/success', failedUrl: 'https://yoursite.com/failed' }, { headers: { Authorization: `Bearer ${token}` } } ); const order = orderResponse.data; console.log('βœ“ Order created:', order.id); console.log('βœ“ Payment URL:', order.providedAction); // 4. Check order status console.log('\n4. Checking order status...'); const statusResponse = await axios.get( `${BASE_URL}/organizations/${ORG_ID}/merchants/${MERCHANT_ID}/orders/${order.id}`, { headers: { Authorization: `Bearer ${token}` } } ); console.log('βœ“ Current status:', statusResponse.data.status); console.log('\nβœ… Success! Payment order created.'); console.log('πŸ‘‰ Redirect your customer to:', order.providedAction); } catch (error) { console.error('❌ Error:', error.response?.data || error.message); } } main(); ``` {/* Python */} ```python BASE_URL = 'https://api-sandbox.koywe.com/api/v1' ORG_ID = os.environ['KOYWE_ORG_ID'] MERCHANT_ID = os.environ['KOYWE_MERCHANT_ID'] API_KEY = os.environ['KOYWE_API_KEY'] SECRET = os.environ['KOYWE_SECRET'] def main(): try: # 1. Authenticate print('1. Authenticating...') auth_response = requests.post( f'{BASE_URL}/auth/sign-in', json={'apiKey': API_KEY, 'secret': SECRET} ) auth_response.raise_for_status() token = auth_response.json()['token'] print('βœ“ Authenticated') headers = {'Authorization': f'Bearer {token}', 'Content-Type': 'application/json'} # 2. Create contact print('\n2. Creating contact...') contact_response = requests.post( f'{BASE_URL}/organizations/{ORG_ID}/merchants/{MERCHANT_ID}/contacts', json={ 'email': 'customer@example.com', 'phone': '+573001234567', 'firstName': 'Juan', 'lastName': 'PΓ©rez', 'countrySymbol': 'CO', 'documentType': 'CC', 'documentNumber': '1234567890', 'businessType': 'PERSON', 'type': 'PERSON' }, headers=headers ) contact_response.raise_for_status() contact_id = contact_response.json()['id'] print(f'βœ“ Contact created: {contact_id}') # 3. Create PAYIN order print('\n3. Creating payment order...') order_response = requests.post( f'{BASE_URL}/organizations/{ORG_ID}/merchants/{MERCHANT_ID}/orders', json={ 'type': 'PAYIN', 'originCurrencySymbol': 'COP', 'destinationCurrencySymbol': 'COP', 'amountIn': 50000, 'description': 'Payment for Order #12345', 'externalId': f'order-{int(time.time())}', 'contactId': contact_id, 'paymentMethods': [{'method': 'PSE', 'extra': 'BANCOLOMBIA'}], 'successUrl': 'https://yoursite.com/success', 'failedUrl': 'https://yoursite.com/failed' }, headers=headers ) order_response.raise_for_status() order = order_response.json() print(f'βœ“ Order created: {order["id"]}') print(f'βœ“ Payment URL: {order["providedAction"]}') # 4. Check order status print('\n4. Checking order status...') status_response = requests.get( f'{BASE_URL}/organizations/{ORG_ID}/merchants/{MERCHANT_ID}/orders/{order["id"]}', headers=headers ) status_response.raise_for_status() print(f'βœ“ Current status: {status_response.json()["status"]}') print('\nβœ… Success! Payment order created.') print(f'πŸ‘‰ Redirect your customer to: {order["providedAction"]}') except requests.exceptions.HTTPError as error: print(f'❌ Error: {error.response.json()}') except Exception as error: print(f'❌ Error: {str(error)}') if __name__ == '__main__': main() ``` --- ## What's Next? Congratulations! You've successfully created your first payment order. Here's what to explore next: Understand organizations, merchants, virtual accounts, and order types Full production-ready integration with webhooks and error handling Test different payment scenarios in the sandbox environment Get real-time notifications when payments complete ## Need Help? - πŸ“§ Email: soporte@koywe.com - πŸ“š [API Reference](/api-reference) - πŸ› [Troubleshooting Guide](/en/accepting-payments/troubleshooting) --- # 🏁 Getting Started _Complete integration guide for dashboard users and API developers_ Source: https://docs.koywe.com/en/getting-started/setup-guide # Getting Started with Koywe Payments API This guide provides a comprehensive walkthrough for integrating with the Koywe Payments API. The integration process follows a two-fold approach to accommodate both business users and developers. ## Overview The Koywe integration process involves two main phases: 1. **Dashboard/Frontend Setup** - Business users receive organization access and manage merchants through our web interface 2. **API Integration** - Developers implement programmatic access using provided credentials and recommended endpoints ## Integration Flow B[User Accepts Invitation] B --> C[User Gets Organization Root Access] C --> D[Create Merchants via Dashboard] D --> E[Invite Users to Merchants] E --> F[API Team Receives Credentials] F --> G[Implement API Integration] G --> H[Begin Payment Processing]`} /> --- # Part 1: Dashboard/Frontend Setup This section guides business users through the initial setup process using the Koywe dashboard interface. ## Prerequisites Before beginning the dashboard setup, ensure you have: - **Organization invitation email** from the Koywe team - **Access to your organization's email** to receive and accept invitations - **Basic understanding** of your organization's merchant structure needs ## Step 1: Organization Root User Setup ### Receiving and Accepting Organization Invitation The Koywe team will send an organization invitation to your main user's email address. This invitation provides root access to your entire organization within the Koywe platform. **Process:** 1. Receive invitation email from Koywe team 2. Click invitation link and complete registration/sign-in 3. Accept organization invitation to gain access 4. Access the Koywe dashboard with root permissions ### Understanding Organization Root Permissions As an organization root user, you have full administrative access including: - Merchant management (create, view, edit, delete) - User management across all merchants - Organization-wide settings configuration - API credential generation and management ## Step 2: Merchant Creation and Management ### Creating Merchants via Dashboard **Process:** 1. Navigate to merchant management section 2. Create new merchant with business details 3. Configure merchant-specific settings 4. Review automatically created default resources ### Understanding Default Resources When you create a merchant, the system automatically creates: **Virtual Accounts:** - Payin accounts for temporarily holding incoming payments - Payout accounts for managing outgoing payments - Settlement accounts for final fund processing **Default Contact:** - Contact information matching merchant details - Used for transaction reporting and compliance - Represents the merchant in reports and communications ## Step 3: User Management ### Inviting Users to Merchants **Process:** 1. Select target merchant 2. Navigate to user management 3. Send invitations with appropriate role assignments 4. Monitor invitation status and user access ### Permission Assignment **Key Concepts:** - Creating user automatically receives root permissions over new merchant - This prevents merchants without any user permissions - Organization root users maintain access to all merchants - Permissions can be modified later via dashboard or API --- # Part 2: API Integration This section provides guidance for developers implementing API integration with the Koywe Payments system. ## Prerequisites Before beginning API integration, ensure you have: - **API key and secret** provided by the Koywe team - **Organization ID** provided by the Koywe team - **Development environment** set up for API testing ## Step 1: Authentication Setup ### API Credentials Your integration will use: - API key and secret for authentication - Organization ID for scoping operations - Bearer token for API requests ### Authentication Flow **Process:** 1. Use provided API key and secret to authenticate 2. Receive bearer token for subsequent requests 3. Include token in all API requests 4. Implement token refresh logic as needed ## Step 2: Recommended API Integration Flow The Koywe team recommends following this sequence for optimal integration: ### Organization Invitation Management **Purpose:** Programmatically invite users to your organization - Send invitations to organization root users - Provide access to dashboard for frontend management - Assign appropriate organization-level roles ### Merchant Creation **Purpose:** Create merchants under your organization via API - Define merchant business details - Configure merchant settings programmatically - Automatically create default virtual accounts and contacts ### Merchant User Invitations **Purpose:** Invite users to specific merchants - Send targeted invitations for merchant-specific access - Assign merchant-level roles and permissions - Enable focused merchant management ### API Credential Generation **Purpose:** Generate API credentials for merchants - Create merchant-specific API keys - Enable merchant-scoped API operations - Support different credential types and permissions ## Step 3: Understanding Default Resource Creation ### Automatic Resource Creation When creating merchants via API, the system automatically provisions: **Virtual Accounts:** - Multiple account types for different payment flows - Multi-currency support - Automatic balance tracking and reporting **Default Contacts:** - Merchant contact information - Transaction reporting integration - Compliance and regulatory documentation ### Permission Inheritance **Key Concepts:** - API operations inherit user permissions - Organization-level access enables merchant creation - Merchant creators automatically receive merchant permissions - Permissions can be modified post-creation ## Step 4: Implementation Considerations ### Environment Setup **Development Environment:** - Use sandbox environment for testing - Separate credentials from production - Test complete integration flow **Production Environment:** - Use production API credentials - Implement proper error handling - Monitor API usage and performance ### Best Practices **Security:** - Store credentials securely - Use HTTPS for all API communications - Implement proper token management **Error Handling:** - Handle authentication failures - Manage API rate limits - Implement retry logic for transient failures **Testing:** - Test complete integration workflow - Verify resource creation and permissions - Validate error scenarios --- # Next Steps After completing this setup guide: 1. **Payment Processing**: Review specific payment creation and processing endpoints 2. **Webhook Integration**: Set up webhook handlers for real-time notifications 3. **Advanced Configuration**: Explore additional API features and customization options 4. **Production Deployment**: Implement security measures and monitoring for production use ## Additional Resources - [API Reference Documentation](/api-reference) - [Organization Setup & Invitations](/en/getting-started/organization-setup) - [Onboarding & KYB](/en/getting-started/onboarding-kyb) - [Deposit Accounts](/en/balance-management/deposit-accounts) - [Passkeys & Approvals](/en/advanced/passkeys-and-approvals) For additional support, contact the Koywe development team via Slack or to soporte@koywe.com or refer to our comprehensive API documentation. --- # πŸ§ͺ Testing in Sandbox _Complete guide to testing all payment scenarios_ Source: https://docs.koywe.com/en/getting-started/testing # 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.com ``` ### Getting 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: ```bash 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](/en/advanced/passkeys-and-approvals) for the full policy model and [Error Code Catalog](/en/advanced/error-codes) for `POL*` codes. --- ## Test Payment Methods by Country ### Colombia (COP) πŸ‡¨πŸ‡΄ #### PSE (Pagos Seguros en LΓ­nea) **Test Banks**: - `BANCOLOMBIA` - `DAVIVIENDA` - `BOGOTA` - `OCCIDENTE` **Usage**: ```javascript { "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**: ```javascript { "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**: ```javascript { "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**: ```javascript { "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**: ```javascript { "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**: ```javascript { "method": "CARD" } ``` --- ### Chile (CLP) πŸ‡¨πŸ‡± #### Khipu **Usage**: ```javascript { "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**: ```javascript { "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**: {/* Multi-language code examples */} {/* Node.js */} ```javascript 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.providedAction); // 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 β†’ COMPLETED ``` --- ### Failed Payment Scenario **Test payment failure handling**: Use the special test amount `666` to simulate a failed payment: {/* Multi-language code examples */} {/* Node.js */} ```javascript 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 β†’ FAILED ``` --- ### Expired Payment **Test order expiration**: {/* Multi-language code examples */} {/* Node.js */} ```javascript 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. {/* Multi-language code examples */} {/* Node.js */} ```javascript 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.providedAction); 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.providedAction); ``` {/* Python */} ```python 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['providedAction']}") return order # Usage link = test_payment_link() print(f"\n🌐 Open this URL to test: {link['providedAction']}") ``` **Testing the checkout flow**: 1. Create the payment link using the code above 2. Copy the `providedAction` 3. Open it in your browser 4. You'll see the Koywe-branded checkout page 5. Fill in test customer information 6. Select a payment method 7. Complete the simulated payment (choose success or fail) 8. 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: 1. **[webhook.site](https://webhook.site)** - Instant webhook URL 2. **[ngrok](https://ngrok.com)** - Tunnel to localhost 3. **[RequestBin](https://requestbin.com)** - Webhook inspector ### Using webhook.site ### Get webhook URL Visit [webhook.site](https://webhook.site) and copy your unique URL ### Create order with webhook ```javascript 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: 1. `order.created` - Order is created 2. `order.pending` - Waiting for payment 3. `order.processing` - Payment being processed 4. `order.paid` - Payment confirmed 5. `order.completed` - Funds credited ### Webhook Payload Example ```json { "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 ```javascript { "firstName": "Juan", "lastName": "PΓ©rez Test", "countrySymbol": "CO", "businessType": "PERSON", "type": "PERSON", "documentType": "CC", "documentNumber": "1234567890" } ``` #### Brazil ```javascript { "firstName": "Maria", "lastName": "Silva Test", "countrySymbol": "BR", "businessType": "PERSON", "type": "PERSON", "documentType": "CPF", "documentNumber": "12345678900" } ``` #### Mexico ```javascript { "firstName": "Pedro", "lastName": "GarcΓ­a Test", "countrySymbol": "MX", "businessType": "PERSON", "type": "PERSON", "documentType": "RFC", "documentNumber": "XAXX010101000" } ``` #### Chile ```javascript { "firstName": "Ana", "lastName": "LΓ³pez Test", "countrySymbol": "CL", "businessType": "PERSON", "type": "PERSON", "documentType": "RUT", "documentNumber": "11111111-1" } ``` --- ### Test Bank Accounts For PAYOUT testing: #### Colombia ```javascript { "name": "Bancolombia COP", "kind": "BANK", "isDefault": true, "countrySymbol": "CO", "currencySymbol": "COP", "entity": "BANCOLOMBIA", "accountNumber": "1234567890", "type": "SAVINGS" } ``` #### Brazil ```javascript { "name": "Banco do Brasil BRL", "kind": "BANK", "isDefault": true, "countrySymbol": "BR", "currencySymbol": "BRL", "entity": "BANCO_DO_BRASIL", "accountNumber": "12345678", "type": "CHECKING" } ``` #### Mexico ```javascript { "name": "BBVA Mexico MXN", "kind": "BANK", "isDefault": true, "countrySymbol": "MX", "currencySymbol": "MXN", "entity": "BBVA_MEXICO", "accountNumber": "012345678901234567", "type": "CHECKING" } ``` #### Chile ```javascript { "name": "Banco Chile CLP", "kind": "BANK", "isDefault": true, "countrySymbol": "CL", "currencySymbol": "CLP", "entity": "BANCO_CHILE", "accountNumber": "12345678", "type": "CHECKING" } ``` --- ## 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 | |-------------------|-----------------| | ETHEREUM | Sepolia | | POLYGON | Amoy | | BSC | BSC Testnet | **Example**: Specify `"network": "POLYGON"` in your request, and sandbox will use Amoy automatically. **Same currency support as production**: Which symbols each network accepts does not change in sandbox β€” see [Supported Networks](/en/crypto-operations/onramp#supported-networks). ### Test Wallet Address Use this address for receiving test crypto: ``` 0x0000000000000000000000000000000000000000 ``` **Never use production addresses in sandbox** - Always use test/burn addresses like the zero address above. ### ONRAMP Test {/* Multi-language code examples */} {/* Node.js */} ```javascript 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 {/* Multi-language code examples */} {/* Node.js */} ```javascript 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 allowed - `X-RateLimit-Remaining`: Requests remaining - `X-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 {/* Multi-language code examples */} {/* Node.js */} ```javascript 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 {/* Multi-language code examples */} {/* Node.js */} ```javascript 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, { firstName: 'Test', lastName: 'Customer', countrySymbol: 'CO', businessType: 'PERSON', type: 'PERSON', email: 'test@example.com', 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.providedAction); 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 {/* Multi-language code examples */} {/* Node.js */} ```javascript async function testPayoutFlow() { console.log('1. Create provider contact...'); const provider = await createContact(token, orgId, merchantId, { firstName: 'Test', lastName: 'Provider', countrySymbol: 'CO', businessType: 'COMPANY', type: 'BUSINESS', email: 'provider@example.com', documentType: 'NIT', documentNumber: '900123456-1' }); console.log('2. Add provider bank account...'); const bankAccount = await addBankAccountToContact(token, orgId, merchantId, provider.id, { name: 'Bancolombia COP', kind: 'BANK', isDefault: true, countrySymbol: 'CO', currencySymbol: 'COP', entity: 'BANCOLOMBIA', accountNumber: '1234567890', type: '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/simulate ``` This endpoint is **sandbox-only**. It returns an error in production. ### Request ```json { "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 ```json { "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` β€” `true` the first time a virtual account had to be created for the currency; `false` on subsequent calls. ### Example {/* Multi-language code examples */} {/* Node.js */} ```javascript 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: ```javascript // 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 --- ## Need Help? soporte@koywe.com For sandbox access issues or testing questions Common issues and solutions Complete API documentation 5-minute integration guide --- # Webhooks & Statuses _Configure webhook endpoints and inspect event delivery_ Source: https://docs.koywe.com/en/getting-started/webhooks-overview ### Webhooks overview - Webhooks notify organizations in near real time about changes to related entities (orders, contacts, merchants, invitations). - You can configure multiple endpoints per organization; all active endpoints receive all events. - Event status can be inspected via the API. ### Getting started 1. Create an endpoint with a URL and a secret. 2. Store your secret securely; it's used to sign each webhook. 3. Verify signatures on every request (see Security below). 4. Use Ping to test your integration before going live. ### Managing endpoints (Org‑scoped) - Create: POST /organizations/:organizationId/webhooks - List/get: GET /organizations/:organizationId/webhooks[/ :webhookId] - Rotate secret: POST /organizations/:organizationId/webhooks/:webhookId/rotate-secret - Pause/resume: POST /organizations/:organizationId/webhooks/:webhookId/(pause|resume) - Delete: DELETE /organizations/:organizationId/webhooks/:webhookId (soft delete) - Ping: POST /organizations/:organizationId/webhooks/:webhookId/ping Secrets are write‑only and never returned after creation/rotation. ### Events (read APIs) - List: GET /organizations/:organizationId/webhook-events - Filters: type, from, to, merchantId, resourceType, resourceId, page, limit - Get: GET /organizations/:organizationId/webhook-events/:eventId - Deliveries (lean status): GET /organizations/:organizationId/webhook-events/:eventId/deliveries - Returns per-endpoint deliveries with deliveryStatus, retryCount, lastRetryAt, lastStatusCode, lastRespondedAt Replay (Org-scoped, internal use by ops or via permissions): - Replay an event: POST /organizations/:organizationId/webhook-events/:eventId/replay - Re-enqueues to all active endpoints (or a single endpoint internally) ### Event types Grouped by resource family. See [Webhooks Deep Dive β†’ Event Types](/en/advanced/webhooks#event-types) for a description of each event and when it fires. - **Orders**: `order.created`, `order.approved`, `order.processing`, `order.paid`, `order.completed`, `order.failed`, `order.expired`, `order.canceled`, `order.refunded`, `order.updated` - **Merchants**: `merchant.created` - **KYB**: `merchant.kyb.in_progress`, `merchant.kyb.approved`, `merchant.kyb.rejected` - **Invitations** (fired for both `invitation` and `organization_invitation` resource types): `invitation.created`, `invitation.accepted`, `invitation.expired`, `invitation.failed`, `invitation.assigned` - **Policy approvals**: `policy.approval.requested`, `policy.approval.received`, `policy.approval.approved`, `policy.approval.rejected` - **Policy execution** (dynamic per resource type): `policy.order.executed`, `policy.account.executed`, `policy.deal.executed`, `policy.user_invite.executed`, `policy.passkey_enrollment.executed`, `policy.wallet_access_policy.executed`, `policy.policy.executed`, `policy.policy_rule.executed` - **Bank income**: `bank_income.received` - **System**: `webhook.ping` Subject to expansion; breaking changes roll out via the payload `version` field. ### Payload and headers - Content type: application/json - Body skeleton ```json { "id": "evt_...", "type": "order.paid", "version": "v1", "occurred_at": "2025-08-16T21:52:14.292Z", "source": "koywe.api", "environment": "production", "organization_id": "org_...", "merchant_id": "mrc_...", "data": { /* type-specific */ }, "relationships": { "self": { "type": "order", "id": "ord_..." } } } ``` - Headers (set by Koywe): - Koywe-Event-Id, Koywe-Event-Type, Koywe-Event-Version - Koywe-Organization-Id, Koywe-Merchant-Id (if applicable) - Koywe-Environment - Koywe-Webhook-Id - Koywe-Signature: HMAC-SHA256 (hex) over the raw request body using your endpoint secret - Koywe-Integrity-Signature: added by the delivery service to protect in-flight integrity Environment is one of: local | qa | sandbox | production. ### Security (signature verification) - Compute HMAC-SHA256 of the exact request body using your saved endpoint secret. - Compare your digest (hex) with Koywe-Signature. If they don't match, reject the webhook. - Replay-safe: use idempotency by event id (id is stable across replays). Minimal pseudo-verification: ```text expected = hex(hmac_sha256(secret, raw_body)) if header["Koywe-Signature"] != expected: reject ``` ### Delivery behavior and retries - On event creation, Koywe persists the event and enqueues a delivery per active endpoint. - Each delivery receives a unique task id; you can fetch lean status via the deliveries endpoint. - If an endpoint fails, other endpoints still receive their deliveries. - You can request a replay to resend an event. **Retry rules**: - Request timeout is 30 seconds. - `5xx`, `429`, and network errors / timeouts are **retried** with backoff. After the retry budget is exhausted, the delivery is marked `FAILED`. - Any other `4xx` (`400`, `401`, `403`, `404`, `422`, …) is **terminal**: the delivery is marked `FAILED` after one attempt with no retry. Fix the endpoint and use the replay endpoint to resend. - Return `5xx` if you want automatic retries during transient downstream issues. Return `429` to signal throttling β€” it is treated like other transient errors (retried with backoff) and can still be marked `FAILED` once the retry budget is exhausted. ### Best practices - Respond 2xx quickly (< 5s); process asynchronously on your side. - Validate Koywe-Signature on every request. - Treat webhooks as at-least-once delivery; deduplicate using event id. - Use the ping endpoint to verify connectivity after configuration or secret rotation. ### Permissions (org-scoped) - organization_webhooks.manage: create/update/pause/resume/delete/rotate - organization_webhooks.view: list/get endpoints - organization_webhook_events.view: list/get events, view deliveries - organization_webhook_events.replay: replay events This summary complements the OpenAPI docs with usage guidance, security expectations, and event taxonomy. --- # Core Concepts Overview _Understanding the Koywe Payments architecture_ Source: https://docs.koywe.com/en/core-concepts # Core Concepts Before integrating with Koywe Payments, it's important to understand the key concepts that form the foundation of our system. ## The Hierarchy Koywe Payments is built on a hierarchical structure that allows you to manage multiple business units and currencies efficiently: B[Merchant 1] A --> C[Merchant 2] A --> D[Merchant 3] B --> E[Virtual Account COP] B --> F[Virtual Account USD] B --> G[Virtual Account BRL] E --> H[Orders] F --> H G --> H style A fill:#003C2A,stroke:#C9FF1F,stroke-width:2px,color:#fff style B fill:#005544,stroke:#C9FF1F,stroke-width:2px,color:#fff style C fill:#005544,stroke:#C9FF1F,stroke-width:2px,color:#fff style D fill:#005544,stroke:#C9FF1F,stroke-width:2px,color:#fff`} /> This structure allows you to: - Manage multiple business units (merchants) under one organization - Maintain separate virtual accounts for each currency - Track all transactions and orders per merchant - Assign different permissions and access levels --- ## Key Entities ### Organizations Your **Organization** is the top-level entity that represents your company in the Koywe system. - One organization can contain multiple merchants - Manages company-wide settings and users - Controls API credentials and permissions - Handles billing and reporting at the organization level [Learn more about Organizations β†’](/en/core-concepts/organizations-and-merchants) ### Merchants A **Merchant** represents an individual business unit or brand within your organization. - Each merchant has its own virtual accounts - Separate transaction tracking per merchant - Independent payment processing - Useful for multi-brand companies or franchises [Learn more about Merchants β†’](/en/core-concepts/organizations-and-merchants) ### Virtual Accounts **Virtual Accounts** are multi-currency balance accounts that hold your funds within the Koywe system. - One virtual account per currency per merchant - Automatically created when you create a merchant - Used for PAYIN, PAYOUT, and transfer operations - Real-time balance tracking [Learn more about Virtual Accounts β†’](/en/core-concepts/virtual-accounts) ### Contacts **Contacts** represent your customers or providers in the system. - Store customer/provider information - Link bank accounts for payouts - Track payment history - Manage KYC/compliance data [Learn more about Contacts β†’](/en/core-concepts/contacts-and-bank-accounts) ### Orders **Orders** are the core transaction entities in Koywe Payments. - Six order types: PAYIN, PAYOUT, BALANCE_TRANSFER, ONRAMP, OFFRAMP, PAYMENT_LINK - Track payment status through lifecycle - Link to contacts and virtual accounts - Generate webhooks for status changes [Learn more about Orders β†’](/en/core-concepts/orders-and-order-types) ### Quotes **Quotes** provide exchange rate locks for currency conversions. - Get real-time exchange rates - Lock rates for a specific time period - Use in orders for transparent pricing - Optional but recommended for customer transparency [Learn more about Quotes β†’](/en/core-concepts/quotes-and-exchange-rates) --- ## How They Work Together ### Example: Accepting a Payment >API: Create PAYIN Order Note over API: Links to MerchantUses Payment MethodCreates Contact API-->>App: Returns Order + Payment URL App->>Cust: Redirect to Payment URL Cust->>API: Completes Payment API->>VA: Credits COP Virtual Account API->>App: Webhook: order.paid API->>App: Webhook: order.completed`} /> **The flow:** 1. Your application creates an order for a specific **Merchant** 2. The order links to a **Contact** (the customer) 3. Customer pays using a **Payment Method** 4. Funds are credited to the merchant's **Virtual Account** 5. You receive webhook notifications about the **Order** status changes ### Example: Paying a Provider >API: Check Virtual Account Balance API-->>App: Balance: 1,000,000 COP App->>API: Create PAYOUT Order Note over API: Validates sufficient balanceLinks to ContactGets bank account info API->>VA: Debits COP Virtual Account API->>Bank: Transfers to Provider Bank-->>API: Confirms Transfer API->>App: Webhook: order.completed`} /> **The flow:** 1. Check your **Virtual Account** balance 2. Create a PAYOUT order for a **Contact** (provider) 3. Funds are debited from your **Virtual Account** 4. Transferred to the provider's bank account 5. You receive webhook confirmation --- ## Typical Integration Flow When integrating Koywe Payments, you'll typically follow these steps: ### Authenticate Use your API key and secret to obtain an access token. ### Create or Retrieve a Contact Store customer or provider information for tracking and compliance. ### Get a Quote (Optional) If currency conversion is involved, get an exchange rate quote. ### Create an Order Specify the order type (PAYIN, PAYOUT, etc.) and required details. ### Handle the Order - For PAYIN: Redirect customer to payment URL - For PAYOUT: Wait for confirmation - For transfers: Check new balances ### Monitor via Webhooks Receive real-time notifications about order status changes. --- ## Understanding Order Types Each order type serves a specific purpose in the payment flow: | Order Type | Direction | Use Case | Example | |------------|-----------|----------|---------| | **PAYIN** | External β†’ Virtual Account | Accept customer payments | Customer pays for a product | | **PAYOUT** | Virtual Account β†’ External | Pay providers/contractors | Pay a vendor's invoice | | **BALANCE_TRANSFER** | Virtual Account β†’ Virtual Account | Currency exchange | Convert COP to USD | | **ONRAMP** | Fiat β†’ Crypto | Buy cryptocurrency | Buy USDC with COP | | **OFFRAMP** | Crypto β†’ Fiat | Sell cryptocurrency | Sell BTC for COP | | **PAYMENT_LINK** | External β†’ Virtual Account | Share payment link | Invoice payment link | [Detailed guide for each order type β†’](/en/core-concepts/orders-and-order-types) --- ## Virtual Account Balance Flow Understanding how money moves through virtual accounts: Customer Payment] -->|Credits| B[Virtual Account COP] B -->|Transfer| C[Virtual Account USD] B -->|PAYOUT| D[Provider Bank] B -->|ONRAMP| E[Crypto Wallet] E -->|OFFRAMP| B style B fill:#005544,stroke:#C9FF1F,stroke-width:3px,color:#fff`} /> **Virtual accounts act as the central hub** for all your payment operations: - Receive funds from customer payments (PAYIN) - Hold balances in multiple currencies - Source for provider payments (PAYOUT) - Source for crypto purchases (ONRAMP) - Destination for crypto sales (OFFRAMP) - Enable instant currency transfers --- ## Data Relationships Understanding how entities relate to each other: --- ## Next Steps Now that you understand the core concepts, dive deeper into each topic: See where Koywe settles and over which rail Learn how to structure your business units Manage multi-currency balances Store customer and provider information Understand the six order types Get and use exchange rate quotes Try the 5-minute quickstart --- # Contacts & Bank Accounts _Managing customer and provider information_ Source: https://docs.koywe.com/en/core-concepts/contacts-and-bank-accounts # Contacts & Bank Accounts Contacts represent your customers or providers in the Koywe system, storing their information and linked bank accounts for payment operations. ## What are Contacts? A **Contact** is a person or business entity that you interact with through payments: - **Customers** who pay you (PAYIN) - **Providers** who you pay (PAYOUT) - **Both** in marketplace scenarios ### Why Use Contacts? Track payment history per customer or provider Store KYC/compliance information (documents, tax IDs) Link bank accounts for easy recurring payments Generate reports per contact or business type --- ## Contact Structure ### Required Fields ```javascript { "firstName": "Juan", // First name "countrySymbol": "CO", // Country code "businessType": "PERSON", // PERSON, COMPANY, etc. "type": "PERSON" // PERSON, BUSINESS, GOVERNMENT, NGO, FOREIGN } ``` ### Optional Fields ```javascript { "lastName": "PΓ©rez", // Last name "email": "customer@example.com", // Email address "phone": "+573001234567", // Phone number "documentType": "CC", // ID document type "documentNumber": "1234567890", // ID document number (required if documentType is provided) "taxIdType": "NIT", // Tax ID type (deprecated, use documentType) "taxIdNumber": "900123456", // Tax ID number (deprecated, use documentNumber) "address": { "street": "Calle 123", "city": "BogotΓ‘", "state": "Cundinamarca", "zipCode": "110111", "country": "CO" } } ``` --- ## Creating Contacts ### Basic Contact Creation {/* Multi-language code examples */} {/* Node.js */} ```javascript async function createContact(token, orgId, merchantId, contactData) { const response = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/contacts`, { firstName: contactData.firstName, lastName: contactData.lastName, // Optional email: contactData.email, // Optional phone: contactData.phone, // Optional countrySymbol: contactData.countrySymbol, documentType: contactData.documentType, // Optional documentNumber: contactData.documentNumber, // Optional (required if documentType is provided) businessType: 'PERSON', // or 'COMPANY' type: 'PERSON' // PERSON, BUSINESS, GOVERNMENT, NGO, FOREIGN }, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } } ); return response.data; } // Usage - Creating a customer contact const customer = await createContact(token, orgId, merchantId, { firstName: 'Juan', lastName: 'PΓ©rez', countrySymbol: 'CO', email: 'customer@example.com', phone: '+573001234567', documentType: 'CC', documentNumber: '1234567890' }); console.log('Contact created:', customer.id); ``` {/* Python */} ```python def create_contact(token, org_id, merchant_id, contact_data): response = requests.post( f'https://api-sandbox.koywe.com/api/v1/organizations/{org_id}/merchants/{merchant_id}/contacts', json={ 'firstName': contact_data['first_name'], 'lastName': contact_data.get('last_name'), # Optional 'email': contact_data.get('email'), # Optional 'phone': contact_data.get('phone'), # Optional 'countrySymbol': contact_data['country'], 'documentType': contact_data.get('document_type'), # Optional 'documentNumber': contact_data.get('document_number'),# Optional (required if documentType is provided) 'businessType': 'PERSON', 'type': 'PERSON' }, headers={ 'Authorization': f'Bearer {token}', 'Content-Type': 'application/json' } ) return response.json() # Usage customer = create_contact(token, org_id, merchant_id, { 'first_name': 'Juan', 'last_name': 'PΓ©rez', 'email': 'customer@example.com', 'phone': '+573001234567', 'country': 'CO', 'document_type': 'CC', 'document_number': '1234567890' }) ``` {/* cURL */} ```bash curl -X POST 'https://api-sandbox.koywe.com/api/v1/organizations/YOUR_ORG_ID/merchants/YOUR_MERCHANT_ID/contacts' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "firstName": "Juan", "lastName": "PΓ©rez", "email": "customer@example.com", "phone": "+573001234567", "countrySymbol": "CO", "documentType": "CC", "documentNumber": "1234567890", "businessType": "PERSON", "type": "PERSON" }' ``` **Response:** ```json { "id": "cnt_abc123", "firstName": "Juan", "lastName": "PΓ©rez", "email": "customer@example.com", "phone": "+573001234567", "countrySymbol": "CO", "documentType": "CC", "documentNumber": "1234567890", "businessType": "PERSON", "createdAt": "2025-11-13T10:00:00Z", "merchantId": "mrc_xyz789" } ``` --- ## Document Types by Country ### Colombia (CO) | Document Type | Code | Description | Example | |---------------|------|-------------|---------| | CΓ©dula de CiudadanΓ­a | CC | National ID | 1234567890 | | CΓ©dula de ExtranjerΓ­a | CE | Foreign ID | 1234567 | | NIT | NIT | Tax ID (companies) | 900123456-1 | ### Brazil (BR) | Document Type | Code | Description | Example | |---------------|------|-------------|---------| | CPF | CPF | Individual Tax ID | 123.456.789-00 | | CNPJ | CNPJ | Company Tax ID | 12.345.678/0001-90 | ### Mexico (MX) | Document Type | Code | Description | Example | |---------------|------|-------------|---------| | RFC | RFC | Tax ID | XAXX010101000 | | CURP | CURP | Population Registry | ABCD123456HDFXXX09 | ### Chile (CL) | Document Type | Code | Description | Example | |---------------|------|-------------|---------| | RUT | RUT | Tax ID | 11.111.111-1 | ### Argentina (AR) | Document Type | Code | Description | Example | |---------------|------|-------------|---------| | DNI | DNI | National ID | 12345678 | | CUIT | CUIT | Tax ID | 20-12345678-9 | ### Peru (PE) | Document Type | Code | Description | Example | |---------------|------|-------------|---------| | Documento Nacional de Identidad | DNI | National ID | 12345678 | | Registro Único de Contribuyentes | RUC | Tax ID (companies) | 20123456789 | | CarnΓ© de ExtranjerΓ­a | CED_EXT | Foreign Resident ID | 001234567 | ### Bolivia (BO) | Document Type | Code | Description | Example | |---------------|------|-------------|---------| | CΓ©dula de Identidad | CED_CIU | National ID | 1234567 | | NΓΊmero de IdentificaciΓ³n Tributaria | NIT | Tax ID (companies) | 1234567890 | ### Venezuela (VE) | Document Type | Code | Description | Example | |---------------|------|-------------|---------| | CΓ©dula de Identidad | CED_CIU | National ID (V/E prefix) | V12345678 | | NΓΊmero de IdentificaciΓ³n Tributaria | NIT | Tax ID (companies, J/G prefix) | J123456789 | ### United States (US) | Document Type | Code | Description | Example | |---------------|------|-------------|---------| | Employer Identification Number | EIN | Employer Tax ID | 12-3456789 | --- ## Bank Accounts ### Linking Bank Accounts to Contacts For **PAYOUT** operations, you need to link a bank account to the contact: **Automatic Association**: Bank accounts are automatically linked to the contact's information. The holder name is taken from the contact's `firstName` and `lastName` (when provided). Bank codes can be deduced from account numbers for most countries. ### Optional Fields with Defaults | Field | Type | Default | Description | |-------|------|---------|-------------| | `type` | string | `VIRTUAL` | Account type: `SAVINGS`, `CHECKING`, or `VIRTUAL`. Required for Colombia. Optional for most countries. | | `isVirtual` | boolean | `false` | Whether this is a virtual account. | | `isTracked` | boolean | `false` | Whether this account is tracked for balance monitoring. | ### Country-Specific Required Fields | Country | `entity` (bankCode) | Notes | |---------|---------------------|-------| | Colombia (CO) | **Required** | Must provide bank code | | Chile (CL) | **Required** | Must provide bank code | | Peru (PE) | Optional | Auto-deduced from account number (CCI) | | Argentina (AR) | Optional | Auto-deduced from CVU/CBU | | Mexico (MX) | Optional | Auto-deduced from CLABE | | Brazil (BR) | Optional | Auto-deduced | {/* Multi-language code examples */} {/* Node.js */} ```javascript async function addBankAccountToContact(token, orgId, merchantId, contactId, bankData) { const response = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/contacts/${contactId}/accounts`, { name: bankData.name, kind: 'BANK', isDefault: bankData.isDefault, countrySymbol: bankData.country, currencySymbol: bankData.currency, entity: bankData.entity, // Required for CO, CL. Optional for others (auto-deduced) accountNumber: bankData.accountNumber, type: bankData.type // Required for Colombia: 'SAVINGS' or 'CHECKING' }, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } } ); return response.data; } // Usage - Colombia (entity required) const bankAccountCO = await addBankAccountToContact(token, orgId, merchantId, 'cnt_abc123', { name: 'Bancolombia COP', isDefault: true, country: 'CO', currency: 'COP', entity: 'BANCOLOMBIA', // Required for Colombia and Chile accountNumber: '1234567890', type: 'SAVINGS' // Required for Colombia }); // Usage - Peru (entity auto-deduced from CCI) const bankAccountPE = await addBankAccountToContact(token, orgId, merchantId, 'cnt_def456', { name: 'Peru PEN', isDefault: false, country: 'PE', currency: 'PEN', accountNumber: '00219100012345678901' // 20-digit CCI - bank code auto-deduced }); console.log('Bank account added:', bankAccountCO.id); ``` {/* Python */} ```python def add_bank_account_to_contact(token, org_id, merchant_id, contact_id, bank_data): payload = { 'name': bank_data['name'], 'kind': 'BANK', 'isDefault': bank_data['is_default'], 'countrySymbol': bank_data['country'], 'currencySymbol': bank_data['currency'], 'accountNumber': bank_data['account_number'] } # entity required for CO, CL; optional for others if bank_data.get('entity'): payload['entity'] = bank_data['entity'] # type required for Colombia; optional for other countries (defaults to VIRTUAL) if bank_data.get('type'): payload['type'] = bank_data['type'] response = requests.post( f'https://api-sandbox.koywe.com/api/v1/organizations/{org_id}/merchants/{merchant_id}/contacts/{contact_id}/accounts', json=payload, headers={ 'Authorization': f'Bearer {token}', 'Content-Type': 'application/json' } ) return response.json() # Usage - Colombia (entity required) bank_account_co = add_bank_account_to_contact(token, org_id, merchant_id, 'cnt_abc123', { 'country': 'CO', 'currency': 'COP', 'entity': 'BANCOLOMBIA', # Required for Colombia and Chile 'account_number': '1234567890', 'type': 'SAVINGS' # Required for Colombia }) # Usage - Chile (entity required) bank_account_cl = add_bank_account_to_contact(token, org_id, merchant_id, 'cnt_ghi789', { 'country': 'CL', 'currency': 'CLP', 'entity': 'BANCO_ESTADO', # Required for Chile 'account_number': '12345678901' }) ``` {/* cURL */} ```bash # Colombia - entity required curl -X POST 'https://api-sandbox.koywe.com/api/v1/organizations/YOUR_ORG_ID/merchants/YOUR_MERCHANT_ID/contacts/cnt_abc123/accounts' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "name": "Bancolombia COP", "kind": "BANK", "isDefault": true, "countrySymbol": "CO", "currencySymbol": "COP", "entity": "BANCOLOMBIA", "accountNumber": "1234567890", "type": "SAVINGS" }' ``` **Country-Specific Requirements**: See [Merchant External Accounts](/en/core-concepts/merchant-external-accounts) for detailed validation rules by country. Contact bank accounts follow the same validations (CLABE for Mexico, CCI for Peru, CVU/CBU for Argentina, etc.). --- ## Retrieving Contacts ### List All Contacts {/* Multi-language code examples */} {/* Node.js */} ```javascript async function listContacts(token, orgId, merchantId, options = {}) { const response = await axios.get( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/contacts`, { params: { search: options.search, // Optional: search by name or email limit: options.limit || 50, page: options.page || 1 }, headers: { 'Authorization': `Bearer ${token}` } } ); return response.data; } // Usage const contacts = await listContacts(token, orgId, merchantId, { search: 'juan', limit: 20 }); console.log(`Found ${contacts.length} contacts`); ``` ### Get Specific Contact {/* Multi-language code examples */} {/* Node.js */} ```javascript async function getContact(token, orgId, merchantId, contactId) { const response = await axios.get( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/contacts/${contactId}`, { headers: { 'Authorization': `Bearer ${token}` } } ); return response.data; } // Usage const contact = await getContact(token, orgId, merchantId, 'cnt_abc123'); console.log('Contact:', contact); ``` ### Get Contact's Bank Accounts {/* Multi-language code examples */} {/* Node.js */} ```javascript async function getContactBankAccounts(token, orgId, merchantId, contactId) { const response = await axios.get( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/contacts/${contactId}/accounts`, { headers: { 'Authorization': `Bearer ${token}` } } ); return response.data; } // Usage const bankAccounts = await getContactBankAccounts(token, orgId, merchantId, 'cnt_abc123'); console.log('Bank accounts:', bankAccounts); ``` --- ## Updating Contacts {/* Multi-language code examples */} {/* Node.js */} ```javascript async function updateContact(token, orgId, merchantId, contactId, updates) { const response = await axios.put( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/contacts/${contactId}`, updates, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } } ); return response.data; } // Usage - Update phone number or address const updated = await updateContact(token, orgId, merchantId, 'cnt_abc123', { phone: '+573009876543', address: { street: 'Calle 456', city: 'MedellΓ­n' } }); ``` --- ## Validation Requirements ### Person vs Company **For PERSON (individual)**: - Use individual document types (CC, DNI, CPF, etc.) - `businessType`: `"PERSON"` - Individual's `firstName` (and optionally `lastName`) **For COMPANY (business)**: - Use company document types (NIT, CNPJ, CUIT, etc.) - `businessType`: `"COMPANY"` - `type`: `"BUSINESS"` - Company legal name as `firstName` ### Document Number Formats Document numbers must match the expected format for each country: - **Colombia CC**: 6-10 digits - **Brazil CPF**: 11 digits (with or without formatting) - **Mexico RFC**: 12-13 characters - **Chile RUT**: Format XX.XXX.XXX-X --- ## Best Practices ### When to Create Contacts **Create contacts for**: - Customers making payments (PAYIN) - for tracking and compliance - Providers receiving payments (PAYOUT) - required for bank account linking - Recurring transactions with the same entity ### Reusing Contacts **Reuse existing contacts** for the same customer/provider to: - Maintain payment history - Avoid duplicate records - Simplify reconciliation {/* Multi-language code examples */} {/* Node.js */} ```javascript // Check if contact exists before creating async function getOrCreateContact(token, orgId, merchantId, email) { // Try to find existing const contacts = await listContacts(token, orgId, merchantId, { search: email }); if (contacts.length > 0) { return contacts[0]; // Return existing } // Create new if not found return await createContact(token, orgId, merchantId, { email: email, // ... other fields }); } ``` ### Data Privacy **Security Considerations**: - Store only necessary information - Follow GDPR/data protection regulations - Don't store sensitive data in metadata fields - Implement proper access controls --- ## Common Scenarios ### Scenario 1: E-Commerce Customer Payment ```javascript // 1. Create customer contact const customer = await createContact(token, orgId, merchantId, { firstName: 'MarΓ­a', lastName: 'GarcΓ­a', countrySymbol: 'CO', businessType: 'PERSON', type: 'PERSON', email: 'customer@example.com', documentType: 'CC', documentNumber: '1234567890', phone: '+573001234567' }); // 2. Create PAYIN order linked to contact const order = await createPayinOrder(token, orgId, merchantId, { contactId: customer.id, amount: 50000, currency: 'COP', // ... other fields }); ``` ### Scenario 2: Provider Payout ```javascript // 1. Create provider contact const provider = await createContact(token, orgId, merchantId, { firstName: 'Servicios ABC SAS', countrySymbol: 'CO', businessType: 'COMPANY', type: 'BUSINESS', email: 'provider@example.com', documentType: 'NIT', documentNumber: '900123456-1' }); // 2. Add provider's bank account const bankAccount = await addBankAccountToContact(token, orgId, merchantId, provider.id, { name: 'Bancolombia COP', kind: 'BANK', isDefault: true, countrySymbol: 'CO', currencySymbol: 'COP', entity: 'BANCOLOMBIA', accountNumber: '1234567890', type: 'CHECKING' }); // 3. Create PAYOUT order const payout = await createPayoutOrder(token, orgId, merchantId, { contactId: provider.id, destinationAccountId: bankAccount.id, // Link to bank account amount: 500000, currency: 'COP' }); ``` --- ## Next Steps Learn how contacts are used in orders Create PAYIN orders with customer contacts Complete guide to provider payouts Full contacts API documentation --- # Merchant External Accounts _Setting up bank and crypto accounts for receiving funds_ Source: https://docs.koywe.com/en/core-concepts/merchant-external-accounts # Merchant External Accounts Configure your merchant's external bank accounts and crypto wallets to receive funds from virtual account settlements and crypto purchases. ## What are Merchant External Accounts? **Merchant External Accounts** are your merchant's own bank accounts and crypto wallets where you receive funds from Koywe operations. ### Types of External Accounts Your company bank accounts for receiving fiat settlements and withdrawals Your crypto wallet addresses for receiving cryptocurrency from ONRAMP deals --- ## External Bank Accounts vs Virtual Accounts Understanding the difference: | Feature | Virtual Account | External Bank Account | |---------|----------------|----------------------| | **Location** | Within Koywe | Your own bank | | **Purpose** | Operations & transactions | Final settlement destination | | **Setup** | Automatic | Manual configuration | | **Usage** | PAYINs, PAYOUTs, transfers | Withdrawals, settlements | | **Currencies** | Multiple supported | Bank-specific | | **Fees** | None for internal ops | May have transfer fees | |Credits| B[Virtual Account COP] B -->|Operates| C[PAYOUTs, Transfers, ONRAMP] B -->|Settlement| D[Your External Bank Account] style B fill:#005544,stroke:#C9FF1F,stroke-width:2px,color:#fff style D fill:#2563eb,stroke:#1d4ed8,stroke-width:2px,color:#fff`} /> **Automatic Settlement**: Funds in virtual accounts are automatically settled to your registered external bank account after a configured period (typically 1-7 days). You can also manually request withdrawals. --- ## Setting Up External Bank Accounts ### When You Need an External Bank Account βœ… **Required for**: - Receiving automatic settlements from virtual accounts - Withdrawing funds from virtual balances - Receiving merchant payouts - Compliance and verification ### Creating an External Bank Account **Automatic Association**: Bank accounts are automatically associated with your merchant's registered business information. No need to send holder name or document numbers - we use the information already in the system. **Unified Endpoint**: Both bank accounts and crypto wallets use the same `/accounts` endpoint. The `kind` field determines the account type: `"BANK"` for fiat bank accounts, `"CRYPTO"` for crypto wallets. {/* Multi-language code examples */} {/* Node.js */} ```javascript async function addMerchantBankAccount(token, orgId, merchantId) { const response = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/accounts`, { name: 'Company Settlement Account', kind: 'BANK', // Indicates this is a fiat bank account countrySymbol: 'CO', currencySymbol: 'COP', entity: 'BANCOLOMBIA', // Bank code accountNumber: '1234567890', type: 'CHECKING', // or 'SAVINGS' isDefault: true, // Set as default settlement account isVirtual: false // External account (not Koywe-managed) }, { headers: { 'Authorization': `Bearer ${token}` } } ); console.log('Bank account added:', response.data.id); console.log('Status:', response.data.status); return response.data; } const bankAccount = await addMerchantBankAccount(token, orgId, merchantId); ``` {/* Python */} ```python def add_merchant_bank_account(token, org_id, merchant_id): response = requests.post( f'https://api-sandbox.koywe.com/api/v1/organizations/{org_id}/merchants/{merchant_id}/accounts', headers={'Authorization': f'Bearer {token}'}, json={ 'name': 'Company Settlement Account', 'kind': 'BANK', # Indicates this is a fiat bank account 'countrySymbol': 'CO', 'currencySymbol': 'COP', 'entity': 'BANCOLOMBIA', # Bank code 'accountNumber': '1234567890', 'type': 'CHECKING', # or 'SAVINGS' 'isDefault': True, # Set as default settlement account 'isVirtual': False # External account (not Koywe-managed) } ) response.raise_for_status() bank_account = response.json() print(f'Bank account added: {bank_account["id"]}') print(f'Status: {bank_account["status"]}') return bank_account bank_account = add_merchant_bank_account(token, org_id, merchant_id) ``` {/* cURL */} ```bash curl -X POST https://api-sandbox.koywe.com/api/v1/organizations/{orgId}/merchants/{merchantId}/accounts \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Company Settlement Account", "kind": "BANK", "countrySymbol": "CO", "currencySymbol": "COP", "entity": "BANCOLOMBIA", "accountNumber": "1234567890", "type": "CHECKING", "isDefault": true, "isVirtual": false }' ``` ### Required Fields by Country **Bank Code Flexibility**: For most countries, the bank code can be deduced from the account number. You can still provide it, but the API will validate against the deduced value. For countries where it's required, validation ensures the bank is in the supported list. #### Chile πŸ‡¨πŸ‡± ```javascript { name: 'Chile Settlement Account', kind: 'BANK', countrySymbol: 'CL', currencySymbol: 'CLP', entity: 'BANCO_CHILE', // Required: BANCO_CHILE, BCI, SANTANDER_CHILE, etc. accountNumber: '12345678', // Required isDefault: true, isVirtual: false } ``` **Validations**: Bank code (`entity`) must be in the valid banks list. --- #### Colombia πŸ‡¨πŸ‡΄ ```javascript { name: 'Colombia Settlement Account', kind: 'BANK', countrySymbol: 'CO', currencySymbol: 'COP', entity: 'BANCOLOMBIA', // Required: BANCOLOMBIA, DAVIVIENDA, BOGOTA, etc. accountNumber: '1234567890', // Required type: 'CHECKING', // Required: 'CHECKING' or 'SAVINGS' isDefault: true, isVirtual: false } ``` **Validations**: Account type must be supported by the bank. Account number length validated (min/max). --- #### Brazil πŸ‡§πŸ‡· ```javascript { name: 'Brazil Settlement Account', kind: 'BANK', countrySymbol: 'BR', currencySymbol: 'BRL', accountNumber: '12345-6', // Required (OR pixKey) // entity: '001' // Optional (deduced from accountNumber) isDefault: true, isVirtual: false } ``` **Validations**: Bank code deduced from account number. PIX key can be used instead of account number. --- #### Mexico πŸ‡²πŸ‡½ ```javascript { name: 'Mexico Settlement Account', kind: 'BANK', countrySymbol: 'MX', currencySymbol: 'MXN', accountNumber: '012345678901234567', // Required: CLABE (18 digits) // entity: 'BBVA_MEXICO' // Optional (deduced from accountNumber) isDefault: true, isVirtual: false } ``` **Validations**: CLABE format validation (18 digits). Check digit validation. Bank deduced from CLABE. --- #### Argentina πŸ‡¦πŸ‡· ```javascript { name: 'Argentina Settlement Account', kind: 'BANK', countrySymbol: 'AR', currencySymbol: 'ARS', accountNumber: '0123456789012345678901', // Required: CVU or CBU or alias // entity: 'BANCO_NACION' // Optional (deduced from accountNumber) isDefault: true, isVirtual: false } ``` **Validations**: CVU/CBU format validation. Check digit validation. Bank deduced from account number. Alias supported. --- #### Peru πŸ‡΅πŸ‡ͺ ```javascript { name: 'Peru Settlement Account', kind: 'BANK', countrySymbol: 'PE', currencySymbol: 'PEN', accountNumber: '01234567890123456789', // Required: CCI format // entity: 'BCP' // Optional (deduced from accountNumber) isDefault: true, isVirtual: false } ``` **Validations**: CCI format validation (20 digits). Bank deduced from account number. --- #### Bolivia πŸ‡§πŸ‡΄ ```javascript { name: 'Bolivia Settlement Account', kind: 'BANK', countrySymbol: 'BO', currencySymbol: 'BOB', accountNumber: '1234567890', // Required // entity: 'BNB' // Optional (deduced from accountNumber) isDefault: true, isVirtual: false } ``` **Validations**: Bank deduced from account number. --- #### United States πŸ‡ΊπŸ‡Έ ```javascript { name: 'US Settlement Account', kind: 'BANK', countrySymbol: 'US', currencySymbol: 'USD', accountNumber: '123456789', // Required routingNumber: '021000021', // Required // entity: 'CHASE' // Optional (deduced from routingNumber) isDefault: true, isVirtual: false } ``` **Validations**: Bank deduced from routing number. Generic bank saved if not found. ### Verification Process ### Account Created External bank account is created with status `PENDING_VERIFICATION` ### Microdeposit Sent (if required) Koywe may send a small test deposit to verify account ownership ### Verification Completed You verify the deposit amount or account is auto-verified ### Account Activated Status changes to `VERIFIED` and ready for settlements **Automatic Verification**: The bank account is automatically linked to your merchant's registered business information. Ensure your merchant profile has the correct legal name and tax ID, as these will be used for account verification. ### Listing Merchant Bank Accounts {/* Multi-language code examples */} {/* Node.js */} ```javascript async function getMerchantBankAccounts(token, orgId, merchantId) { const response = await axios.get( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/accounts`, { params: { kind: 'BANK' }, // Filter for bank accounts only headers: { 'Authorization': `Bearer ${token}` } } ); console.log('External bank accounts:'); response.data.forEach(account => { console.log(`- ${account.currencySymbol}: ${account.entity} ****${account.accountNumber.slice(-4)}`); console.log(` Default: ${account.isDefault}, Status: ${account.status}`); }); return response.data; } const accounts = await getMerchantBankAccounts(token, orgId, merchantId); ``` --- ## Setting Up External Crypto Wallets ### When You Need an External Crypto Wallet βœ… **Required for**: - Receiving cryptocurrency from ONRAMP deals - Sending crypto to your own custody solutions - Long-term crypto holdings outside Koywe **Embedded Wallets vs External Wallets**: Koywe-managed embedded wallets are provisioned through the passkey flow (`/webauthn/wallet/prepare` and `/webauthn/wallet/complete`) β€” see [Passkeys & Approvals](/en/advanced/passkeys-and-approvals). External wallets are YOUR OWN wallets on hardware devices, exchanges, or other custody solutions where you want to receive crypto, and you register them yourself with the calls below. ### Adding an External Crypto Wallet **Same Endpoint as Bank Accounts**: External crypto wallets use the same `/accounts` endpoint as bank accounts. Set `kind: "CRYPTO"` to create a crypto wallet instead of a bank account. {/* Multi-language code examples */} {/* Node.js */} ```javascript async function addExternalCryptoWallet(token, orgId, merchantId, walletData) { const response = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/accounts`, { name: walletData.name, // e.g., "Company Ledger Wallet" kind: 'CRYPTO', // Indicates this is a crypto wallet currencySymbol: walletData.currency, // USDC, USDT, BTC, ETH, etc. network: walletData.network, // ETHEREUM, POLYGON, BITCOIN, etc. address: walletData.address, // Your wallet address isDefault: walletData.isDefault || false, isVirtual: false // External wallet (not Koywe-managed) }, { headers: { 'Authorization': `Bearer ${token}` } } ); console.log('External wallet added:', response.data.id); console.log('Address:', response.data.address); console.log('Network:', response.data.network); return response.data; } // Example: Add USDC on Ethereum const usdcWallet = await addExternalCryptoWallet(token, orgId, merchantId, { name: 'Company Ledger - USDC', currency: 'USDC', network: 'ETHEREUM', address: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb2', isDefault: true }); console.log('USDC wallet registered for ONRAMP operations'); ``` {/* Python */} ```python def add_external_crypto_wallet(token, org_id, merchant_id, wallet_data): response = requests.post( f'https://api-sandbox.koywe.com/api/v1/organizations/{org_id}/merchants/{merchant_id}/accounts', headers={'Authorization': f'Bearer {token}'}, json={ 'name': wallet_data['name'], 'kind': 'CRYPTO', # Indicates this is a crypto wallet 'currencySymbol': wallet_data['currency'], 'network': wallet_data['network'], 'address': wallet_data['address'], 'isDefault': wallet_data.get('is_default', False), 'isVirtual': False # External wallet (not Koywe-managed) } ) response.raise_for_status() wallet = response.json() print(f'External wallet added: {wallet["id"]}') print(f'Address: {wallet["address"]}') print(f'Network: {wallet["network"]}') return wallet # Example: Add USDT on Polygon usdt_wallet = add_external_crypto_wallet(token, org_id, merchant_id, { 'name': 'Company Custody - USDT', 'currency': 'USDT', 'network': 'POLYGON', 'address': '0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063', 'is_default': True }) print('USDT wallet registered for ONRAMP operations') ``` ### Supported Networks and Currencies | Currency | Name | Networks | | --- | --- | --- | | `USDT` | Tether | `ETHEREUM` Β· `POLYGON` Β· `SOLANA` Β· `BASE` Β· `ALGORAND` Β· `TRON` Β· `BSC` | | `USDC` | USD Coin | `ETHEREUM` Β· `POLYGON` Β· `SOLANA` Β· `BASE` Β· `ALGORAND` | | `ETH` | Ether | `ETHEREUM` Β· `BASE` | | `BTC` | Bitcoin | `BITCOIN` | | `EURC` | Euro Coin | `BASE` | | `MATIC` | Polygon | `POLYGON` | | `SOL` | Solana | `SOLANA` | | `TRX` | Tron | `TRON` | Only these pairs are valid. Any other symbol/network combination is rejected by the API. **Network Matching Critical**: When creating ONRAMP deals, the `network` parameter must match one of your registered external wallet networks. Always verify the network before sending crypto! ### Listing External Crypto Wallets {/* Multi-language code examples */} {/* Node.js */} ```javascript async function getExternalWallets(token, orgId, merchantId) { const response = await axios.get( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/accounts`, { params: { kind: 'CRYPTO' }, // Filter for crypto wallets only headers: { 'Authorization': `Bearer ${token}` } } ); console.log('External crypto wallets:'); response.data.forEach(wallet => { console.log(`- ${wallet.currencySymbol} (${wallet.network})`); console.log(` Address: ${wallet.address}`); console.log(` Default: ${wallet.isDefault}, Status: ${wallet.status}`); }); return response.data; } const wallets = await getExternalWallets(token, orgId, merchantId); ``` --- ## Using External Accounts in Operations ### ONRAMP: Send Crypto to External Wallet When creating ONRAMP deals, specify your external wallet as the destination: {/* Multi-language code examples */} {/* Node.js */} ```javascript // 1. Get quote const quote = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/quotes`, { orderType: 'ONRAMP', executable: true, originCurrencySymbol: 'COP', destinationCurrencySymbol: 'USDC', amountIn: 1000000, // 1M COP network: 'ETHEREUM' // Required for ONRAMP/OFFRAMP }, { headers: { 'Authorization': `Bearer ${token}` } } ); // 2. Create deal with external wallet as destination const deal = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/deals`, { destinationAccountId: usdcWallet.id, // Your external wallet ID quoteId: quote.data.id }, { headers: { 'Authorization': `Bearer ${token}` } } ); console.log('ONRAMP deal created'); console.log('Crypto will be sent to:', usdcWallet.address); console.log('Network:', usdcWallet.network); ``` **Use Embedded vs External**: For operational crypto (frequent trading, OFFRAMP), use embedded wallets. For long-term holdings or specific custody requirements, use external wallets. ### Withdrawals: Move Funds to External Bank Account Request withdrawal from virtual account to your external bank account: {/* Multi-language code examples */} {/* Node.js */} ```javascript async function withdrawToExternalBank(token, orgId, merchantId, withdrawalData) { // Check virtual account balance first const balances = await axios.get( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/accounts/balances`, { headers: { 'Authorization': `Bearer ${token}` } } ); const copBalance = balances.data.find(b => b.currencySymbol === 'COP'); if (copBalance.availableBalance < withdrawalData.amount) { throw new Error('Insufficient balance for withdrawal'); } // Create withdrawal order const withdrawal = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/withdrawals`, { currency: 'COP', amount: withdrawalData.amount, destinationAccountId: withdrawalData.bankAccountId, // Your external bank account description: 'Withdrawal to company account' }, { headers: { 'Authorization': `Bearer ${token}` } } ); console.log('Withdrawal initiated:', withdrawal.data.id); console.log('Amount:', withdrawal.data.amount, 'COP'); console.log('Status:', withdrawal.data.status); // "PROCESSING" console.log('ETA:', withdrawal.data.estimatedSettlement); return withdrawal.data; } const withdrawal = await withdrawToExternalBank(token, orgId, merchantId, { amount: 5000000, // 5M COP bankAccountId: bankAccount.id }); ``` **Automatic Settlements**: If you have automatic settlements enabled, funds will be transferred to your primary external bank account automatically after the configured holding period. Manual withdrawals give you more control over timing. --- ## Best Practices ### Security Always double-check crypto wallet addresses before adding them. Errors are irreversible. Send small test transactions to new external wallets before large transfers. Set up webhooks to track settlements and withdrawals to external accounts. Maintain records of all external accounts for compliance and auditing. ### Account Management βœ… **Do**: - Keep primary accounts up to date - Verify new accounts immediately - Use separate wallets per network - Document account purposes internally ❌ **Don't**: - Share account credentials - Use exchange deposit addresses as external wallets (use withdrawal addresses) - Add unverified or test wallets in production - Use personal accounts for business operations --- ## Common Patterns ### Multi-Currency Setup For businesses operating in multiple countries: ```javascript // Setup external bank accounts for each currency const copAccount = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/accounts`, { name: 'Bancolombia COP', kind: 'BANK', isDefault: true, countrySymbol: 'CO', currencySymbol: 'COP', entity: 'BANCOLOMBIA', accountNumber: '1234567890', type: 'CHECKING' }, { headers: { 'Authorization': `Bearer ${token}` } } ); const brlAccount = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/accounts`, { name: 'Brazil BRL', kind: 'BANK', isDefault: false, countrySymbol: 'BR', currencySymbol: 'BRL', accountNumber: '12345-6' // Bank deduced automatically }, { headers: { 'Authorization': `Bearer ${token}` } } ); const mxnAccount = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/accounts`, { name: 'Mexico MXN', kind: 'BANK', isDefault: false, countrySymbol: 'MX', currencySymbol: 'MXN', accountNumber: '012345678901234567' // CLABE - bank deduced }, { headers: { 'Authorization': `Bearer ${token}` } } ); console.log('Multi-currency external accounts configured'); console.log('All accounts auto-linked to merchant business information'); ``` ### Multi-Network Crypto Setup For diversified crypto holdings: ```javascript // USDC on multiple networks const usdcEthereum = await addExternalCryptoWallet(token, orgId, merchantId, { name: 'USDC - Ethereum Main', currency: 'USDC', network: 'ETHEREUM', address: '0x...', isDefault: true }); const usdcPolygon = await addExternalCryptoWallet(token, orgId, merchantId, { name: 'USDC - Polygon Low Fees', currency: 'USDC', network: 'POLYGON', address: '0x...', isDefault: false }); // Different stablecoins const usdtTron = await addExternalCryptoWallet(token, orgId, merchantId, { name: 'USDT - Tron', currency: 'USDT', network: 'TRON', address: 'T...', isDefault: false }); console.log('Multi-network external wallets configured'); ``` --- ## Next Steps Understand internal balance management Buy crypto to external wallets Send payments to external accounts Monitor external account operations --- # Orders & Order Types _Understanding the seven order types and when to use each_ Source: https://docs.koywe.com/en/core-concepts/orders-and-order-types # Orders & Order Types ## Orders create β€” quick reference > **Authoritative schema:** `npx @koyweforest/cli orders create --schema` prints the full, up-to-date JSON schema for the create request. If the summary below ever diverges from that output, the CLI schema is the source of truth. **Endpoint (PAYIN / PAYOUT / BALANCE_TRANSFER / PAYMENT_LINK / INTER_MERCHANT_TRANSFER):** `POST /organizations/{orgId}/merchants/{merchantId}/orders` **ONRAMP and OFFRAMP are not created via this endpoint.** Crypto operations go through the **deals** endpoint (`POST /organizations/{orgId}/merchants/{merchantId}/deals`) because a deal can be paid in full or across multiple partial payments, each of which creates its own order. See the [ONRAMP](#onramp---buy-cryptocurrency) and [OFFRAMP](#offramp---sell-cryptocurrency) sections below, or use `npx @koyweforest/cli flow deal`. **Required fields:** - `type` β€” always required. - `originCurrencySymbol`, `destinationCurrencySymbol` β€” required **unless you pass `quoteId`** (in which case omit them along with `amountIn`/`amountOut`; the quote carries all three). **`type` enum (7 values β€” ONRAMP and OFFRAMP route via `/deals`, the rest via `/orders`):** `PAYIN` Β· `PAYOUT` Β· `BALANCE_TRANSFER` Β· `PAYMENT_LINK` Β· `INTER_MERCHANT_TRANSFER` β€” via `/orders` `ONRAMP` Β· `OFFRAMP` β€” via `/deals` ([see below](#onramp---buy-cryptocurrency)) **Amount specification (mutually exclusive β€” pick exactly one):** - **`amountIn`** β€” amount in origin currency. - **`amountOut`** β€” amount in destination currency. - **`quoteId`** β€” ID of a prior quote that locks the rate. When you pass `quoteId`, omit `originCurrencySymbol`, `destinationCurrencySymbol`, `amountIn`, and `amountOut` β€” the quote supplies them. **Conditional requirements (for types that route via `/orders`):** | Order type | Also required | |---|---| | `PAYIN` | `paymentMethods` (exactly one element) | | `PAYOUT` | `destinationAccountId` | | `BALANCE_TRANSFER` | An active policy with a matching rule (see [Passkeys & Approvals](/en/advanced/passkeys-and-approvals)). Auto-sets `isMerchantSelfOrder=true` if not provided. | | `PAYMENT_LINK` | `paymentMethods` may be `[]` | | `INTER_MERCHANT_TRANSFER` | `destinationMerchantId` | For `ONRAMP` / `OFFRAMP` see the [Deals](#onramp---buy-cryptocurrency) sections β€” those types require `destinationAccountId` and a `network` from the enum below. **`network` enum (ONRAMP/OFFRAMP, passed on the deal):** `ETHEREUM` Β· `POLYGON` Β· `SOLANA` Β· `TRON` Β· `BSC` Β· `BITCOIN` Β· `BASE` Β· `ALGORAND` **`paymentMethods` shape:** ```json [ { "method": "KHIPU" } ] ``` ```json [ { "method": "PSE", "extra": { "bankAccount": { "name": "BANCOLOMBIA" } } } ] ``` **Policy & MFA:** Operations gated by policy return `POL00007` (`428 Precondition Required`) and the order enters `ON_HOLD` until approved. Pass `--mfa-token ` to confirm inline, or use `npx @koyweforest/cli flow order --wait` to block on polling. See the full [CLI reference for `orders create`](/en/cli/reference#orders), the [Error Code Catalog](/en/advanced/error-codes) for every `OR*` / `BAA*` / `POL*` code, and the complete [orders API spec](/api-reference). --- Orders are the core transaction entities in Koywe Payments. Each order type serves a specific purpose in your payment operations. ## Order Types Overview Koywe Payments supports seven distinct order types: | Type | Purpose | Origin | Destination | Use Case | |------|---------|--------|-------------|----------| | **PAYIN** | Accept customer payments | External (customer) | Virtual balance | E-commerce checkout | | **PAYOUT** | Pay providers | Virtual balance | External (provider bank) | Vendor payments | | **BALANCE_TRANSFER** | Currency exchange | Virtual balance (currency A) | Virtual balance (currency B) | Convert COP to USD | | **ONRAMP** | Buy crypto | Virtual balance (fiat) | Crypto wallet | Buy USDC with COP | | **OFFRAMP** | Sell crypto | Crypto wallet | Virtual balance (fiat) | Sell BTC for COP | | **PAYMENT_LINK** | Payment link | External (customer) | Virtual balance | Invoice payment | | **INTER_MERCHANT_TRANSFER** | Move funds between merchants in the same organization | Virtual balance (merchant A) | Virtual balance (merchant B) | Treasury operations within one organization | --- ## Order Status Lifecycle All orders follow a similar status progression: PENDING: Order Created PENDING --> ON_HOLD: Policy/MFA Required ON_HOLD --> PROCESSING: Approved (MFA or approval) ON_HOLD --> CANCELLED: Approval Rejected PENDING --> PROCESSING: Payment Initiated PROCESSING --> PAID: Payment Confirmed PAID --> COMPLETED: Funds Settled PENDING --> EXPIRED: Time Limit Exceeded PENDING --> CANCELLED: Cancelled by User PROCESSING --> FAILED: Payment Failed PAID --> FAILED: Settlement Failed COMPLETED --> [*] EXPIRED --> [*] CANCELLED --> [*] FAILED --> [*]`} /> **Status Descriptions**: | Status | Description | Actions Available | |--------|-------------|-------------------| | `PENDING` | Order created, waiting for payment | Cancel, Check status | | `ON_HOLD` | Held pending MFA verification or human approval under an active policy (`POL00007` at creation). See [Policy & MFA](#orders-create--quick-reference) and [Passkeys & Approvals](/en/advanced/passkeys-and-approvals). | Approve / reject via dashboard, provide `--mfa-token`, or poll with `npx @koyweforest/cli flow order --wait` | | `PROCESSING` | Payment being processed | Check status | | `PAID` | Payment confirmed, settlement in progress | Check status | | `COMPLETED` | Funds delivered, order complete | View details | | `FAILED` | Payment or settlement failed | Retry, Contact support | | `CANCELLED` | Order cancelled (or approval rejected) | View details | | `EXPIRED` | Payment window expired | Create new order | --- ## PAYIN - Accept Customer Payments Accept payments from customers into your virtual balance. ### When to Use - E-commerce checkout - Service payments - Subscription billing - Invoice payments - Donation collection ### Flow Diagram >K: Create PAYIN Order K-->>M: Return Payment URL M->>C: Redirect to Payment URL C->>P: Complete Payment P->>K: Confirm Payment K->>V: Credit Virtual Account K->>M: Webhook: order.paid K->>M: Webhook: order.completed`} /> ### Required Fields ```javascript { "type": "PAYIN", // Order type "originCurrencySymbol": "COP", // Fiat currency "destinationCurrencySymbol": "COP", // Same as origin "amountIn": 50000, // Amount to collect "paymentMethods": [ // At least one payment method { "method": "PSE", // Payment method code "extra": { "bankAccount": { "name": "BANCOLOMBIA" } } // Per-method extras } ] } ``` ### Optional Fields ```javascript { "description": "Payment for Order #12345", // Customer-facing description "successUrl": "https://yoursite.com/success", // Redirect after success "failedUrl": "https://yoursite.com/failed", // Redirect after failure "contactId": "cnt_abc123", // Link to customer contact "externalId": "order-12345", // Your internal reference "dueDate": "2025-11-14T23:59:59Z" // Payment deadline } ``` ### Example {/* Multi-language code examples */} {/* Node.js */} ```javascript async function createPayinOrder(token, orgId, merchantId) { const response = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/orders`, { type: 'PAYIN', originCurrencySymbol: 'COP', destinationCurrencySymbol: 'COP', amountIn: 50000, description: 'Payment for Order #12345', externalId: `order-${Date.now()}`, paymentMethods: [ { method: 'PSE', extra: { bankAccount: { name: 'BANCOLOMBIA' } } } ], successUrl: 'https://yoursite.com/success', failedUrl: 'https://yoursite.com/failed' }, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } } ); return response.data; } const order = await createPayinOrder(token, orgId, merchantId); console.log('Payment URL:', order.providedAction); // Redirect customer to order.providedAction ``` [Complete PAYIN Integration Guide β†’](/en/accepting-payments/integration-guide) --- ## PAYOUT - Pay Providers Send payments from your virtual balance to external bank accounts. ### When to Use - Vendor payments - Contractor payouts - Refunds to customers - Affiliate commissions - Partner settlements ### Flow Diagram >K: Create PAYOUT Order K->>V: Reserve Funds K->>B: Initiate Transfer B-->>K: Confirm Transfer K->>V: Debit Funds K->>M: Webhook: order.completed`} /> ### Required Fields ```javascript { "type": "PAYOUT", // Order type "originCurrencySymbol": "COP", // Fiat currency "destinationCurrencySymbol": "COP", // Same as origin "amountIn": 100000, // Amount to send "destinationAccountId": "ba_xyz789" // Provider's bank account ID } ``` ### Optional Fields ```javascript { "description": "Payment for Invoice #456", // Internal description "contactId": "cnt_provider123", // Link to provider contact "externalId": "invoice-456" // Your internal reference } ``` ### Example {/* Multi-language code examples */} {/* Node.js */} ```javascript async function createPayoutOrder(token, orgId, merchantId, contactId, bankAccountId) { // 1. Check balance first const balances = await getMerchantBalances(token, orgId, merchantId); const copBalance = balances.find(b => b.currencySymbol === 'COP'); if (copBalance.availableBalance < 100000) { throw new Error('Insufficient balance'); } // 2. Create payout order const response = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/orders`, { type: 'PAYOUT', originCurrencySymbol: 'COP', destinationCurrencySymbol: 'COP', amountIn: 100000, contactId: contactId, destinationAccountId: bankAccountId, description: 'Payment for Invoice #456', externalId: `invoice-456-${Date.now()}` }, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } } ); return response.data; } const payout = await createPayoutOrder(token, orgId, merchantId, 'cnt_provider', 'ba_xyz789'); console.log('Payout created:', payout.id); ``` **Important**: Always check your virtual account balance before creating PAYOUT orders. The order will fail if you have insufficient funds. [Complete PAYOUT Integration Guide β†’](/en/paying-providers/integration-guide) --- ## BALANCE_TRANSFER - Currency Exchange Transfer funds between different currency virtual accounts (instant currency exchange). ### When to Use - Convert COP to USD for international payments - Rebalance currency holdings - Lock in exchange rates - Prepare funds for specific currency payouts ### Flow Diagram >K: Get Quote (COP β†’ USD) K-->>M: Exchange Rate M->>K: Create BALANCE_TRANSFER K->>V1: Debit COP K->>V2: Credit USD K->>M: Order Completed (instant)`} /> ### Required Fields ```javascript { "type": "BALANCE_TRANSFER", // Order type "originCurrencySymbol": "COP", // Source currency "destinationCurrencySymbol": "USD", // Target currency "amountIn": 1000000 // Amount to convert (1M COP) } ``` ### Example {/* Multi-language code examples */} {/* Node.js */} ```javascript async function transferCurrency(token, orgId, merchantId) { // 1. Get quote for exchange rate const quote = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/quotes`, { orderType: 'BALANCE_TRANSFER', executable: true, originCurrencySymbol: 'COP', destinationCurrencySymbol: 'USD', amountIn: 1000000 }, { headers: { 'Authorization': `Bearer ${token}` } } ); console.log('Exchange rate:', quote.data.exchangeRate); console.log('Will receive:', quote.data.finalAmountOut, 'USD'); // 2. Create transfer order β€” passing quoteId locks rate & amount, so omit // originCurrencySymbol / destinationCurrencySymbol / amountIn / amountOut const response = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/orders`, { type: 'BALANCE_TRANSFER', quoteId: quote.data.id }, { headers: { 'Authorization': `Bearer ${token}` } } ); return response.data; } const transfer = await transferCurrency(token, orgId, merchantId); console.log('Transfer completed instantly:', transfer.status); // "COMPLETED" ``` **Instant Settlement**: BALANCE_TRANSFER orders complete instantly. No waiting for external confirmations. [Complete Balance Transfer Guide β†’](/en/balance-management) --- ## ONRAMP - Buy Cryptocurrency Convert fiat currency to cryptocurrency. **Uses Deals Endpoint**: ONRAMP operations use the `/deals` endpoint. Deals can be paid in full or partially, with each payment creating orders that execute the crypto purchase. ### When to Use - Buy crypto for treasury - Offer crypto purchases to users - Hedge with stablecoins - Pay crypto-native providers ### Supported Cryptocurrencies | Currency | Name | Networks | | --- | --- | --- | | `USDT` | Tether | `ETHEREUM` Β· `POLYGON` Β· `SOLANA` Β· `BASE` Β· `ALGORAND` Β· `TRON` Β· `BSC` | | `USDC` | USD Coin | `ETHEREUM` Β· `POLYGON` Β· `SOLANA` Β· `BASE` Β· `ALGORAND` | | `ETH` | Ether | `ETHEREUM` Β· `BASE` | | `BTC` | Bitcoin | `BITCOIN` | | `EURC` | Euro Coin | `BASE` | | `MATIC` | Polygon | `POLYGON` | | `SOL` | Solana | `SOLANA` | | `TRX` | Tron | `TRON` | Only these pairs are valid. Any other symbol/network combination is rejected by the API. ### Required Fields for Quote ```javascript { "orderType": "ONRAMP", "executable": true, "originCurrencySymbol": "COP", // Fiat currency "destinationCurrencySymbol": "USDC", // Crypto currency "amountIn": 50000 // Fiat amount } ``` ### Required Fields for Deal ```javascript { // CRYPTO account ID from K3 β€” not a wallet address "destinationAccountId": "acc_77a80cf9-496c-4d72-9c33-fca3c8d5bcfe", "quoteId": "quote_xyz789" } ``` **Simplified**: Deals only need the destination account and quote ID. The amounts and currencies come from the quote. ### Example {/* Multi-language code examples */} {/* Node.js */} ```javascript async function buyUSDC(token, orgId, merchantId, cryptoAccountId) { // 1. Get quote const quote = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/quotes`, { orderType: 'ONRAMP', executable: true, originCurrencySymbol: 'COP', destinationCurrencySymbol: 'USDC', amountIn: 50000 }, { headers: { 'Authorization': `Bearer ${token}` } } ); console.log('Will receive:', quote.data.finalAmountOut, 'USDC'); console.log('Exchange rate:', quote.data.exchangeRate); console.log('Fee:', quote.data.fee); // 2. Create deal (not order!) const response = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/deals`, { destinationAccountId: cryptoAccountId, quoteId: quote.data.id }, { headers: { 'Authorization': `Bearer ${token}` } } ); return response.data; } const deal = await buyUSDC(token, orgId, merchantId, 'acc_77a80cf9-496c-4d72-9c33-fca3c8d5bcfe'); console.log('ONRAMP deal created:', deal.id); console.log('Deal can be paid in full or partially'); ``` **Partial Payments**: ONRAMP deals can be paid in multiple installments. Each payment creates an order that purchases the proportional amount of crypto. Great for dollar-cost averaging! **Balance Required**: By default, sufficient balance in your virtual account is required to close an ONRAMP deal. Deals execute **automatically** when funds are credited to your account. Pre-approved merchants can operate without upfront balance. [Complete ONRAMP Guide β†’](/en/crypto-operations/onramp) --- ## OFFRAMP - Sell Cryptocurrency Convert cryptocurrency to fiat currency. **Uses Deals Endpoint**: OFFRAMP operations use the `/deals` endpoint. Unlike ONRAMP, OFFRAMP deals **must be paid completely** (no partial payments). The deal creates orders when fully funded. ### When to Use - Sell crypto holdings - Convert crypto payments to fiat - Realize crypto gains - Prepare fiat for operations ### Required Fields for Quote ```javascript { "orderType": "OFFRAMP", "executable": true, "originCurrencySymbol": "USDC", // Crypto currency "destinationCurrencySymbol": "COP", // Fiat currency "amountIn": 10 // Crypto amount (10 USDC) } ``` ### Required Fields for Deal ```javascript { "destinationAccountId": "va_cop_12345", // Virtual account ID "quoteId": "quote_xyz789" // Quote ID } ``` **Complete Payment Only**: OFFRAMP deals must be funded completely. Partial payments are not supported. ### Example {/* Multi-language code examples */} {/* Node.js */} ```javascript async function sellUSDC(token, orgId, merchantId) { // 1. Get quote const quote = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/quotes`, { orderType: 'OFFRAMP', executable: true, originCurrencySymbol: 'USDC', destinationCurrencySymbol: 'COP', amountIn: 10 // 10 USDC }, { headers: { 'Authorization': `Bearer ${token}` } } ); console.log('Will receive:', quote.data.finalAmountOut, 'COP'); // 2. Create deal (not order!) const response = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/deals`, { destinationAccountId: 'va_cop_12345', // Virtual account for COP quoteId: quote.data.id }, { headers: { 'Authorization': `Bearer ${token}` } } ); return response.data; } const deal = await sellUSDC(token, orgId, merchantId); console.log('OFFRAMP deal created:', deal.id); console.log('Deposit address:', deal.cryptoDestinationWallet); console.log('Send full crypto amount to complete'); // Funds will be credited to COP virtual account after crypto received ``` **Complete Funding**: After creating the deal, you must send the **full crypto amount** to the provided deposit address. Once received, the deal creates orders and credits your virtual account with fiat. [Complete OFFRAMP Guide β†’](/en/crypto-operations/offramp) --- ## PAYMENT_LINK - Payment Links Generate shareable payment links with a **complete Koywe-branded checkout flow** - no contact or payment method needed upfront! ### What Makes PAYMENT_LINK Special? Customer enters their own information in the checkout Customer selects their preferred payment method Beautiful, secure checkout hosted by Koywe Send via email, WhatsApp, SMS, or QR code ### When to Use - **E-commerce checkout**: Simple payment collection without complex integration - **Invoice payments**: Send payment link for issued invoices - **Email/WhatsApp**: Share payment requests directly - **QR code payments**: Generate QR for in-person payments - **One-time collections**: Quick payment requests without storing customer data ### How It Works >K: 1. Create PAYMENT_LINK (amount only) K-->>You: 2. Return payment URL You->>C: 3. Share link (email/WhatsApp/QR) C->>K: 4. Open link, enter details C->>K: 5. Select payment method C->>P: 6. Complete payment P->>K: 7. Confirm payment K->>You: 8. Webhook: order.completed`} /> ### Differences from PAYIN | Feature | PAYIN | PAYMENT_LINK | |---------|-------|--------------| | **Contact** | Optional but recommended | Not needed - customer enters data | | **Payment Method** | Must be specified | Customer selects from all available | | **Checkout UI** | Redirect to payment provider | Koywe-branded checkout flow | | **Data Collection** | You collect customer data | Koywe collects customer data | | **Best For** | Integrated checkout experience | Standalone payment requests | ### Minimal Required Fields **That's it!** No contact, no payment method, no customer details needed. Just amount and currency. ```javascript { "type": "PAYMENT_LINK", // Order type "originCurrencySymbol": "COP", // Currency "destinationCurrencySymbol": "COP", // Same as origin "amountIn": 75000 // Amount to collect } ``` ### Optional Fields ```javascript { "description": "Invoice #789", // Shows to customer "dueDate": "2025-11-14T23:59:59Z", // Link expiry "externalId": "invoice-789", // Your reference "successUrl": "https://yoursite.com/success", // Redirect after payment "failedUrl": "https://yoursite.com/failed" // Redirect on failure } ``` ### Complete Example {/* Multi-language code examples */} {/* Node.js - Simple */} ```javascript // Minimal example - just create and share! async function createPaymentLink(amount, description) { const response = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/orders`, { type: 'PAYMENT_LINK', originCurrencySymbol: 'COP', destinationCurrencySymbol: 'COP', amountIn: amount, description: description }, { headers: { 'Authorization': `Bearer ${token}` } } ); return response.data; } // Usage const link = await createPaymentLink(75000, 'Invoice #789 - Web Design'); console.log('Share this link:', link.providedAction); // Send via email, WhatsApp, SMS, or generate QR code await sendEmail(customerEmail, { subject: 'Payment Request - Invoice #789', body: `Please pay here: ${link.providedAction}` }); ``` {/* Node.js - With Options */} ```javascript // Full example with all options async function createPaymentLinkWithOptions(invoiceData) { const dueDate = new Date(); dueDate.setHours(dueDate.getHours() + 24); // Expires in 24h const response = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/orders`, { type: 'PAYMENT_LINK', originCurrencySymbol: 'COP', destinationCurrencySymbol: 'COP', amountIn: invoiceData.amount, description: invoiceData.description, externalId: invoiceData.invoiceNumber, dueDate: dueDate.toISOString(), successUrl: 'https://yoursite.com/payment/success', failedUrl: 'https://yoursite.com/payment/failed' }, { headers: { 'Authorization': `Bearer ${token}` } } ); return response.data; } // Usage const link = await createPaymentLinkWithOptions({ amount: 150000, description: 'Monthly Subscription - November 2025', invoiceNumber: 'INV-2025-11-789' }); console.log('Payment link:', link.providedAction); ``` {/* Python */} ```python def create_payment_link(amount, description): 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': amount, 'description': description }, headers={'Authorization': f'Bearer {token}'} ) return response.json() # Usage link = create_payment_link(75000, 'Invoice #789 - Web Design') print(f"Share this link: {link['providedAction']}") ``` **Response:** ```json { "id": "ord_abc123", "type": "PAYMENT_LINK", "status": "PENDING", "providedAction": "https://checkout.koywe.com/pay/ord_abc123", "providedActionType": "URL", "amountIn": 75000, "originCurrencySymbol": "COP", "description": "Invoice #789 - Web Design", "createdAt": "2025-11-13T15:00:00Z" } ``` **Customer Experience**: When customers open the payment link, they'll see a Koywe-branded checkout where they can: 1. Review payment details 2. Enter their personal information 3. Select their preferred payment method (PSE, PIX, Nequi, etc.) 4. Complete the payment --- ## External ID (Idempotency) Use `externalId` to ensure idempotent order creation: ```javascript { "externalId": "order-12345-20251113" // Your unique identifier } ``` **Benefits**: - Safe to retry failed requests - Prevents duplicate orders - Links to your internal systems --- ## INTER_MERCHANT_TRANSFER - Move Funds Between Merchants Move virtual-balance funds from one merchant to another merchant **in the same organization**, in the same currency, without hitting an external bank. Useful for treasury operations, fee settlement, and internal rebalancing across merchants you control. ### When to Use - Consolidating balances across your own merchants within one organization - Settling internal commissions between a parent and child merchant - Rebalancing liquidity so a specific merchant has enough funds for an upcoming payout ### Flow Diagram >K: POST /orders { type: INTER_MERCHANT_TRANSFER, destinationMerchantId: B, ... } K->>K: Debit A's virtual account K->>K: Credit B's virtual account K->>A: 201 Created { status: COMPLETED } K->>B: Webhook: balance credited`} /> ### Required Fields - `type` β€” `INTER_MERCHANT_TRANSFER` - `destinationMerchantId` β€” **Required.** Merchant inside the same organization. Cross-organization transfers are rejected (`IMT00002`). - `originCurrencySymbol` / `destinationCurrencySymbol` β€” must be the **same** currency (`IMT00008`; same-currency is MVP behaviour). - `amountIn` or `amountOut` β€” one of the two (same rule as the other `/orders` types). ### Example β€” Create Order ```javascript const response = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${sourceMerchantId}/orders`, { type: 'INTER_MERCHANT_TRANSFER', destinationMerchantId: 'mer_destination_abc', originCurrencySymbol: 'USD', destinationCurrencySymbol: 'USD', amountIn: 1000, description: 'July commission settlement' }, { headers: { 'Authorization': `Bearer ${token}` } } ); // Sandbox completes instantly like BALANCE_TRANSFER: console.log(response.data.status); // "COMPLETED" ``` ### Example β€” Response ```json { "id": "ord_imt_xyz789", "type": "INTER_MERCHANT_TRANSFER", "status": "COMPLETED", "originMerchantId": "mer_source_123", "destinationMerchantId": "mer_destination_abc", "originCurrencySymbol": "USD", "destinationCurrencySymbol": "USD", "amountIn": 1000, "amountOut": 1000, "description": "July commission settlement", "createdAt": "2026-04-24T12:00:00Z", "completedAt": "2026-04-24T12:00:01Z" } ``` ### Listing & Filtering The OpenAPI spec's `orders list` filter enum omits `INTER_MERCHANT_TRANSFER` today. Despite that, the query parameter `?type=INTER_MERCHANT_TRANSFER` **does filter correctly** against the API β€” the omission is a spec gap, not a runtime one. ```bash # Via CLI (the CLI reflects all 7 types correctly): npx @koyweforest/cli orders list --type INTER_MERCHANT_TRANSFER --format table # Via raw HTTP: curl -H "Authorization: Bearer $TOKEN" \ "https://api-sandbox.koywe.com/api/v1/organizations/$ORG/merchants/$MERCHANT/orders?type=INTER_MERCHANT_TRANSFER" ``` ### Common Errors | Code | Cause | Fix | |---|---|---| | `IMT00001` | Destination merchant not found | Check `destinationMerchantId` exists | | `IMT00002` | Destination merchant is in a different organization | Transfers are restricted to same-org merchants | | `IMT00003` | Source and destination merchant are the same | Use `BALANCE_TRANSFER` for same-merchant currency exchange | | `IMT00004` | Destination merchant is disabled | Enable the merchant or pick another destination | | `IMT00005` | Insufficient balance in source merchant account | Fund the source account or reduce the amount | | `IMT00007` | `destinationMerchantId` missing | Required field for INTER_MERCHANT_TRANSFER | | `IMT00008` | Origin and destination currency don't match | Same-currency only (MVP) | See the full [Error Code Catalog](/en/advanced/error-codes#imt--inter-merchant-transfer) for every `IMT*` code. --- ## Next Steps Create your first order Complete PAYIN integration guide Complete PAYOUT integration guide Track order status changes --- # Organizations & Merchants _Understanding the hierarchy and structure_ Source: https://docs.koywe.com/en/core-concepts/organizations-and-merchants # Organizations & Merchants The Organization-Merchant hierarchy is the foundation of how you structure your business in Koywe Payments. ## Organizations An **Organization** is the top-level entity that represents your company in the Koywe system. ### What is an Organization? - The root entity for your entire company - Can contain multiple merchants (business units) - Manages company-wide users and permissions - Controls API credentials and access - Handles organization-level reporting and billing ### Organization Permissions Organizations have hierarchical permissions: - **Organization-level users** can access all merchants within the organization - **Root users** have full administrative access - **API credentials** can be scoped to organization or merchant level B[Org Admin User] A --> C[Merchant: Acme Store] A --> D[Merchant: Acme Marketplace] B -.can access.-> C B -.can access.-> D style A fill:#003C2A,stroke:#C9FF1F,stroke-width:2px,color:#fff`} /> --- ## Merchants A **Merchant** represents an individual business unit, brand, or operational entity within your organization. ### What is a Merchant? - A sub-entity under an organization - Has its own virtual accounts for each currency - Processes payments independently - Maintains separate transaction history - Can have specific users and permissions ### When to Use Multiple Merchants Use separate merchants when you need to: If you operate multiple brands or storefronts, each can be a separate merchant: - **Acme Store** (main e-commerce) - **Acme Marketplace** (peer-to-peer platform) - **Acme Services** (consulting business) For franchise or multi-location businesses: - **Store Location 1** (BogotΓ‘) - **Store Location 2** (MedellΓ­n) - **Store Location 3** (Cali) Different types of businesses under one company: - **Retail Division** - **Wholesale Division** - **Services Division** Keep financial operations separate for: - Different legal entities - Different tax jurisdictions - Separate accounting/reconciliation ### Merchant Automatic Resources When a merchant is created, Koywe automatically provisions: 1. **Virtual Accounts**: One for each supported currency - COP (Colombian Peso) - BRL (Brazilian Real) - MXN (Mexican Peso) - CLP (Chilean Peso) - USD (US Dollar) - EUR (Euro) - And more... 2. **Default Contact**: Merchant information as a contact - Used for compliance and reporting - Represents the merchant in transactions --- ## How They Work Together M1[Merchant 1] Organization --> M2[Merchant 2] subgraph M1[Merchant 1: Main Store] M1_VA1[Virtual Account COP] M1_VA2[Virtual Account USD] M1_VA3[Virtual Account BRL] M1_O[Orders] M1_C[Contacts] end subgraph M2[Merchant 2: Marketplace] M2_VA1[Virtual Account COP] M2_VA2[Virtual Account USD] M2_O[Orders] M2_C[Contacts] end style Organization fill:#003C2A,stroke:#C9FF1F,stroke-width:2px,color:#fff style M1 fill:#005544,stroke:#C9FF1F,stroke-width:2px,color:#fff style M2 fill:#005544,stroke:#C9FF1F,stroke-width:2px,color:#fff`} /> --- ## API Operations ### Getting Organization Information {/* Multi-language code examples */} {/* Node.js */} ```javascript async function getOrganizations(token) { const response = await axios.get( 'https://api-sandbox.koywe.com/api/v1/user/organizations', { headers: { 'Authorization': `Bearer ${token}` } } ); return response.data; } // Usage const orgs = await getOrganizations(token); console.log('Organizations:', orgs); ``` {/* Python */} ```python def get_organizations(token): response = requests.get( 'https://api-sandbox.koywe.com/api/v1/user/organizations', headers={'Authorization': f'Bearer {token}'} ) return response.json() # Usage orgs = get_organizations(token) print(f'Organizations: {orgs}') ``` {/* cURL */} ```bash curl -X GET 'https://api-sandbox.koywe.com/api/v1/user/organizations' \ -H 'Authorization: Bearer YOUR_TOKEN' ``` **Response:** ```json [ { "id": "org_abc123", "name": "Acme Corporation", "country": "CO", "createdAt": "2025-01-01T00:00:00Z" } ] ``` ### Listing Merchants in an Organization {/* Multi-language code examples */} {/* Node.js */} ```javascript async function getMerchants(token, organizationId) { const response = await axios.get( `https://api-sandbox.koywe.com/api/v1/organizations/${organizationId}/merchants`, { headers: { 'Authorization': `Bearer ${token}` } } ); return response.data; } // Usage const merchants = await getMerchants(token, 'org_abc123'); console.log('Merchants:', merchants); ``` {/* Python */} ```python def get_merchants(token, organization_id): response = requests.get( f'https://api-sandbox.koywe.com/api/v1/organizations/{organization_id}/merchants', headers={'Authorization': f'Bearer {token}'} ) return response.json() # Usage merchants = get_merchants(token, 'org_abc123') print(f'Merchants: {merchants}') ``` {/* cURL */} ```bash curl -X GET 'https://api-sandbox.koywe.com/api/v1/organizations/org_abc123/merchants' \ -H 'Authorization: Bearer YOUR_TOKEN' ``` **Response:** ```json [ { "id": "mrc_xyz789", "name": "Acme Store", "organizationId": "org_abc123", "status": "ACTIVE", "createdAt": "2025-01-15T00:00:00Z" }, { "id": "mrc_def456", "name": "Acme Marketplace", "organizationId": "org_abc123", "status": "ACTIVE", "createdAt": "2025-02-01T00:00:00Z" } ] ``` ### Getting Specific Merchant Details {/* Multi-language code examples */} {/* Node.js */} ```javascript async function getMerchant(token, organizationId, merchantId) { const response = await axios.get( `https://api-sandbox.koywe.com/api/v1/organizations/${organizationId}/merchants/${merchantId}`, { headers: { 'Authorization': `Bearer ${token}` } } ); return response.data; } // Usage const merchant = await getMerchant(token, 'org_abc123', 'mrc_xyz789'); console.log('Merchant details:', merchant); ``` --- ## Best Practices ### Organization Structure **Single Organization**: Most businesses only need one organization. Only create multiple if you have completely separate legal entities. ### Merchant Strategy **Do** create separate merchants for: - Different brands with separate branding - Franchises or locations requiring separate reporting - Business lines with different tax implications **Don't** create separate merchants for: - Different product categories within the same brand - Different payment types (use order metadata instead) - Temporary campaigns or promotions ### Naming Conventions Use clear, descriptive names: βœ… **Good names:** - "Acme Store - Main E-commerce" - "Acme BogotΓ‘ Location" - "Acme Wholesale Division" ❌ **Avoid:** - "Merchant 1" - "Test Merchant" - "New Merchant 2025" --- ## Permissions and Access Control ### Organization-Level Permissions Users with organization-level access can: - View all merchants - Create new merchants - Manage organization settings - Generate API credentials ### Merchant-Level Permissions Users with merchant-level access can: - View only assigned merchant(s) - Process orders for that merchant - Manage merchant contacts - View merchant balances ### API Credential Scoping API credentials can be scoped to: 1. **Organization Level** - Access all merchants - Useful for platform-wide integrations 2. **Merchant Level** - Access only specific merchant - More secure for limited use cases ```javascript // Organization-level credentials can access any merchant const orders = await axios.get( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/orders`, { headers: { 'Authorization': `Bearer ${orgLevelToken}` } } ); // Merchant-level credentials are pre-scoped const orders = await axios.get( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/orders`, { headers: { 'Authorization': `Bearer ${merchantLevelToken}` } } ); ``` --- ## Real-World Example ### Multi-Brand E-Commerce Company **Organization**: FashionCo Inc. **Merchants**: 1. **PremiumBrand Store** - High-end fashion e-commerce - Virtual accounts: COP, USD, EUR - Average transaction: $200 USD 2. **BudgetBrand Store** - Affordable fashion e-commerce - Virtual accounts: COP, BRL, MXN - Average transaction: $30 USD 3. **FashionCo Marketplace** - Third-party seller platform - Virtual accounts: COP, USD, BRL, MXN - Handles both PAYIN (customer) and PAYOUT (seller) **Benefits of this structure**: - Separate financial reporting per brand - Different branding for payment experiences - Independent balance management - Clearer reconciliation --- ## Next Steps Learn how merchants manage multi-currency balances Understand how to process payments for your merchants Complete step-by-step setup for organizations and merchants Explore all organization and merchant endpoints --- # Quotes & Exchange Rates _Understanding quotes and rate locking_ Source: https://docs.koywe.com/en/core-concepts/quotes-and-exchange-rates # Quotes & Exchange Rates Quotes provide exchange rate information and allow you to lock rates for a specific time period before creating orders. ## What are Quotes? A **Quote** is a request for exchange rate information that returns: - Current exchange rate between currencies - Fees and taxes breakdown - Expiration time (how long the rate is valid) - Expected amounts (input and output) ### Quote Time-to-Live (TTL) **Short Validity**: Quotes are valid for **10-15 seconds** only. After expiration, you must request a new quote. Always check the `validForSeconds` or `validUntil` field. **Why so short?** - Crypto and forex markets are volatile - Ensures accurate, real-time pricing - Protects against price manipulation - Prevents stale rate usage ### When to Use Quotes **Use quotes when**: - You need to show customers the exact amount they'll receive - Converting between currencies (BALANCE_TRANSFER) - Buying or selling crypto (ONRAMP/OFFRAMP) - You want rate transparency **Optional but Recommended**: Quotes are optional for most order types but highly recommended for transparency and better user experience. --- ## Creating a Quote ### Basic Quote Request {/* Multi-language code examples */} {/* Node.js */} ```javascript async function getQuote(token, orgId, merchantId, quoteData) { const response = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/quotes`, { orderType: quoteData.orderType, // Order type (PAYIN, PAYOUT, BALANCE_TRANSFER, ONRAMP, OFFRAMP) executable: true, // Whether the quote can be used to create an order originCurrencySymbol: quoteData.from, // Source currency destinationCurrencySymbol: quoteData.to, // Target currency amountIn: quoteData.amount // Amount to convert }, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } } ); return response.data; } // Usage - Get quote for BALANCE_TRANSFER const quote = await getQuote(token, orgId, merchantId, { orderType: 'BALANCE_TRANSFER', from: 'COP', to: 'USD', amount: 1000000 // 1,000,000 COP }); console.log('Exchange rate:', quote.exchangeRate); console.log('Will receive:', quote.finalAmountOut, 'USD'); console.log('Valid for:', quote.validForSeconds, 'seconds'); ``` {/* Python */} ```python def get_quote(token, org_id, merchant_id, quote_data): response = requests.post( f'https://api-sandbox.koywe.com/api/v1/organizations/{org_id}/merchants/{merchant_id}/quotes', json={ 'orderType': quote_data['order_type'], 'executable': True, 'originCurrencySymbol': quote_data['from'], 'destinationCurrencySymbol': quote_data['to'], 'amountIn': quote_data['amount'] }, headers={ 'Authorization': f'Bearer {token}', 'Content-Type': 'application/json' } ) return response.json() # Usage quote = get_quote(token, org_id, merchant_id, { 'order_type': 'BALANCE_TRANSFER', 'from': 'COP', 'to': 'USD', 'amount': 1000000 }) print(f"Exchange rate: {quote['exchangeRate']}") print(f"Will receive: {quote['finalAmountOut']} USD") ``` {/* cURL */} ```bash curl -X POST 'https://api-sandbox.koywe.com/api/v1/organizations/YOUR_ORG_ID/merchants/YOUR_MERCHANT_ID/quotes' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "orderType": "BALANCE_TRANSFER", "executable": true, "originCurrencySymbol": "COP", "destinationCurrencySymbol": "USD", "amountIn": 1000000 }' ``` **Response:** ```json { "id": "qte_abc123xyz", "orderType": "BALANCE_TRANSFER", "originCurrencySymbol": "COP", "destinationCurrencySymbol": "USD", "requestedAmountIn": 1000000, "finalAmountIn": 1000000, "finalAmountOut": 250, "exchangeRate": 4000, "fee": 5000, "taxes": 0, "validForSeconds": 300, "validUntil": "2025-11-13T15:05:00Z", "components": [ { "type": "FEE", "code": "PROCESSING_FEE", "name": "Processing Fee", "currency": "COP", "calculatedAmount": 5000 } ], "summary": { "totalBaseFeesInOriginCurrency": 5000, "totalTaxOnFeeInOriginCurrency": 0, "totalEffectiveFeeInOriginCurrency": 5000, "amountToBeConverted": 995000 } } ``` --- ## Quote Response Fields | Field | Type | Description | |-------|------|-------------| | `id` | string | Unique quote identifier (use in orders) | | `orderType` | string | Order type (PAYIN, BALANCE_TRANSFER, etc.) | | `originCurrencySymbol` | string | Source currency | | `destinationCurrencySymbol` | string | Target currency | | `requestedAmountIn` | number | Original requested input amount | | `requestedAmountOut` | number | Original requested output amount | | `finalAmountIn` | number | Total amount the user pays (origin currency) | | `finalAmountOut` | number | Total amount the user receives (destination currency) | | `exchangeRate` | number | Effective exchange rate applied | | `fee` | number | Total fees charged (base fees + tax on fees, in origin currency) | | `taxes` | number | Total taxes applied to the transaction amount | | `validForSeconds` | number | Seconds until expiration | | `validUntil` | string | ISO timestamp of expiration | | `components` | array | Detailed breakdown of fees and taxes | | `summary` | object | Summary of financial calculations | --- ## Using Quotes in Orders ### Rate Locking When you create an order with a `quoteId`, the exchange rate is locked: {/* Multi-language code examples */} {/* Node.js */} ```javascript async function createOrderWithQuote(token, orgId, merchantId) { // 1. Get quote const quote = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/quotes`, { orderType: 'BALANCE_TRANSFER', executable: true, originCurrencySymbol: 'COP', destinationCurrencySymbol: 'USD', amountIn: 1000000 }, { headers: { 'Authorization': `Bearer ${token}` } } ); console.log('Rate locked at:', quote.data.exchangeRate); console.log('Valid for:', quote.data.validForSeconds, 'seconds'); // 2. Create order with quote (must be within validFor period) const order = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/orders`, { type: 'BALANCE_TRANSFER', originCurrencySymbol: 'COP', destinationCurrencySymbol: 'USD', amountIn: 1000000, quoteId: quote.data.id // Lock the rate from quote }, { headers: { 'Authorization': `Bearer ${token}` } } ); console.log('Order created with locked rate'); return order.data; } ``` **Quote Expiration**: Quotes are valid for a limited time (10-15 seconds). If the quote expires before you create the order, you'll need to get a new quote. --- ## Quote Types by Order Type ### PAYIN Quotes For accepting payments (typically same currency, so rate = 1): ```javascript { "orderType": "PAYIN", "executable": true, "originCurrencySymbol": "COP", "destinationCurrencySymbol": "COP", "amountIn": 50000 } ``` **Response**: ```json { "exchangeRate": 1, "finalAmountIn": 50000, "finalAmountOut": 50000, "fee": 500, "taxes": 0 } ``` ### BALANCE_TRANSFER Quotes For currency conversion: ```javascript { "orderType": "BALANCE_TRANSFER", "executable": true, "originCurrencySymbol": "COP", "destinationCurrencySymbol": "USD", "amountIn": 1000000 } ``` **Response**: ```json { "exchangeRate": 4000, // 4,000 COP = 1 USD "finalAmountOut": 250, // Will receive 250 USD "fee": 5000, // 5,000 COP total fees "taxes": 0 } ``` ### ONRAMP Quotes For buying crypto: **Network Required**: ONRAMP quotes require a `network` parameter to specify the blockchain network for the crypto asset. ```javascript { "orderType": "ONRAMP", "executable": true, "originCurrencySymbol": "COP", "destinationCurrencySymbol": "USDC", "amountIn": 50000, "network": "ETHEREUM" // Required for ONRAMP/OFFRAMP } ``` **Supported networks**: | Network | Chain | Currencies | | --- | --- | --- | | `ETHEREUM` | Ethereum | `ETH` Β· `USDC` Β· `USDT` | | `POLYGON` | Polygon | `MATIC` Β· `USDC` Β· `USDT` | | `SOLANA` | Solana | `SOL` Β· `USDC` Β· `USDT` | | `BASE` | Base | `ETH` Β· `USDC` Β· `USDT` Β· `EURC` | | `ALGORAND` | Algorand | `USDC` Β· `USDT` | | `TRON` | Tron | `TRX` Β· `USDT` | | `BSC` | BNB Smart Chain | `USDT` | | `BITCOIN` | Bitcoin | `BTC` | Only these pairs are valid. Any other symbol/network combination is rejected by the API. **Response**: ```json { "exchangeRate": 4050, // Includes crypto price "finalAmountOut": 12.34, // Will receive 12.34 USDC "fee": 2500, // Total fees (includes network fee) "taxes": 0 } ``` ### OFFRAMP Quotes For selling crypto: **Network Required**: OFFRAMP quotes require a `network` parameter to specify the blockchain network for the crypto asset. ```javascript { "orderType": "OFFRAMP", "executable": true, "originCurrencySymbol": "USDC", "destinationCurrencySymbol": "COP", "amountIn": 10, // 10 USDC "network": "ETHEREUM" // Required for ONRAMP/OFFRAMP } ``` **Response**: ```json { "exchangeRate": 3950, // Slightly lower than buy rate (spread) "finalAmountOut": 39500, // Will receive 39,500 COP "fee": 500, // Total fees "taxes": 0 } ``` --- ## Understanding Fees ### Fee Breakdown B[- Fees] B --> C[- Taxes] C --> D[Γ— Exchange Rate] D --> E[Amount Out]`} /> **Example calculation**: ``` Amount In (finalAmountIn): 1,000,000 COP - Fee: -5,000 COP - Taxes: 0 COP = Net Amount: 995,000 COP Γ· Exchange Rate: Γ·4,000 COP/USD = Amount Out (finalAmountOut): 248.75 USD ``` The `components` array in the response provides the detailed breakdown of each fee and tax applied. The `summary` object provides totals. ### Fee Types | Fee Type | When Applied | Typical Range | |----------|--------------|---------------| | **Processing Fee** | All transactions | 0.5% - 2% | | **Network Fee** | Crypto transactions (ONRAMP/OFFRAMP) | Variable (blockchain fees) | | **Tax on Amount** | Country-specific (e.g., VAT) | Varies by country | | **Tax on Fee** | Country-specific | Varies by country | --- ## Showing Rates to Users ### User-Friendly Display {/* Multi-language code examples */} {/* Node.js */} ```javascript async function displayQuoteToUser(token, orgId, merchantId, amount) { const quote = await getQuote(token, orgId, merchantId, { orderType: 'BALANCE_TRANSFER', from: 'COP', to: 'USD', amount: amount }); // Format for user display const display = { amountToSend: `${amount.toLocaleString()} COP`, willReceive: `${quote.finalAmountOut.toFixed(2)} USD`, exchangeRate: `1 USD = ${quote.exchangeRate.toLocaleString()} COP`, totalFees: `${quote.fee.toLocaleString()} COP`, expiresIn: `${Math.floor(quote.validForSeconds / 60)} minutes`, effectiveRate: (amount / quote.finalAmountOut).toFixed(2) + ' COP/USD' }; console.log('You send:', display.amountToSend); console.log('They receive:', display.willReceive); console.log('Exchange rate:', display.exchangeRate); console.log('Total fees:', display.totalFees); console.log('Rate expires in:', display.expiresIn); console.log('Effective rate:', display.effectiveRate); return quote; } // Usage await displayQuoteToUser(token, orgId, merchantId, 1000000); ``` **Output:** ``` You send: 1,000,000 COP They receive: 248.75 USD Exchange rate: 1 USD = 4,000 COP Total fees: 5,000 COP Rate expires in: 5 minutes Effective rate: 4020.00 COP/USD ``` --- ## Retrieving a Quote Get details of an existing quote: {/* Multi-language code examples */} {/* Node.js */} ```javascript async function getQuoteById(token, orgId, merchantId, quoteId) { const response = await axios.get( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/quotes/${quoteId}`, { headers: { 'Authorization': `Bearer ${token}` } } ); return response.data; } // Usage const quote = await getQuoteById(token, orgId, merchantId, 'qte_abc123'); console.log('Quote status:', quote); ``` --- ## Best Practices ### When to Get a Quote **Always get a quote for**: - Currency conversions (BALANCE_TRANSFER) - Crypto operations (ONRAMP/OFFRAMP) - Showing rate to users before they commit ### Rate Display **Show users**: - Exchange rate in familiar terms (1 USD = X COP) - Total fees clearly separated - Effective rate (including fees) - Expiration time ### Handling Expiration {/* Multi-language code examples */} {/* Node.js */} ```javascript async function createOrderWithAutoRetry(token, orgId, merchantId, orderData) { let quote = await getQuote(token, orgId, merchantId, { orderType: orderData.orderType, from: orderData.from, to: orderData.to, amount: orderData.amount }); try { // Try to create order with quote return await createOrderWithQuote(token, orgId, merchantId, quote.id); } catch (error) { if (error.message.includes('expired') || error.message.includes('invalid quote')) { // Quote expired, get new quote and retry console.log('Quote expired, getting new quote...'); quote = await getQuote(token, orgId, merchantId, { orderType: orderData.orderType, from: orderData.from, to: orderData.to, amount: orderData.amount }); return await createOrderWithQuote(token, orgId, merchantId, quote.id); } throw error; } } ``` --- ## Quote vs No Quote ### With Quote ```javascript // 1. Get quote const quote = await getQuote(...); // 2. Show user the rate console.log('Rate:', quote.exchangeRate); // 3. User confirms // 4. Create order with locked rate const order = await createOrder({ ...orderData, quoteId: quote.id }); ``` **Benefits**: - Rate locked - User knows exactly what they'll receive - Better transparency ### Without Quote ```javascript // Create order directly (uses current rate) const order = await createOrder({ type: 'BALANCE_TRANSFER', originCurrencySymbol: 'COP', destinationCurrencySymbol: 'USD', amountIn: 1000000 // No quoteId - uses current rate }); ``` **Considerations**: - Rate determined at order creation time - Small rate fluctuation possible - Faster (one less API call) --- ## Common Scenarios ### Scenario 1: Show Rate Before Payment ```javascript // User views checkout page const quote = await getQuote(token, orgId, merchantId, { orderType: 'PAYIN', from: 'COP', to: 'COP', amount: 50000 }); // Display to user console.log(`Pay ${quote.finalAmountIn} COP`); console.log(`Fee: ${quote.fee} COP`); console.log(`Total: ${quote.finalAmountIn} COP`); // finalAmountIn already includes fees // User clicks "Pay" const order = await createOrder({ ...orderData, quoteId: quote.id }); ``` ### Scenario 2: Currency Converter Tool ```javascript // Real-time currency converter async function convertCurrency(amount, from, to) { const quote = await getQuote(token, orgId, merchantId, { orderType: 'BALANCE_TRANSFER', from: from, to: to, amount: amount }); return { from: `${amount} ${from}`, to: `${quote.finalAmountOut} ${to}`, rate: `1 ${to} = ${quote.exchangeRate} ${from}`, fee: `${quote.fee} ${from}` }; } // Usage const result = await convertCurrency(1000000, 'COP', 'USD'); console.log(result); // { from: "1000000 COP", to: "248.75 USD", rate: "1 USD = 4000 COP", fee: "5000 COP" } ``` --- ## Next Steps Learn how to create orders with quotes Complete guide to currency conversion Using quotes for crypto purchases Full quotes API documentation --- # Supported Corridors & Currencies _Where Koywe settles, in which currency, over which local rail_ Source: https://docs.koywe.com/en/core-concepts/supported-corridors # Supported Corridors & Currencies A **corridor** is a country plus a currency plus a direction: COP payins in Colombia, BOB payouts in Bolivia, MXN in both directions in Mexico. Each corridor runs over a specific local rail, and the rail is what determines how fast funds settle and which fields an order needs. This page is the coverage reference. For how to actually build against a corridor, see [Accepting Payments](/en/accepting-payments) for payins and [Paying Providers](/en/paying-providers) for payouts. --- ## Coverage by Country | Country | Currency | Direction | Local rail | Notes | | --- | --- | --- | --- | --- | | πŸ‡¦πŸ‡· Argentina | `ARS` | `PAYIN` Β· `PAYOUT` | Named CVU (virtual account) | Named accounts only, in both directions. | | πŸ‡§πŸ‡΄ Bolivia | `BOB` | `PAYIN` | QR | Wire payin in development. | | πŸ‡§πŸ‡΄ Bolivia | `BOB` | `PAYOUT` | Bank transfer | All banks supported. If a destination bank is missing from our catalogue we add it. | | πŸ‡§πŸ‡· Brazil | `BRL` | `PAYIN` Β· `PAYOUT` | PIX | Settles in seconds. | | πŸ‡¨πŸ‡± Chile | `CLP` | `PAYIN` | Instant bank transfer | Per-merchant virtual accounts in development. | | πŸ‡¨πŸ‡± Chile | `CLP` | `PAYOUT` | Bulk payment file | Effectively instant. Batched, and split into parts for transactions above CLP 7,000,000. | | πŸ‡¨πŸ‡΄ Colombia | `COP` | `PAYIN` | PSE | β€” | | πŸ‡¨πŸ‡΄ Colombia | `COP` | `PAYOUT` | ACH Colombia, Bre-B, A2A | ACH is cycle-based; Bre-B settles in seconds. A2A in development (instant to some banks). | | πŸ‡²πŸ‡½ Mexico | `MXN` | `PAYIN` Β· `PAYOUT` | SPEI via virtual accounts | β€” | | πŸ‡΅πŸ‡ͺ Peru | `PEN` | `PAYIN` | Virtual CCI for deposits; wire transfer or recaudo | β€” | | πŸ‡΅πŸ‡ͺ Peru | `USD` | `PAYIN` | Wire transfer or recaudo | β€” | | πŸ‡΅πŸ‡ͺ Peru | `PEN` Β· `USD` | `PAYOUT` | Wire transfer | β€” | | πŸ‡ΊπŸ‡Έ United States | `USD` | `PAYIN` Β· `PAYOUT` | Named accounts; payouts by wire or ACH | Requires separate KYB/KYC onboarding. | | πŸ‡»πŸ‡ͺ Venezuela | `VES` | `PAYIN` Β· `PAYOUT` | Bank transfer | Requires additional compliance checks. | | 🌐 Cross-border | Multiple | `PAYOUT` | Circle Payments Network / Tether T-0 β€” stablecoin settlement between institutions, local fiat delivered at destination | Live in Hong Kong, Singapore, the Philippines, India, Thailand, Japan and European markets. | Coverage as of the last update to this page. The live list of payin methods per country and currency is served by GET /api/v1/payment-method. **Direction** uses the same vocabulary as the API: `PAYIN` is money coming in from a payer, `PAYOUT` is money going out to a bank account. See [Orders & Order Types](/en/core-concepts/orders-and-order-types). --- ## Coverage by Currency The same coverage, framed by currency. These symbols are what you pass to `originCurrencySymbol`, `destinationCurrencySymbol` and `currencySymbol` in the API. | Currency | Name | Corridors | | --- | --- | --- | | `ARS` | Argentine peso | πŸ‡¦πŸ‡· Argentina (`PAYIN` Β· `PAYOUT`) | | `BOB` | Boliviano | πŸ‡§πŸ‡΄ Bolivia (`PAYIN` Β· `PAYOUT`) | | `BRL` | Brazilian real | πŸ‡§πŸ‡· Brazil (`PAYIN` Β· `PAYOUT`) | | `CLP` | Chilean peso | πŸ‡¨πŸ‡± Chile (`PAYIN` Β· `PAYOUT`) | | `COP` | Colombian peso | πŸ‡¨πŸ‡΄ Colombia (`PAYIN` Β· `PAYOUT`) | | `MXN` | Mexican peso | πŸ‡²πŸ‡½ Mexico (`PAYIN` Β· `PAYOUT`) | | `PEN` | Peruvian sol | πŸ‡΅πŸ‡ͺ Peru (`PAYIN` Β· `PAYOUT`) | | `USD` | US dollar | πŸ‡΅πŸ‡ͺ Peru (`PAYIN` Β· `PAYOUT`) Β· πŸ‡ΊπŸ‡Έ United States (`PAYIN` Β· `PAYOUT`) | | `VES` | Venezuelan bolΓ­var | πŸ‡»πŸ‡ͺ Venezuela (`PAYIN` Β· `PAYOUT`) | Coverage as of the last update to this page. The live list of payin methods per country and currency is served by GET /api/v1/payment-method. **Crypto currencies** are documented separately, since support there is defined by symbol *and* network β€” see [Supported Networks](/en/crypto-operations/onramp#supported-networks) for the valid pairs. --- ## Checking Coverage at Runtime The table above is the commercial view and is updated by hand. The API is the live source for which **payin methods** a corridor exposes right now, including per-method amount limits: ```bash curl -X GET 'https://api-sandbox.koywe.com/api/v1/payment-method?countrySymbol=CO¤cySymbol=COP' ``` This endpoint is public β€” no authentication required. With the CLI: ```bash koywe payment-methods list --country-symbol CO --currency-symbol COP ``` Each method comes back with its `method` code, `responseType` (`QR`, `PAYMENT_LINK`, `PUSH_NOTIFICATION` or `BANK_TRANSFER`), `requiredFields` and `minAmount` / `maxAmount`. Build your checkout from that response rather than hardcoding a method list β€” new rails appear without an API change. [Payment method details by country β†’](/en/accepting-payments/payment-methods) --- ## Reading the Table ### Named accounts In Argentina and the United States, funds move through accounts held in the payer's or beneficiary's own name. The account holder's identity has to match, in both directions β€” an unnamed or third-party account is rejected by the rail, not by Koywe. ### Virtual accounts Argentina (CVU), Mexico (SPEI) and Peru (CCI) issue an account number per merchant that payers deposit into, so a payin arrives already attributed. See [Virtual Accounts](/en/core-concepts/virtual-accounts) and [Deposit Accounts](/en/balance-management/deposit-accounts). ### Batched rails Two corridors are not one-transfer-per-order: - **Chile payouts** go out in a bulk payment file. Settlement is effectively instant, but a single transaction above CLP 7,000,000 is split into parts. - **Colombia ACH** runs on settlement cycles rather than continuously. Bre-B, on the same corridor, settles in seconds. ### Cross-border settlement Payouts outside Latin America settle between institutions in stablecoin over the Circle Payments Network or Tether T-0, with local fiat delivered at the destination. Live markets are listed in the table above. This is an institutional corridor β€” talk to your Koywe contact before designing against it. ### In development Rails marked *in development* in the table are not callable yet. Do not build against them until they appear in the `GET /payment-method` response for that corridor. --- ## Compliance Notes **United States** requires a separate KYB/KYC onboarding from your Latin American merchants, and **Venezuela** requires additional compliance checks before a corridor is enabled. Neither is available by default β€” contact Koywe to have them enabled on your organization. See [Onboarding & KYB](/en/getting-started/onboarding-kyb) for what the process needs. --- ## Next Steps Build a payin against any corridor above Send payouts to local bank accounts Method-by-method detail per country The fields each country's rail requires --- # Virtual Accounts _Managing multi-currency balances_ Source: https://docs.koywe.com/en/core-concepts/virtual-accounts # Virtual Accounts Virtual Accounts are multi-currency balance accounts that hold your funds within the Koywe system, enabling seamless payment operations without traditional bank accounts. ## What are Virtual Accounts? A **Virtual Account** is a balance account in a specific currency, similar to a bank account but existing within the Koywe platform. ### Key Characteristics - **One per currency per merchant**: Each merchant has a virtual account for each currency they work with - **Automatically created**: Generated when you create a merchant - **Real-time balances**: Instant updates as transactions occur - **No bank account needed**: Hold funds without opening multiple bank accounts - **Used for all operations**: Source and destination for PAYINs, PAYOUTs, and transfers --- ## How Virtual Accounts Work PAYIN] -->|Credits| B[Virtual Account COP] B -->|PAYOUT| C[Provider Bank] B -->|BALANCE_TRANSFER| D[Virtual Account USD] B -->|ONRAMP| E[Buy Crypto] F[Sell CryptoOFFRAMP] -->|Credits| B style B fill:#005544,stroke:#C9FF1F,stroke-width:3px,color:#fff`} /> **Virtual accounts serve as**: - **Receiving accounts** for customer payments (PAYIN) - **Holding accounts** for funds in multiple currencies - **Source accounts** for provider payments (PAYOUT) - **Transfer endpoints** for currency exchanges (BALANCE_TRANSFER) - **Funding source** for crypto purchases (ONRAMP) - **Destination** for crypto sales (OFFRAMP) --- ## Balance Types Each virtual account tracks three types of balances: ### 1. Available Balance The amount immediately available for use. ```javascript { "availableBalance": 1000000, // 1,000,000 COP available "currencySymbol": "COP" } ``` **Can be used for**: - Creating PAYOUT orders - BALANCE_TRANSFER to other currencies - ONRAMP crypto purchases ### 2. Pending Balance Funds that are being processed but not yet available. ```javascript { "pendingBalance": 50000, // 50,000 COP pending "currencySymbol": "COP" } ``` **Examples**: - Customer payment being confirmed (PAYIN in PROCESSING status) - Crypto sale being settled (OFFRAMP in PROCESSING) ### 3. Reserved Balance Funds temporarily locked for ongoing operations. ```javascript { "reservedBalance": 25000, // 25,000 COP reserved "currencySymbol": "COP" } ``` **Examples**: - PAYOUT order in progress - BALANCE_TRANSFER being executed --- ## Checking Balances ### Get All Balances for a Merchant {/* Multi-language code examples */} {/* Node.js */} ```javascript async function getMerchantBalances(token, orgId, merchantId) { const response = await axios.get( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/accounts/balances`, { headers: { 'Authorization': `Bearer ${token}` } } ); return response.data; } // Usage const balances = await getMerchantBalances(token, orgId, merchantId); balances.forEach(balance => { console.log(`${balance.currencySymbol}: ${balance.availableBalance} available`); }); ``` {/* Python */} ```python def get_merchant_balances(token, org_id, merchant_id): response = requests.get( f'https://api-sandbox.koywe.com/api/v1/organizations/{org_id}/merchants/{merchant_id}/accounts/balances', headers={'Authorization': f'Bearer {token}'} ) return response.json() # Usage balances = get_merchant_balances(token, org_id, merchant_id) for balance in balances: print(f"{balance['currencySymbol']}: {balance['availableBalance']} available") ``` {/* cURL */} ```bash curl -X GET 'https://api-sandbox.koywe.com/api/v1/organizations/YOUR_ORG_ID/merchants/YOUR_MERCHANT_ID/accounts/balances' \ -H 'Authorization: Bearer YOUR_TOKEN' ``` **Response:** ```json [ { "id": "va_cop_12345", "currencySymbol": "COP", "availableBalance": 1000000, "pendingBalance": 50000, "reservedBalance": 25000, "totalBalance": 1075000, "merchantId": "mrc_xyz789" }, { "id": "va_usd_67890", "currencySymbol": "USD", "availableBalance": 500, "pendingBalance": 0, "reservedBalance": 0, "totalBalance": 500, "merchantId": "mrc_xyz789" } ] ``` ### Get Balance for Specific Currency {/* Multi-language code examples */} {/* Node.js */} ```javascript async function getBalanceForCurrency(token, orgId, merchantId, currencySymbol) { const balances = await getMerchantBalances(token, orgId, merchantId); return balances.find(b => b.currencySymbol === currencySymbol); } // Usage const copBalance = await getBalanceForCurrency(token, orgId, merchantId, 'COP'); console.log('COP available:', copBalance.availableBalance); ``` --- ## Balance Operations ### Crediting (Adding Funds) Funds are **automatically credited** to virtual accounts through: 1. **PAYIN orders** (customer payments) ```javascript // Customer pays 50,000 COP // Virtual Account COP: +50,000 ``` 2. **OFFRAMP orders** (selling crypto) ```javascript // Sell 10 USDC for COP // Virtual Account COP: +8,000 (example rate) ``` ### Debiting (Removing Funds) Funds are **automatically debited** from virtual accounts through: 1. **PAYOUT orders** (provider payments) ```javascript // Pay provider 100,000 COP // Virtual Account COP: -100,000 ``` 2. **BALANCE_TRANSFER orders** (currency exchange) ```javascript // Convert 1,000,000 COP to USD // Virtual Account COP: -1,000,000 // Virtual Account USD: +1,250 (example rate) ``` 3. **ONRAMP orders** (buying crypto) ```javascript // Buy 10 USDC with COP // Virtual Account COP: -8,000 (example rate) ``` --- ## Balance Validation ### Before Creating PAYOUT Orders Always check available balance before creating a PAYOUT: {/* Multi-language code examples */} {/* Node.js */} ```javascript async function safeCreatePayout(token, orgId, merchantId, payoutAmount, currency) { // 1. Get current balance const balance = await getBalanceForCurrency(token, orgId, merchantId, currency); // 2. Validate sufficient funds if (balance.availableBalance < payoutAmount) { throw new Error( `Insufficient balance: ${balance.availableBalance} ${currency} available, ` + `${payoutAmount} ${currency} required` ); } // 3. Create payout order const order = await createPayoutOrder(token, orgId, merchantId, { amount: payoutAmount, currency: currency, // ... other fields }); return order; } // Usage try { const payout = await safeCreatePayout(token, orgId, merchantId, 100000, 'COP'); console.log('Payout created:', payout.id); } catch (error) { console.error('Error:', error.message); } ``` --- ## Fund Settlement and Long-Term Holdings **Automatic Settlement**: Funds held in virtual accounts cannot remain indefinitely. After a certain number of days, balances are **automatically settled** to your merchant's registered bank account. ### Settlement Behavior Virtual accounts are designed for **active payment operations**, not long-term fund storage: |Funds remain| B{Days in Account} B -->|< Settlement Period| C[Available for Operations] B -->|β‰₯ Settlement Period| D[Auto-settled to Bank] D --> E[Merchant's Bank Account] style D fill:#ff6b6b,stroke:#c92a2a,stroke-width:2px,color:#fff style E fill:#51cf66,stroke:#2f9e44,stroke-width:2px,color:#fff`} /> **What happens during settlement:** - Funds are transferred to your merchant's registered bank account - Virtual account balance returns to zero - You receive a notification about the settlement - Transaction history is maintained for reconciliation ### Alternative: Digital Dollar Holdings (USDC) **Hold Funds Long-Term**: Instead of waiting for automatic settlement, you can **convert your fiat balance to USDC** (digital dollars) and hold funds in embedded crypto wallets indefinitely. **Benefits of USDC Holdings:** - βœ… **No automatic settlement** - hold funds as long as needed - βœ… **Stable value** - pegged 1:1 to US Dollar - βœ… **Instant liquidity** - convert back to fiat anytime - βœ… **Lower fees** - blockchain-based transfers - βœ… **Global access** - use across borders ### Converting Fiat to USDC {/* Multi-language code examples */} {/* Node.js */} ```javascript async function convertToUSDC(token, orgId, merchantId, amount, currency) { // 1. Get quote for ONRAMP (fiat to crypto) const quote = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/quotes`, { orderType: 'ONRAMP', executable: true, originCurrencySymbol: currency, destinationCurrencySymbol: 'USDC', amountIn: amount, network: 'ETHEREUM' // Required for ONRAMP/OFFRAMP }, { headers: { 'Authorization': `Bearer ${token}` } } ); console.log(`Converting ${amount} ${currency} β†’ ${quote.data.finalAmountOut} USDC`); // 2. Create deal (not order!) const deal = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/deals`, { // Id of your CRYPTO account in K3 β€” not a wallet address destinationAccountId: 'acc_77a80cf9-496c-4d72-9c33-fca3c8d5bcfe', quoteId: quote.data.id }, { headers: { 'Authorization': `Bearer ${token}` } } ); return deal.data; } // Usage: Convert COP balance to USDC const usdcDeal = await convertToUSDC(token, orgId, merchantId, 1000000, 'COP'); console.log('Deal created:', usdcDeal.id); console.log('Funds will be held as USDC in account:', usdcDeal.destinationAccountId); ``` {/* Python */} ```python def convert_to_usdc(token, org_id, merchant_id, amount, currency): # Get quote quote_response = requests.post( f'https://api-sandbox.koywe.com/api/v1/organizations/{org_id}/merchants/{merchant_id}/quotes', json={ 'orderType': 'ONRAMP', 'executable': True, 'originCurrencySymbol': currency, 'destinationCurrencySymbol': 'USDC', 'amountIn': amount, 'network': 'ETHEREUM' # Required for ONRAMP/OFFRAMP }, headers={'Authorization': f'Bearer {token}'} ) quote = quote_response.json() print(f"Converting {amount} {currency} β†’ {quote['finalAmountOut']} USDC") # Create deal (not order!) deal_response = requests.post( f'https://api-sandbox.koywe.com/api/v1/organizations/{org_id}/merchants/{merchant_id}/deals', json={ # Id of your CRYPTO account in K3 β€” not a wallet address 'destinationAccountId': 'acc_77a80cf9-496c-4d72-9c33-fca3c8d5bcfe', 'quoteId': quote['id'] }, headers={'Authorization': f'Bearer {token}'} ) return deal_response.json() # Usage usdc_deal = convert_to_usdc(token, org_id, merchant_id, 1000000, 'COP') print(f"Deal created: {usdc_deal['id']}") print(f"Funds will be held as USDC in account: {usdc_deal['destinationAccountId']}") ``` ### Converting USDC Back to Fiat When you need the funds back in fiat: {/* Multi-language code examples */} {/* Node.js */} ```javascript async function convertUSDCToFiat(token, orgId, merchantId, usdcAmount, targetCurrency) { // 1. Get quote for OFFRAMP (crypto to fiat) const quote = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/quotes`, { orderType: 'OFFRAMP', executable: true, originCurrencySymbol: 'USDC', destinationCurrencySymbol: targetCurrency, amountIn: usdcAmount, network: 'ETHEREUM' // Required for ONRAMP/OFFRAMP }, { headers: { 'Authorization': `Bearer ${token}` } } ); // 2. Create deal (must be paid completely) const deal = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/deals`, { destinationAccountId: 'va_cop_12345', // Target virtual account quoteId: quote.data.id }, { headers: { 'Authorization': `Bearer ${token}` } } ); return deal.data; } // Usage: Convert USDC back to COP const copDeal = await convertUSDCToFiat(token, orgId, merchantId, 250, 'COP'); console.log('Deal created:', copDeal.id); console.log('USDC will be converted to COP in virtual account'); ``` **Recommended Strategy**: - Keep **operational funds** in virtual accounts for daily PAYIN/PAYOUT operations - Convert **excess balances** to USDC for long-term holdings - Convert USDC back to fiat when you need to make large payouts or withdrawals ### Fund Management Decision Tree B{Need funds withinsettlement period?} B -->|Yes| C[Keep in Virtual Account] C --> D[Use for PAYOUTs/Operations] B -->|No| E{Want to holdlong-term?} E -->|Yes| F[Convert to USDC] F --> G[Hold in Embedded Wallet] G --> H[Convert back when needed] E -->|No| I[Let auto-settle to bank] I --> J[Funds in Bank Account] style F fill:#005544,stroke:#C9FF1F,stroke-width:2px,color:#fff style G fill:#005544,stroke:#C9FF1F,stroke-width:2px,color:#fff style D fill:#4a90e2,stroke:#2e5f8a,stroke-width:2px,color:#fff`} /> [Learn more about ONRAMP (Buying Crypto) β†’](/en/crypto-operations/onramp) [Learn more about OFFRAMP (Selling Crypto) β†’](/en/crypto-operations/offramp) --- ## Multi-Currency Management ### Supported Currencies Virtual accounts are automatically created for: | Currency | Symbol | Region | |----------|--------|--------| | Colombian Peso | COP | Colombia | | Brazilian Real | BRL | Brazil | | Mexican Peso | MXN | Mexico | | Chilean Peso | CLP | Chile | | Argentine Peso | ARS | Argentina | | Peruvian Sol | PEN | Peru | | US Dollar | USD | International | | Euro | EUR | International | ### Currency Exchange via BALANCE_TRANSFER Transfer funds between currencies instantly: >API: Get Quote (COP β†’ USD) API-->>App: Exchange Rate: 4,000 App->>API: Create BALANCE_TRANSFER Order API->>VA_COP: Debit 1,000,000 COP API->>VA_USD: Credit 250 USD API->>App: Order Completed`} /> {/* Multi-language code examples */} {/* Node.js */} ```javascript async function transferBetweenCurrencies(token, orgId, merchantId) { // 1. Get quote const quote = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/quotes`, { orderType: 'BALANCE_TRANSFER', executable: true, originCurrencySymbol: 'COP', destinationCurrencySymbol: 'USD', amountIn: 1000000 // 1,000,000 COP }, { headers: { 'Authorization': `Bearer ${token}` } } ); console.log('Exchange rate:', quote.data.exchangeRate); console.log('Will receive:', quote.data.finalAmountOut, 'USD'); // 2. Create transfer order const order = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/orders`, { type: 'BALANCE_TRANSFER', originCurrencySymbol: 'COP', destinationCurrencySymbol: 'USD', amountIn: 1000000, quoteId: quote.data.id }, { headers: { 'Authorization': `Bearer ${token}` } } ); return order.data; } ``` [Learn more about BALANCE_TRANSFER β†’](/en/balance-management) --- ## Balance History and Reconciliation ### Tracking Balance Changes Monitor balance changes through order history: {/* Multi-language code examples */} {/* Node.js */} ```javascript async function getBalanceHistory(token, orgId, merchantId, startDate, endDate) { const response = await axios.get( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/orders`, { params: { startDate: startDate, endDate: endDate, limit: 100 }, headers: { 'Authorization': `Bearer ${token}` } } ); // Filter orders that affect balance return response.data.filter(order => ['PAYIN', 'PAYOUT', 'BALANCE_TRANSFER', 'ONRAMP', 'OFFRAMP'].includes(order.type) && order.status === 'COMPLETED' ); } // Usage const history = await getBalanceHistory( token, orgId, merchantId, '2025-01-01', '2025-01-31' ); let totalIn = 0; let totalOut = 0; history.forEach(order => { if (['PAYIN', 'OFFRAMP'].includes(order.type)) { totalIn += order.amountOut; } else if (['PAYOUT', 'BALANCE_TRANSFER', 'ONRAMP'].includes(order.type)) { totalOut += order.amountIn; } }); console.log('Total IN:', totalIn); console.log('Total OUT:', totalOut); console.log('Net:', totalIn - totalOut); ``` --- ## Best Practices ### Balance Management **Do:** - Check balances before creating PAYOUT orders - Monitor pending balances for incoming payments - Set up alerts for low balances - Reconcile regularly with your accounting system **Don't:** - Assume instant availability of PAYIN funds (check status) - Create PAYOUT orders exceeding available balance - Ignore reserved balances in your calculations ### Currency Strategy **Multi-Currency Operations**: Keep balances in the currencies you operate in most frequently to minimize conversion fees and exchange rate exposure. **Example strategy**: - Operate mainly in Colombia β†’ Keep most funds in COP - Pay international providers β†’ Keep some USD balance - Occasional Brazilian sales β†’ Convert BRL to COP as needed --- ## Common Scenarios ### Scenario 1: Customer Payment Flow ``` 1. Customer initiates payment: 50,000 COP 2. PAYIN order created: Status PENDING 3. Virtual Account COP: pending +50,000 4. Customer completes payment 5. Order status: PAID 6. Virtual Account COP: available +50,000 7. Your application receives webhook: order.completed ``` ### Scenario 2: Provider Payment Flow ``` 1. Check Virtual Account USD: 500 available 2. Create PAYOUT: 100 USD to provider 3. Virtual Account USD: reserved +100, available -100 4. Bank transfer initiated 5. Bank confirms transfer 6. Order status: COMPLETED 7. Virtual Account USD: reserved -100 (now only 400 total) ``` ### Scenario 3: Currency Exchange ``` 1. Virtual Account COP: 1,000,000 available 2. Virtual Account USD: 0 available 3. Create BALANCE_TRANSFER: 1,000,000 COP β†’ USD 4. Exchange rate: 4,000 COP per USD 5. Virtual Account COP: -1,000,000 6. Virtual Account USD: +250 7. Order completes instantly ``` --- ## Next Steps Learn how orders affect virtual account balances Complete guide to currency exchange Learn how to credit your virtual accounts Learn how to use virtual account balances for payouts --- # Accepting Payments - Overview _Understanding PAYIN orders and payment acceptance_ Source: https://docs.koywe.com/en/accepting-payments # Accepting Payments (PAYIN) Learn how to accept payments from customers using local payment methods across Latin America. ## What is PAYIN? **PAYIN** is the order type used to accept payments from customers into your virtual balance account. |Pays| B[Payment Provider] B -->|Confirms| C[Koywe] C -->|Credits| D[Your Virtual Account] C -->|Notifies| E[Your Application] style D fill:#005544,stroke:#C9FF1F,stroke-width:2px,color:#fff`} /> --- ## Use Cases Accept payments for online purchases at checkout Collect payments for services rendered Recurring payments for subscriptions Accept payments for issued invoices --- ## How It Works ### High-Level Flow ### Customer initiates checkout User adds items to cart and proceeds to payment ### Your app creates PAYIN order Send order details to Koywe API ### Koywe returns payment URL Receive a checkout URL for the customer ### Customer completes payment Customer is redirected to payment page and pays using their preferred method ### Koywe confirms payment Payment provider confirms and Koywe credits your virtual account ### Your app receives webhook You're notified via webhook to fulfill the order ### Detailed Flow Diagram >A: 1. Initiates checkout A->>K: 2. Create PAYIN order K-->>A: 3. Return payment URL A->>C: 4. Redirect to payment URL C->>P: 5. Complete payment (PSE/PIX/etc) P->>K: 6. Confirm payment K->>V: 7. Credit funds K->>A: 8. Webhook: order.paid K->>A: 9. Webhook: order.completed A->>C: 10. Show success page`} /> --- ## Supported Countries and Payment Methods ### Colombia πŸ‡¨πŸ‡΄ | Method | Type | Settlement Time | |--------|------|----------------| | PSE | Bank transfer | Instant - 2 hours | | Nequi | Mobile wallet | Instant | [Learn more about Colombian payment methods β†’](/en/accepting-payments/payment-methods) ### Brazil πŸ‡§πŸ‡· | Method | Type | Settlement Time | |--------|------|----------------| | PIX | Instant payment | Instant | ### Mexico πŸ‡²πŸ‡½ | Method | Type | Settlement Time | |--------|------|----------------| | SPEI | Bank transfer | Instant ⚑ | ### Chile πŸ‡¨πŸ‡± | Method | Type | Settlement Time | |--------|------|----------------| | Khipu | Bank transfer | Instant - 2 hours | ### Argentina πŸ‡¦πŸ‡· | Method | Type | Settlement Time | |--------|------|----------------| | Various | Local methods | Varies | --- ## Prerequisites Before integrating PAYIN orders, ensure you have: **Required**: - βœ… API Key and Secret - βœ… Organization ID - βœ… Merchant ID - βœ… Understanding of [Core Concepts](/en/core-concepts) **Recommended**: - Webhook endpoint configured - Error handling implemented - Testing in sandbox environment --- ## Quick Start Want to jump right in? Follow our step-by-step integration guide: Detailed guide with code examples in cURL, Node.js, and Python Or try the quickstart for a faster overview: Create your first PAYIN order in minutes --- ## Payment Flow Explained ### Step-by-Step Process #### 1. Authentication Obtain an access token using your API credentials. ```javascript const token = await authenticate(apiKey, secret); ``` #### 2. Get Available Payment Methods Query which payment methods are available for the customer's country. ```javascript const methods = await getPaymentMethods('CO', 'COP'); // Returns: PSE, NEQUI, etc. ``` #### 3. Create Contact (Optional but Recommended) Store customer information for tracking and compliance. ```javascript const contact = await createContact({ firstName: 'Juan', lastName: 'PΓ©rez', countrySymbol: 'CO', businessType: 'PERSON', type: 'PERSON', email: 'customer@example.com' }); ``` #### 4. Create PAYIN Order Create the payment order with amount and payment method. ```javascript const order = await createPayinOrder({ amount: 50000, currency: 'COP', paymentMethod: 'PSE', contactId: contact.id }); ``` #### 5. Redirect Customer Send customer to the payment URL to complete payment. ```javascript window.location.href = order.providedAction; ``` #### 6. Handle Webhooks Listen for webhook events to know when payment is complete. ```javascript // Webhook: order.completed // -> Fulfill order, send confirmation email ``` --- ## Order Status Progression Understanding order statuses: PENDING: Order Created PENDING --> PROCESSING: Customer Paying PROCESSING --> PAID: Payment Confirmed PAID --> COMPLETED: Funds Credited PENDING --> EXPIRED: Time Limit Exceeded PENDING --> CANCELLED: User Cancelled PROCESSING --> FAILED: Payment Failed COMPLETED --> [*]: Success EXPIRED --> [*] CANCELLED --> [*] FAILED --> [*]`} /> | Status | Description | Next Action | |--------|-------------|-------------| | **PENDING** | Order created, waiting for customer | Customer needs to pay | | **PROCESSING** | Payment being processed | Wait for confirmation | | **PAID** | Payment confirmed | Funds being settled | | **COMPLETED** | Funds in your account | Fulfill order | | **FAILED** | Payment failed | Show error to customer | | **EXPIRED** | Payment window expired | Create new order | | **CANCELLED** | Order cancelled | No action needed | --- ## Key Concepts ### Payment URL Every PAYIN order returns a `providedAction`: ```json { "providedAction": "https://checkout.koywe.com/pay/ord_abc123" } ``` This URL: - Is hosted by Koywe - Provides a secure payment experience - Handles the payment method UI - Redirects back to your `successUrl` or `failedUrl` ### Success and Failed URLs Specify where to redirect customers after payment: ```javascript { "successUrl": "https://yoursite.com/payment/success", "failedUrl": "https://yoursite.com/payment/failed" } ``` **Best Practice**: Don't rely solely on these redirects for order fulfillment. Always use webhooks as the source of truth. ### External ID (Idempotency) Use `externalId` to link orders to your system and prevent duplicates: ```javascript { "externalId": "order-12345" // Your internal order ID } ``` **Benefits**: - Safe to retry failed API calls - Prevents duplicate charges - Easy reconciliation with your database --- ## Common Integration Patterns ### Pattern 1: Simple Checkout ```javascript // 1. User clicks "Pay" // 2. Create order const order = await createPayinOrder({ amount: cartTotal, currency: 'COP', paymentMethod: 'PSE' }); // 3. Redirect window.location.href = order.providedAction; // 4. Handle webhook // 5. Fulfill order ``` ### Pattern 2: Payment Method Selection ```javascript // 1. Show available methods const methods = await getPaymentMethods(country, currency); // 2. User selects method // 3. Create order with selected method const order = await createPayinOrder({ amount: cartTotal, currency: 'COP', paymentMethod: userSelectedMethod }); // 4. Redirect and handle webhook ``` ### Pattern 3: Stored Customer ```javascript // 1. Check if customer exists let contact = await findContact(email); if (!contact) { contact = await createContact(customerData); } // 2. Create order linked to contact const order = await createPayinOrder({ amount: cartTotal, contactId: contact.id, // ... other fields }); // 3. Track payment history per contact ``` --- ## Next Steps Step-by-step implementation with code examples Detailed guide to each payment method by country Common issues and how to resolve them Test payment flows in sandbox environment --- ## Additional Resources - [Core Concepts - Orders](/en/core-concepts/orders-and-order-types) - [Webhooks Deep Dive](/en/advanced/webhooks) - [Error Handling](/en/advanced/error-handling) - [API Reference](/api-reference) --- # Accepting Payments - Integration Guide _Complete step-by-step integration for production_ Source: https://docs.koywe.com/en/accepting-payments/integration-guide # Accepting Payments Integration Guide This comprehensive guide walks you through integrating payment acceptance into your application for production use. ## Prerequisites Before you begin: - [ ] API credentials (key, secret, organizationId, merchantId) - [ ] Webhook endpoint configured (recommended) - [ ] Test environment access - [ ] Understanding of [Core Concepts](/en/core-concepts) - [ ] Familiarity with [Order Types](/en/core-concepts/orders-and-order-types) --- ## Integration Overview >Koywe: 1. Get Payment Methods Koywe-->>Client: Available methods Client->>Koywe: 2. Create PAYIN Order Koywe-->>Client: Order + Payment URL Client->>Customer: 3. Redirect to Payment URL Customer->>Koywe: 4. Complete Payment Koywe->>Client: 5. Webhook: order.paid Koywe->>Client: 6. Webhook: order.completed`} /> --- ## Step 1: Authenticate Obtain an access token for all subsequent requests: {/* Multi-language code examples */} {/* cURL */} ```bash curl -X POST 'https://api-sandbox.koywe.com/api/v1/auth/sign-in' \ -H 'Content-Type: application/json' \ -d '{ "apiKey": "your_api_key", "secret": "your_secret" }' ``` {/* Node.js */} ```javascript const axios = require('axios'); async function authenticate() { const response = await axios.post( 'https://api-sandbox.koywe.com/api/v1/auth/sign-in', { apiKey: process.env.KOYWE_API_KEY, secret: process.env.KOYWE_SECRET } ); return response.data.token; } // Usage const token = await authenticate(); ``` {/* Python */} ```python def authenticate(): response = requests.post( 'https://api-sandbox.koywe.com/api/v1/auth/sign-in', json={ 'apiKey': os.environ['KOYWE_API_KEY'], 'secret': os.environ['KOYWE_SECRET'] } ) response.raise_for_status() return response.json()['token'] # Usage token = authenticate() ``` **Response:** ```json { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." } ``` **Token Management**: Tokens expire after 1 hour. Implement token caching and refresh logic in production. ### Production Token Management {/* Multi-language code examples */} {/* Node.js */} ```javascript class KoyweClient { constructor(apiKey, secret, baseUrl) { this.apiKey = apiKey; this.secret = secret; this.baseUrl = baseUrl; this.token = null; this.tokenExpiry = null; } async getToken() { // Return cached token if still valid if (this.token && this.tokenExpiry > Date.now()) { return this.token; } // Get new token const response = await axios.post(`${this.baseUrl}/auth/sign-in`, { apiKey: this.apiKey, secret: this.secret }); this.token = response.data.token; // Set expiry to 55 minutes (5 min buffer) this.tokenExpiry = Date.now() + (55 * 60 * 1000); return this.token; } } ``` --- ## Step 2: Get Available Payment Methods Query which payment methods are available for your target country and currency: {/* Multi-language code examples */} {/* cURL */} ```bash curl -X GET 'https://api-sandbox.koywe.com/api/v1/payment-method?countrySymbol=CO¤cySymbol=COP' \ -H 'Authorization: Bearer YOUR_TOKEN' ``` {/* Node.js */} ```javascript async function getPaymentMethods(token, countrySymbol, currencySymbol) { const response = await axios.get( 'https://api-sandbox.koywe.com/api/v1/payment-method', { params: { countrySymbol: countrySymbol, // e.g., 'CO' for Colombia currencySymbol: currencySymbol // e.g., 'COP' }, headers: { 'Authorization': `Bearer ${token}` } } ); return response.data; } // Usage - Get Colombian payment methods const methods = await getPaymentMethods(token, 'CO', 'COP'); console.log('Available methods:', methods); // Example response // [ // { // "method": "PSE", // "name": "PSE - Pagos Seguros en LΓ­nea", // "supportedCountries": ["CO"], // "supportedCurrencies": ["COP"], // "extra": { // "banks": ["BANCOLOMBIA", "DAVIVIENDA", "BOGOTA"] // } // }, // { // "method": "NEQUI", // "name": "Nequi", // "supportedCountries": ["CO"], // "supportedCurrencies": ["COP"] // } // ] ``` {/* Python */} ```python def get_payment_methods(token, country_symbol, currency_symbol): response = requests.get( 'https://api-sandbox.koywe.com/api/v1/payment-method', params={ 'countrySymbol': country_symbol, 'currencySymbol': currency_symbol }, headers={'Authorization': f'Bearer {token}'} ) response.raise_for_status() return response.json() # Usage methods = get_payment_methods(token, 'CO', 'COP') for method in methods: print(f"Method: {method['method']} - {method['name']}") ``` **Caching**: Cache payment methods per country/currency to reduce API calls. Methods don't change frequently. --- ## Step 3: Create Contact (Optional but Recommended) Create a contact for the customer to track payment history and meet compliance requirements: {/* Multi-language code examples */} {/* Node.js */} ```javascript async function createContact(token, orgId, merchantId, contactData) { const response = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/contacts`, { firstName: contactData.firstName, // Required lastName: contactData.lastName, // Optional email: contactData.email, // Optional but recommended phone: contactData.phone, // Optional countrySymbol: contactData.countrySymbol, // Required (e.g., 'CO') documentType: contactData.documentType, // Optional (e.g., 'CC') documentNumber: contactData.documentNumber, // Optional (required if documentType is provided) businessType: 'PERSON', // 'PERSON' or 'COMPANY' type: 'PERSON' // Required: PERSON, BUSINESS, GOVERNMENT, NGO, FOREIGN }, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } } ); return response.data; } // Usage const contact = await createContact(token, orgId, merchantId, { firstName: 'Juan', lastName: 'PΓ©rez', email: 'customer@example.com', phone: '+573001234567', countrySymbol: 'CO', documentType: 'CC', // Colombian ID (optional) documentNumber: '1234567890' }); console.log('Contact created:', contact.id); ``` {/* Python */} ```python def create_contact(token, org_id, merchant_id, contact_data): response = requests.post( f'https://api-sandbox.koywe.com/api/v1/organizations/{org_id}/merchants/{merchant_id}/contacts', json={ 'firstName': contact_data['first_name'], 'lastName': contact_data.get('last_name'), 'email': contact_data.get('email'), 'phone': contact_data.get('phone'), 'countrySymbol': contact_data['country_symbol'], 'documentType': contact_data.get('document_type'), 'documentNumber': contact_data.get('document_number'), 'businessType': 'PERSON', 'type': 'PERSON' }, headers={ 'Authorization': f'Bearer {token}', 'Content-Type': 'application/json' } ) response.raise_for_status() return response.json() # Usage contact = create_contact(token, org_id, merchant_id, { 'first_name': 'Juan', 'last_name': 'PΓ©rez', 'email': 'customer@example.com', 'phone': '+573001234567', 'country_symbol': 'CO', 'document_type': 'CC', 'document_number': '1234567890' }) ``` {/* cURL */} ```bash curl -X POST 'https://api-sandbox.koywe.com/api/v1/organizations/YOUR_ORG_ID/merchants/YOUR_MERCHANT_ID/contacts' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "firstName": "Juan", "lastName": "PΓ©rez", "email": "customer@example.com", "phone": "+573001234567", "countrySymbol": "CO", "documentType": "CC", "documentNumber": "1234567890", "businessType": "PERSON", "type": "PERSON" }' ``` ### Document Types by Country | Country | Code | Document Type | Example | |---------|------|---------------|---------| | Colombia | CC | CΓ©dula de CiudadanΓ­a | 1234567890 | | Brazil | CPF | Cadastro de Pessoas FΓ­sicas | 12345678900 | | Mexico | RFC | Registro Federal de Contribuyentes | XAXX010101000 | | Chile | RUT | Rol Único Tributario | 11111111-1 | [Complete document types reference β†’](/en/core-concepts/contacts-and-bank-accounts) --- ## Step 4: Create PAYIN Order Create the payment order with all required details: {/* Multi-language code examples */} {/* Node.js */} ```javascript async function createPayinOrder(token, orgId, merchantId, orderData) { const response = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/orders`, { type: 'PAYIN', // Order type originCurrencySymbol: orderData.currency, // e.g., 'COP' destinationCurrencySymbol: orderData.currency, // Same as origin for PAYIN amountIn: orderData.amount, // Amount to collect description: orderData.description, // Customer-facing description externalId: orderData.externalId, // Your internal reference (for idempotency) contactId: orderData.contactId, // Contact from Step 3 (optional) paymentMethods: [ // At least one payment method required { method: orderData.paymentMethod, // e.g., 'PSE', 'PIX', 'SPEI' extra: orderData.extra // Additional data (e.g., bank for PSE) } ], successUrl: orderData.successUrl, // Redirect after success failedUrl: orderData.failedUrl // Redirect after failure }, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } } ); return response.data; } // Usage const order = await createPayinOrder(token, orgId, merchantId, { currency: 'COP', amount: 50000, // 50,000 COP description: 'Payment for Order #12345', externalId: `order-12345-${Date.now()}`, // Unique per attempt contactId: contact.id, paymentMethod: 'PSE', extra: { bankAccount: { name: 'BANCOLOMBIA' } }, // Bank selection for PSE (per-method extras) successUrl: 'https://yoursite.com/payment/success', failedUrl: 'https://yoursite.com/payment/failed' }); console.log('Order ID:', order.id); console.log('Payment URL:', order.providedAction); // Redirect customer to order.providedAction ``` {/* Python */} ```python def create_payin_order(token, org_id, merchant_id, order_data): response = requests.post( f'https://api-sandbox.koywe.com/api/v1/organizations/{org_id}/merchants/{merchant_id}/orders', json={ 'type': 'PAYIN', 'originCurrencySymbol': order_data['currency'], 'destinationCurrencySymbol': order_data['currency'], 'amountIn': order_data['amount'], 'description': order_data['description'], 'externalId': order_data['external_id'], 'contactId': order_data.get('contact_id'), 'paymentMethods': [ { 'method': order_data['payment_method'], 'extra': order_data.get('extra') } ], 'successUrl': order_data['success_url'], 'failedUrl': order_data['failed_url'] }, headers={ 'Authorization': f'Bearer {token}', 'Content-Type': 'application/json' } ) response.raise_for_status() return response.json() # Usage order = create_payin_order(token, org_id, merchant_id, { 'currency': 'COP', 'amount': 50000, 'description': 'Payment for Order #12345', 'external_id': f'order-12345-{int(time.time())}', 'contact_id': contact['id'], 'payment_method': 'PSE', 'extra': 'BANCOLOMBIA', 'success_url': 'https://yoursite.com/payment/success', 'failed_url': 'https://yoursite.com/payment/failed' }) print(f"Order ID: {order['id']}") print(f"Payment URL: {order['providedAction']}") ``` {/* cURL */} ```bash curl -X POST 'https://api-sandbox.koywe.com/api/v1/organizations/YOUR_ORG_ID/merchants/YOUR_MERCHANT_ID/orders' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "type": "PAYIN", "originCurrencySymbol": "COP", "destinationCurrencySymbol": "COP", "amountIn": 50000, "description": "Payment for Order #12345", "externalId": "order-12345-1699999999", "contactId": "cnt_abc123", "paymentMethods": [ { "method": "PSE", "extra": { "bankAccount": { "name": "BANCOLOMBIA" } } } ], "successUrl": "https://yoursite.com/payment/success", "failedUrl": "https://yoursite.com/payment/failed" }' ``` **Response:** ```json { "id": "ord_abc123xyz", "type": "PAYIN", "status": "PENDING", "amountIn": 50000, "originCurrencySymbol": "COP", "destinationCurrencySymbol": "COP", "providedAction": "https://checkout.koywe.com/pay/ord_abc123xyz", "externalId": "order-12345-1699999999", "description": "Payment for Order #12345", "createdAt": "2025-11-13T10:00:00Z" } ``` **Payment URL Generated**: The `providedAction` is where you should redirect your customer to complete the payment. --- ## Step 5: Redirect Customer to Payment Send the customer to the payment URL: {/* Multi-language code examples */} {/* JavaScript (Frontend) */} ```javascript // Direct redirect window.location.href = order.providedAction; // Or open in new window window.open(order.providedAction, '_blank'); // Or return URL to frontend res.json({ orderId: order.id, providedAction: order.providedAction }); ``` {/* React Example */} ```javascript function CheckoutButton() { const handleCheckout = async () => { try { // Call your backend to create order const response = await fetch('/api/create-payment', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ amount: 50000, description: 'Order #12345' }) }); const { providedAction } = await response.json(); // Redirect to payment window.location.href = providedAction; } catch (error) { console.error('Payment error:', error); } }; return ( Pay Now ); } ``` --- ## Step 6: Handle Webhooks Listen for webhook events to track payment status: {/* Multi-language code examples */} {/* Node.js Express */} ```javascript const express = require('express'); const crypto = require('crypto'); app.post('/webhooks/koywe', express.raw({type: 'application/json'}), async (req, res) => { // Convert raw body to string once const rawBody = req.body.toString(); // 1. Verify webhook signature const signature = req.headers['koywe-signature']; const secret = process.env.KOYWE_WEBHOOK_SECRET; const expectedSignature = crypto .createHmac('sha256', secret) .update(rawBody) .digest('hex'); if (signature !== expectedSignature) { console.error('Invalid webhook signature'); return res.status(401).send('Invalid signature'); } // 2. Parse event from string const event = JSON.parse(rawBody); const eventId = event.id; // 3. Check for duplicate (idempotency) if (await isEventProcessed(eventId)) { return res.status(200).send('Already processed'); } // 4. Handle event based on type try { switch (event.type) { case 'order.created': console.log('Order created:', event.data.orderId); break; case 'order.pending': console.log('Order pending:', event.data.orderId); await updateOrderStatus(event.data.externalId, 'pending'); break; case 'order.processing': console.log('Payment processing:', event.data.orderId); await updateOrderStatus(event.data.externalId, 'processing'); break; case 'order.paid': console.log('Payment confirmed:', event.data.orderId); await updateOrderStatus(event.data.externalId, 'paid'); await sendConfirmationEmail(event.data.externalId); break; case 'order.completed': console.log('Funds credited:', event.data.orderId); await updateOrderStatus(event.data.externalId, 'completed'); // FULFILL THE ORDER HERE await fulfillOrder(event.data.externalId); await sendShippingNotification(event.data.externalId); break; case 'order.failed': console.log('Payment failed:', event.data.orderId); await updateOrderStatus(event.data.externalId, 'failed'); await sendFailureNotification(event.data.externalId); break; case 'order.expired': console.log('Order expired:', event.data.orderId); await updateOrderStatus(event.data.externalId, 'expired'); break; case 'order.cancelled': console.log('Order cancelled:', event.data.orderId); await updateOrderStatus(event.data.externalId, 'cancelled'); break; } // 5. Mark event as processed await markEventProcessed(eventId); // 6. Respond quickly (< 5 seconds) res.status(200).send('OK'); } catch (error) { console.error('Webhook processing error:', error); // Still return 200 to prevent retries for processing errors res.status(200).send('OK'); // Log error for manual review await logWebhookError(event, error); } }); ``` {/* Python Flask */} ```python from flask import Flask, request app = Flask(__name__) @app.route('/webhooks/koywe', methods=['POST']) def webhook_handler(): # 1. Verify signature signature = request.headers.get('Koywe-Signature') secret = os.environ['KOYWE_WEBHOOK_SECRET'] expected_signature = hmac.new( secret.encode(), request.data, hashlib.sha256 ).hexdigest() if signature != expected_signature: return 'Invalid signature', 401 # 2. Parse event event = json.loads(request.data) event_id = event['id'] # 3. Check for duplicate if is_event_processed(event_id): return 'Already processed', 200 # 4. Handle event try: event_type = event['type'] order_data = event['data'] if event_type == 'order.paid': update_order_status(order_data['externalId'], 'paid') send_confirmation_email(order_data['externalId']) elif event_type == 'order.completed': update_order_status(order_data['externalId'], 'completed') # FULFILL THE ORDER HERE fulfill_order(order_data['externalId']) elif event_type == 'order.failed': update_order_status(order_data['externalId'], 'failed') send_failure_notification(order_data['externalId']) # Mark as processed mark_event_processed(event_id) return 'OK', 200 except Exception as error: print(f'Webhook error: {error}') log_webhook_error(event, error) return 'OK', 200 # Return 200 to prevent retries ``` **Critical**: Always verify webhook signatures to ensure the webhook is from Koywe. [Complete webhooks guide β†’](/en/advanced/webhooks) --- ## Step 7: Check Order Status (Alternative/Supplement to Webhooks) Query order status directly if needed: {/* Multi-language code examples */} {/* Node.js */} ```javascript async function getOrderStatus(token, orgId, merchantId, orderId) { const response = await axios.get( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/orders/${orderId}`, { headers: { 'Authorization': `Bearer ${token}` } } ); return response.data; } // Usage const order = await getOrderStatus(token, orgId, merchantId, 'ord_abc123'); console.log('Current status:', order.status); console.log('Amount:', order.amountIn, order.originCurrencySymbol); ``` **Response:** ```json { "id": "ord_abc123xyz", "type": "PAYIN", "status": "COMPLETED", "amountIn": 50000, "originCurrencySymbol": "COP", "destinationCurrencySymbol": "COP", "externalId": "order-12345", "dates": { "confirmationDate": "2025-11-13T10:00:00Z", "paymentDate": "2025-11-13T10:05:00Z", "deliveryDate": "2025-11-13T10:06:00Z" } } ``` --- ## Complete Integration Example Here's a full end-to-end example: {/* Multi-language code examples */} {/* Node.js */} ```javascript const axios = require('axios'); const BASE_URL = 'https://api-sandbox.koywe.com/api/v1'; const ORG_ID = process.env.KOYWE_ORG_ID; const MERCHANT_ID = process.env.KOYWE_MERCHANT_ID; const API_KEY = process.env.KOYWE_API_KEY; const SECRET = process.env.KOYWE_SECRET; async function acceptPayment(customerData, orderDetails) { try { // 1. Authenticate console.log('1. Authenticating...'); const authResponse = await axios.post(`${BASE_URL}/auth/sign-in`, { apiKey: API_KEY, secret: SECRET }); const token = authResponse.data.token; console.log('βœ“ Authenticated'); const headers = { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }; // 2. Get payment methods console.log('\n2. Getting payment methods...'); const methodsResponse = await axios.get( `${BASE_URL}/payment-method`, { params: { countrySymbol: customerData.country, currencySymbol: orderDetails.currency }, headers: { 'Authorization': `Bearer ${token}` } } ); const methods = methodsResponse.data; console.log('βœ“ Available methods:', methods.map(m => m.method).join(', ')); // 3. Create contact console.log('\n3. Creating customer contact...'); const contactResponse = await axios.post( `${BASE_URL}/organizations/${ORG_ID}/merchants/${MERCHANT_ID}/contacts`, { firstName: customerData.firstName, lastName: customerData.lastName, email: customerData.email, phone: customerData.phone, countrySymbol: customerData.country, documentType: customerData.documentType, documentNumber: customerData.documentNumber, businessType: 'PERSON', type: 'PERSON' }, { headers } ); const contactId = contactResponse.data.id; console.log('βœ“ Contact created:', contactId); // 4. Create PAYIN order console.log('\n4. Creating payment order...'); const orderResponse = await axios.post( `${BASE_URL}/organizations/${ORG_ID}/merchants/${MERCHANT_ID}/orders`, { type: 'PAYIN', originCurrencySymbol: orderDetails.currency, destinationCurrencySymbol: orderDetails.currency, amountIn: orderDetails.amount, description: orderDetails.description, externalId: orderDetails.orderId, contactId: contactId, paymentMethods: [ { method: orderDetails.paymentMethod, extra: orderDetails.bank } ], successUrl: 'https://yoursite.com/payment/success', failedUrl: 'https://yoursite.com/payment/failed' }, { headers } ); const order = orderResponse.data; console.log('βœ“ Order created:', order.id); console.log('βœ“ Status:', order.status); // 5. Return payment URL console.log('\n5. Payment URL ready:'); console.log(order.providedAction); return { success: true, orderId: order.id, providedAction: order.providedAction, status: order.status }; } catch (error) { console.error('❌ Error:', error.response?.data || error.message); throw error; } } // Usage const result = await acceptPayment( { firstName: 'Juan', lastName: 'PΓ©rez', email: 'customer@example.com', phone: '+573001234567', country: 'CO', documentType: 'CC', documentNumber: '1234567890' }, { currency: 'COP', amount: 50000, description: 'Payment for Order #12345', orderId: `order-${Date.now()}`, paymentMethod: 'PSE', bank: 'BANCOLOMBIA' } ); console.log('\nβœ… Complete! Redirect customer to:'); console.log(result.providedAction); ``` --- ## Error Handling ### Common Errors and Solutions **Insufficient Balance** ```javascript // Error: Insufficient balance // Solution: This shouldn't happen for PAYIN (only for PAYOUT) // Check that you're using correct order type ``` **Invalid Payment Method** ```javascript // Error: Payment method not supported // Solution: Get available methods first (Step 2) const methods = await getPaymentMethods(country, currency); // Use only returned methods ``` **Invalid Document** ```javascript // Error: Invalid document number // Solution: Validate document format per country // Colombia CC: 6-10 digits // Brazil CPF: 11 digits // etc. ``` **Expired Token** ```javascript // Error: 401 Unauthorized // Solution: Implement token refresh if (error.response?.status === 401) { token = await authenticate(); // Retry request } ``` [Complete error handling guide β†’](/en/advanced/error-handling) --- ## Testing Test your integration in sandbox: ### Use sandbox URL `https://api-sandbox.koywe.com` ### Use test amount 666 for failures Test failed payment scenario ### Use any other amount for success All other amounts succeed in sandbox ### Verify webhook handling Use webhook.site to inspect webhooks [Complete testing guide β†’](/en/getting-started/testing) --- ## Production Checklist Before going live: - [ ] Change to production URL (`https://api.koywe.com`) - [ ] Update to production API credentials - [ ] Implement webhook signature verification - [ ] Setup error logging and monitoring - [ ] Test with small real amounts - [ ] Implement token caching and refresh - [ ] Setup retry logic for transient failures - [ ] Configure proper timeout values - [ ] Implement idempotency with `externalId` - [ ] Test all payment methods you'll support --- ## Next Steps Detailed guide for each payment method Advanced webhook handling Comprehensive error handling strategies Common issues and solutions --- # Payment Methods by Country _Detailed guide to each payment method_ Source: https://docs.koywe.com/en/accepting-payments/payment-methods # Payment Methods by Country Comprehensive guide to all supported payment methods across Latin America. ## Overview Koywe supports local payment methods for each country, optimized for the best conversion rates and user experience. | Country | Payment Methods | Settlement Time | |---------|----------------|----------------| | πŸ‡¨πŸ‡΄ Colombia | PSE, Nequi | Instant - 2 hours | | πŸ‡§πŸ‡· Brazil | PIX | Instant | | πŸ‡²πŸ‡½ Mexico | SPEI | Instant ⚑ | | πŸ‡¨πŸ‡± Chile | Khipu | Instant - 2 hours | | πŸ‡¦πŸ‡· Argentina | Local methods | Varies | | πŸ‡΅πŸ‡ͺ Peru | QRI | Instant | **Beyond this page**: Bolivia, Venezuela and the United States also settle payins, over rails that are not self-service payment methods (QR, bank transfer and named accounts respectively). For the full country/currency/direction matrix, see [Supported Corridors & Currencies](/en/core-concepts/supported-corridors). --- ## Colombia πŸ‡¨πŸ‡΄ ### PSE (Pagos Seguros en LΓ­nea) **Description**: Colombia's most popular online bank transfer system. Customers pay directly from their bank account. **Currency**: COP (Colombian Peso) **How it works**: 1. Customer selects their bank 2. Redirected to bank's website 3. Logs in and authorizes payment 4. Funds transferred instantly **Settlement**: Instant - 2 hours **Implementation**: {/* Multi-language code examples */} {/* Node.js */} ```javascript const order = await createPayinOrder(token, orgId, merchantId, { type: 'PAYIN', originCurrencySymbol: 'COP', destinationCurrencySymbol: 'COP', amountIn: 50000, paymentMethods: [ { method: 'PSE', extra: { bankAccount: { name: 'BANCOLOMBIA' } } // Bank selection (per-method extras) } ], // ... other fields }); ``` **Supported Banks**: - BANCOLOMBIA - DAVIVIENDA - BANCO_DE_BOGOTA - BANCO_POPULAR - BBVA_COLOMBIA - BANCO_OCCIDENTE - BANCO_AV_VILLAS - BANCO_GNB_SUDAMERIS - And more... **Best for**: E-commerce, services, subscriptions **User Experience**: - βœ… Instant confirmation - βœ… No credit card needed - βœ… Trusted by Colombians - ⚠️ Requires bank account --- ### Nequi **Description**: Colombia's popular mobile wallet. Customers pay using their Nequi app. **Currency**: COP (Colombian Peso) **How it works**: 1. Customer scans QR code or receives push notification 2. Opens Nequi app 3. Confirms payment 4. Instant transfer **Settlement**: Instant **Implementation**: {/* Multi-language code examples */} {/* Node.js */} ```javascript const order = await createPayinOrder(token, orgId, merchantId, { type: 'PAYIN', originCurrencySymbol: 'COP', destinationCurrencySymbol: 'COP', amountIn: 50000, paymentMethods: [ { method: 'NEQUI' // No extra field needed } ], // ... other fields }); ``` **Best for**: Mobile-first experiences, small transactions **User Experience**: - βœ… Very fast - βœ… Mobile-optimized - βœ… Popular among young users - ⚠️ Requires Nequi account --- ## Brazil πŸ‡§πŸ‡· ### PIX **Description**: Brazil's instant payment system. The most popular payment method in Brazil. **Currency**: BRL (Brazilian Real) **How it works**: 1. Customer receives QR code or PIX code 2. Opens banking app 3. Scans QR or enters code 4. Confirms payment 5. Instant transfer **Settlement**: Instant **Implementation**: {/* Multi-language code examples */} {/* Node.js */} ```javascript const order = await createPayinOrder(token, orgId, merchantId, { type: 'PAYIN', originCurrencySymbol: 'BRL', destinationCurrencySymbol: 'BRL', amountIn: 100, // 100 BRL paymentMethods: [ { method: 'PIX_STATIC' // or 'PIX_DYNAMIC' } ], // ... other fields }); ``` **PIX Types**: | Type | Use Case | Reusable | |------|----------|----------| | **PIX_STATIC** | Single payment | No | | **PIX_DYNAMIC** | Multiple payments | Yes | **Best for**: All use cases - PIX is universally accepted in Brazil **User Experience**: - βœ… Instant confirmation - βœ… Works 24/7 - βœ… All Brazilian banks support it - βœ… Very low fees --- ## Mexico πŸ‡²πŸ‡½ ### SPEI **Description**: Mexico's electronic payment system for instant bank transfers. **Currency**: MXN (Mexican Peso) **How it works**: 1. Customer receives bank account details 2. Initiates transfer from their bank 3. Bank processes via SPEI network 4. Funds arrive instantly **Settlement**: Instant ⚑ **Implementation**: {/* Multi-language code examples */} {/* Node.js */} ```javascript const order = await createPayinOrder(token, orgId, merchantId, { type: 'PAYIN', originCurrencySymbol: 'MXN', destinationCurrencySymbol: 'MXN', amountIn: 500, // 500 MXN paymentMethods: [ { method: 'SPEI' } ], // ... other fields }); ``` **Best for**: All transaction sizes, instant settlement required **User Experience**: - βœ… Widely accepted - βœ… No payment limit - βœ… Instant settlement - βœ… Available 24/7 **Mexico Payment Methods**: Currently, SPEI is the only supported payment method for Mexico with instant settlement. --- ## Cards (Credit/Debit) - International **Description**: International credit and debit cards. **Currency**: MXN (Mexican Peso) **Supported Cards**: - Visa - Mastercard - American Express **Settlement**: Instant **Implementation**: {/* Multi-language code examples */} {/* Node.js */} ```javascript const order = await createPayinOrder(token, orgId, merchantId, { type: 'PAYIN', originCurrencySymbol: 'MXN', destinationCurrencySymbol: 'MXN', amountIn: 500, paymentMethods: [ { method: 'CARD' } ], // ... other fields }); ``` **Best for**: International customers, instant payments **User Experience**: - βœ… Instant confirmation - βœ… Familiar to users - ⚠️ Higher fees - ⚠️ May require 3DS verification --- ## Chile πŸ‡¨πŸ‡± ### Khipu **Description**: Chile's popular payment aggregator, connecting to multiple banks. **Currency**: CLP (Chilean Peso) **How it works**: 1. Customer selects bank 2. Redirected to bank or Khipu app 3. Authorizes payment 4. Instant or near-instant transfer **Settlement**: Instant - 2 hours **Implementation**: {/* Multi-language code examples */} {/* Node.js */} ```javascript const order = await createPayinOrder(token, orgId, merchantId, { type: 'PAYIN', originCurrencySymbol: 'CLP', destinationCurrencySymbol: 'CLP', amountIn: 10000, // 10,000 CLP paymentMethods: [ { method: 'KHIPU' } ], // ... other fields }); ``` **Best for**: All Chilean payments **User Experience**: - βœ… Fast - βœ… Multiple bank support - βœ… Trusted in Chile --- ## Peru πŸ‡΅πŸ‡ͺ ### QRI **Description**: QR-based payment method for Peru. Customers pay by scanning a QR code from their banking app. **Currency**: PEN (Peruvian Sol) **How it works**: 1. Customer receives a QR code 2. Opens their banking app 3. Scans the QR code 4. Confirms payment 5. Instant transfer **Settlement**: Instant **Implementation**: {/* Multi-language code examples */} {/* Node.js */} ```javascript const order = await createPayinOrder(token, orgId, merchantId, { type: 'PAYIN', originCurrencySymbol: 'PEN', destinationCurrencySymbol: 'PEN', amountIn: 100, // 100 PEN paymentMethods: [ { method: 'QRI' } ], // ... other fields }); ``` **Best for**: All Peruvian payments **User Experience**: - βœ… Instant confirmation - βœ… Works with all major Peruvian banks - βœ… QR-based, mobile-friendly - ⚠️ Requires banking app with QR support --- ## Testing Payment Methods ### Sandbox Behavior In sandbox, all payment methods automatically succeed: {/* Multi-language code examples */} {/* Test Scenarios */} ```javascript // Success scenario (any amount except 666) const order = await createPayinOrder(token, orgId, merchantId, { amountIn: 50000, paymentMethods: [{ method: 'PSE', extra: { bankAccount: { name: 'BANCOLOMBIA' } } }] // ... other fields }); // Result: Order auto-completes successfully // Failure scenario (amount = 666) const order = await createPayinOrder(token, orgId, merchantId, { amountIn: 666, // Special test amount paymentMethods: [{ method: 'PSE', extra: { bankAccount: { name: 'BANCOLOMBIA' } } }] // ... other fields }); // Result: Order fails ``` [Complete testing guide β†’](/en/getting-started/testing) --- ## Choosing the Right Payment Method ### By Country Always offer the local payment method for best conversion: | Country | Recommended Method | Why | |---------|-------------------|-----| | Colombia | PSE | Most trusted, instant | | Brazil | PIX | Universal, instant, 24/7 | | Mexico | SPEI | Instant settlement, 24/7 | | Chile | Khipu | Best coverage | | Peru | QRI | QR-based, instant | ### By Use Case **E-Commerce**: - Colombia: PSE, Nequi - Brazil: PIX - Mexico: SPEI **Large Transactions**: - All countries: Bank transfers (PSE, SPEI, PIX) **Mobile-First**: - Colombia: Nequi - Brazil: PIX - All: Mobile-optimized checkout --- ## Dynamic Payment Method Selection Let customers choose their preferred method: {/* Multi-language code examples */} {/* Node.js */} ```javascript async function createOrderWithDynamicMethod(country, currency, amount, customerChoice) { // 1. Get available methods const methods = await getPaymentMethods(country, currency); // 2. Validate customer choice const selectedMethod = methods.find(m => m.method === customerChoice.method); if (!selectedMethod) { throw new Error('Payment method not available'); } // 3. Create order const order = await createPayinOrder(token, orgId, merchantId, { originCurrencySymbol: currency, destinationCurrencySymbol: currency, amountIn: amount, paymentMethods: [ { method: customerChoice.method, extra: customerChoice.extra // e.g., bank for PSE } ], // ... other fields }); return order; } // Usage const order = await createOrderWithDynamicMethod( 'CO', 'COP', 50000, { method: 'PSE', extra: { bankAccount: { name: 'BANCOLOMBIA' } } } ); ``` --- ## Next Steps Complete step-by-step integration Test all payment methods in sandbox Common issues and solutions Complete API documentation --- # Troubleshooting - Accepting Payments _Common issues and solutions_ Source: https://docs.koywe.com/en/accepting-payments/troubleshooting # Troubleshooting Accepting Payments Solutions to common issues when integrating payment acceptance. ## Authentication Issues ### 401 Unauthorized Error **Problem**: API returns 401 Unauthorized **Causes**: - Invalid API key or secret - Expired token - Token not included in request **Solutions**: {/* Multi-language code examples */} {/* Check credentials */} ```javascript // Verify credentials are correct console.log('API Key:', process.env.KOYWE_API_KEY); console.log('Secret:', process.env.KOYWE_SECRET ? '***' : 'MISSING'); // Test authentication try { const token = await authenticate(); console.log('βœ“ Authentication successful'); } catch (error) { console.error('βœ— Authentication failed:', error.response?.data); } ``` {/* Token refresh */} ```javascript // Implement token caching with refresh class KoyweClient { constructor(apiKey, secret) { this.apiKey = apiKey; this.secret = secret; this.token = null; this.tokenExpiry = null; } async getToken() { // Return cached token if still valid (with 5-minute buffer) if (this.token && this.tokenExpiry > Date.now() + (5 * 60 * 1000)) { return this.token; } // Get new token const response = await axios.post( 'https://api-sandbox.koywe.com/api/v1/auth/sign-in', { apiKey: this.apiKey, secret: this.secret } ); this.token = response.data.token; this.tokenExpiry = Date.now() + (60 * 60 * 1000); // 1 hour return this.token; } async request(method, url, data) { const token = await this.getToken(); try { return await axios({ method, url, data, headers: { 'Authorization': `Bearer ${token}` } }); } catch (error) { // If 401, token might have expired, retry once if (error.response?.status === 401) { this.token = null; // Clear token const newToken = await this.getToken(); return await axios({ method, url, data, headers: { 'Authorization': `Bearer ${newToken}` } }); } throw error; } } } ``` --- ## Order Creation Issues ### Payment Method Not Supported **Problem**: Error "Payment method not supported for country/currency" **Cause**: Using a payment method that isn't available for the target country or currency **Solution**: {/* Multi-language code examples */} {/* Query available methods first */} ```javascript async function createOrderSafely(country, currency, amount, preferredMethod) { // 1. Get available methods const methods = await getPaymentMethods(country, currency); console.log('Available methods:', methods.map(m => m.method)); // 2. Check if preferred method is available const methodAvailable = methods.some(m => m.method === preferredMethod); if (!methodAvailable) { console.error(`Method ${preferredMethod} not available`); console.log('Use one of:', methods.map(m => m.method)); throw new Error('Payment method not supported'); } // 3. Create order return await createPayinOrder(token, orgId, merchantId, { originCurrencySymbol: currency, destinationCurrencySymbol: currency, amountIn: amount, paymentMethods: [{ method: preferredMethod }] // ... other fields }); } ``` **Common mistakes**: - Using `SPEI` for Colombia (use `PSE` instead) - Using `PSE` for Brazil (use `PIX` instead) - Wrong currency for payment method --- ### Invalid Document Number **Problem**: Error "Invalid document number format" **Cause**: Document number doesn't match expected format for the document type **Solution**: {/* Multi-language code examples */} {/* Validate document formats */} ```javascript function validateDocument(country, documentType, documentNumber) { const validators = { 'CO': { 'CC': /^\d{6,10}$/, // Colombian ID: 6-10 digits 'CE': /^\d{6,7}$/, // Foreign ID: 6-7 digits 'NIT': /^\d{9,10}$/ // Tax ID: 9-10 digits }, 'BR': { 'CPF': /^\d{11}$/, // Individual: 11 digits 'CNPJ': /^\d{14}$/ // Company: 14 digits }, 'MX': { 'RFC': /^[A-Z]{3,4}\d{6}[A-Z0-9]{3}$/ // Tax ID format }, 'CL': { 'RUT': /^\d{7,8}-[\dkK]$/ // Format: 12345678-9 } }; const regex = validators[country]?.[documentType]; if (!regex) { console.warn(`No validator for ${country} ${documentType}`); return true; // Allow if no validator } const isValid = regex.test(documentNumber); if (!isValid) { console.error(`Invalid ${documentType} format: ${documentNumber}`); console.log(`Expected format: ${regex}`); } return isValid; } // Usage const isValid = validateDocument('CO', 'CC', '1234567890'); if (!isValid) { throw new Error('Invalid document number'); } ``` --- ### Duplicate Order / Idempotency Issues **Problem**: Creating duplicate orders or getting "Order already exists" error **Solution**: Use `externalId` for idempotency {/* Multi-language code examples */} {/* Proper idempotency */} ```javascript async function createOrderIdempotent(internalOrderId, orderData) { // Use your internal order ID as externalId const externalId = `order-${internalOrderId}`; try { const order = await createPayinOrder(token, orgId, merchantId, { ...orderData, externalId: externalId // Same externalId = same order }); console.log('Order created:', order.id); return order; } catch (error) { if (error.response?.status === 409) { // Order already exists, retrieve it console.log('Order already exists, retrieving...'); const existingOrder = await getOrderByExternalId(externalId); return existingOrder; } throw error; } } // Safe to retry const order = await createOrderIdempotent('12345', orderData); ``` --- ## Payment Flow Issues ### Order Stuck in PENDING **Problem**: Order stays in PENDING status and never completes **Causes in Production**: - Customer hasn't completed payment - Payment URL expired - Payment provider issues **Causes in Sandbox**: - Using failure test amount (666) - Network issues **Solutions**: {/* Multi-language code examples */} {/* Check order status */} ```javascript async function checkOrderStatus(orderId) { const order = await getOrderStatus(token, orgId, merchantId, orderId); console.log('Status:', order.status); console.log('Created:', order.createdAt); console.log('Due date:', order.dueDate); if (order.status === 'PENDING') { const createdTime = new Date(order.createdAt); const now = new Date(); const minutesElapsed = (now - createdTime) / 1000 / 60; console.log(`Order pending for ${minutesElapsed.toFixed(0)} minutes`); if (minutesElapsed > 30) { console.warn('Order pending for too long - customer may not have paid'); // Consider cancelling or expiring the order } } return order; } ``` **In Sandbox**: - Orders complete automatically after 5-30 seconds - If using amount 666, order will fail (this is intentional for testing) --- ### Payment Completed but No Webhook Received **Problem**: Order shows COMPLETED in API but webhook wasn't received **Causes**: - Webhook endpoint not configured - Webhook endpoint unreachable - Webhook endpoint returning errors - Firewall blocking webhooks **Solutions**: ### Verify webhook endpoint Check that your endpoint is publicly accessible ### Test with webhook.site Use https://webhook.site to see if webhooks are being sent ### Check webhook logs Query webhook delivery attempts: ```javascript const deliveries = await getWebhookDeliveries(orderId); console.log(deliveries); ``` ### Verify signature validation Make sure you're not rejecting webhooks due to failed signature verification ### Implement fallback polling As a backup, poll order status: ```javascript async function waitForCompletion(orderId, maxAttempts = 20) { for (let i = 0; i < maxAttempts; i++) { const order = await getOrderStatus(token, orgId, merchantId, orderId); if (order.status === 'COMPLETED') { return order; } if (['FAILED', 'EXPIRED', 'CANCELLED'].includes(order.status)) { throw new Error(`Order ${order.status}`); } // Wait 3 seconds before next check await new Promise(resolve => setTimeout(resolve, 3000)); } throw new Error('Timeout waiting for order completion'); } ``` --- ### Invalid Webhook Signature **Problem**: Webhook signature verification fails **Cause**: Incorrect signature calculation or wrong secret **Solution**: {/* Multi-language code examples */} {/* Correct signature verification */} ```javascript const crypto = require('crypto'); function verifyWebhookSignature(payload, signature, secret) { // payload should be the raw body (string or Buffer) // NOT parsed JSON const expectedSignature = crypto .createHmac('sha256', secret) .update(payload) .digest('hex'); const isValid = signature === expectedSignature; if (!isValid) { console.error('Signature mismatch'); console.error('Received:', signature); console.error('Expected:', expectedSignature); } return isValid; } // Express example - MUST use raw body app.post('/webhooks/koywe', express.raw({ type: 'application/json' }), // Important: raw, not json (req, res) => { const signature = req.headers['koywe-signature']; const secret = process.env.KOYWE_WEBHOOK_SECRET; // req.body is Buffer when using express.raw if (!verifyWebhookSignature(req.body, signature, secret)) { return res.status(401).send('Invalid signature'); } // Now parse const event = JSON.parse(req.body); // ... handle event res.status(200).send('OK'); } ); ``` --- ## Amount and Currency Issues ### Amount Validation Errors **Problem**: "Invalid amount" or "Amount too low/high" **Causes**: - Amount is 0 or negative - Amount exceeds limits - Decimal amounts where integers expected **Solutions**: {/* Multi-language code examples */} {/* Validate amounts */} ```javascript function validateAmount(amount, currency) { // Minimum amounts per currency const minimums = { 'COP': 1000, // 1,000 COP 'BRL': 1, // 1 BRL 'MXN': 1, // 1 MXN 'CLP': 100, // 100 CLP 'USD': 0.01 // $0.01 USD }; // Maximum amounts per currency const maximums = { 'COP': 50000000, // 50M COP 'BRL': 100000, // 100K BRL 'MXN': 100000, // 100K MXN 'CLP': 10000000, // 10M CLP 'USD': 50000 // 50K USD }; const min = minimums[currency] || 1; const max = maximums[currency] || 1000000; if (amount < min) { throw new Error(`Amount too low. Minimum: ${min} ${currency}`); } if (amount > max) { throw new Error(`Amount too high. Maximum: ${max} ${currency}`); } // Check for invalid decimals (COP, CLP don't use decimals) if (['COP', 'CLP'].includes(currency) && amount % 1 !== 0) { throw new Error(`${currency} does not support decimal amounts`); } return true; } // Usage try { validateAmount(50000, 'COP'); // OK validateAmount(50.5, 'COP'); // Error: no decimals validateAmount(0, 'COP'); // Error: too low } catch (error) { console.error(error.message); } ``` --- ### Currency Mismatch **Problem**: "Currency not supported" or mismatch errors **Cause**: Using wrong currency for country or payment method **Solution**: {/* Multi-language code examples */} {/* Country-currency mapping */} ```javascript const COUNTRY_CURRENCIES = { 'CO': ['COP'], // Colombia: only COP 'BR': ['BRL'], // Brazil: only BRL 'MX': ['MXN'], // Mexico: only MXN 'CL': ['CLP'], // Chile: only CLP 'AR': ['ARS'], // Argentina: only ARS 'PE': ['PEN'] // Peru: only PEN }; function validateCurrency(country, currency) { const validCurrencies = COUNTRY_CURRENCIES[country]; if (!validCurrencies) { throw new Error(`Country ${country} not supported`); } if (!validCurrencies.includes(currency)) { throw new Error( `Currency ${currency} not valid for ${country}. ` + `Use: ${validCurrencies.join(', ')}` ); } return true; } // Usage validateCurrency('CO', 'COP'); // OK validateCurrency('CO', 'USD'); // Error ``` --- ## Network and Timeout Issues ### Request Timeout **Problem**: API requests timeout **Causes**: - Slow network - API under heavy load - Missing timeout configuration **Solutions**: {/* Multi-language code examples */} {/* Configure timeouts */} ```javascript const axios = require('axios'); // Create axios instance with proper timeouts const koyweApi = axios.create({ baseURL: 'https://api-sandbox.koywe.com/api/v1', timeout: 30000, // 30 seconds headers: { 'Content-Type': 'application/json' } }); // Add retry logic async function requestWithRetry(fn, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { try { return await fn(); } catch (error) { const isTimeout = error.code === 'ECONNABORTED'; const isNetworkError = error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED'; const isServerError = error.response?.status >= 500; const shouldRetry = (isTimeout || isNetworkError || isServerError) && i < maxRetries - 1; if (shouldRetry) { const delay = Math.pow(2, i) * 1000; // Exponential backoff console.log(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`); await new Promise(resolve => setTimeout(resolve, delay)); } else { throw error; } } } } // Usage const order = await requestWithRetry(() => createPayinOrder(token, orgId, merchantId, orderData) ); ``` --- ## Contact Support If you've tried these solutions and still have issues: **soporte@koywe.com** Please include: - Order ID or External ID - Error message - Request/response examples (remove sensitive data) - Steps to reproduce - Environment (sandbox/production) --- ## Diagnostic Checklist Use this checklist when troubleshooting: - [ ] API credentials are correct - [ ] Using correct base URL (sandbox vs production) - [ ] Token is valid and not expired - [ ] Payment method is supported for country/currency - [ ] Amount meets minimum/maximum requirements - [ ] Document number format is correct - [ ] Currency matches country - [ ] Webhook endpoint is publicly accessible - [ ] Webhook signature verification is correct - [ ] Network connectivity is stable - [ ] Proper error handling is implemented - [ ] Timeout values are appropriate --- ## Next Steps Review the complete integration guide Test your integration in sandbox Advanced error handling strategies Complete API documentation --- # Paying Providers - Overview _Understanding PAYOUT orders and provider payments_ Source: https://docs.koywe.com/en/paying-providers # Paying Providers (PAYOUT) Learn how to send payments from your virtual balance to external bank accounts. ## What is PAYOUT? **PAYOUT** is the order type used to send funds from your virtual balance account to external bank accounts (providers, vendors, contractors, etc.). |Debit| B[Koywe] B -->|Transfer| C[Payment Network] C -->|Deliver| D[Provider Bank Account] B -->|Webhook| E[Your Application] style A fill:#005544,stroke:#C9FF1F,stroke-width:2px,color:#fff`} /> --- ## Use Cases Pay suppliers and vendors for goods and services Send payments to freelancers and contractors Return payments to customers Pay sellers on your marketplace platform --- ## How It Works ### High-Level Flow ### Check your virtual balance Ensure you have sufficient funds ### Create provider contact Store provider information and bank account ### Create PAYOUT order Specify amount and destination bank account ### Funds are debited Money is immediately reserved from your virtual account ### Bank transfer initiated Koywe initiates transfer to provider's bank ### Provider receives funds Settlement time varies by country (instant to 24 hours) ### You receive confirmation Webhook notification when transfer completes ### Detailed Flow Diagram >K: 1. Check balance K-->>A: Available balance A->>K: 2. Create PAYOUT order K->>V: 3. Reserve & debit funds K->>B: 4. Initiate transfer B->>P: 5. Transfer funds P-->>B: 6. Confirm receipt B-->>K: 7. Confirm completion K->>A: 8. Webhook: order.completed`} /> --- ## Prerequisites Before making payouts: **Required**: - βœ… Sufficient funds in virtual account - βœ… Provider contact created - βœ… Provider bank account linked - βœ… Bank account details verified **Critical**: Always check your virtual account balance before creating a PAYOUT order. Orders will fail if you have insufficient funds. --- ## Supported Countries The payout side of every corridor. For payin coverage, currencies and the local rail behind each one, see [Supported Corridors & Currencies](/en/core-concepts/supported-corridors). ### Colombia πŸ‡¨πŸ‡΄ - **Method**: ACH Colombia, Bre-B - **Currency**: COP - **Settlement**: ACH runs on cycles; Bre-B settles in seconds - **Banks**: All Colombian banks ### Brazil πŸ‡§πŸ‡· - **Method**: PIX transfer - **Currency**: BRL - **Settlement**: Instant - **Banks**: All Brazilian banks with PIX ### Mexico πŸ‡²πŸ‡½ - **Method**: SPEI transfer - **Currency**: MXN - **Settlement**: Instant ⚑ - **Banks**: All Mexican banks ### Chile πŸ‡¨πŸ‡± - **Method**: Bulk payment file - **Currency**: CLP - **Settlement**: Effectively instant. Transactions above CLP 7,000,000 are split into parts - **Banks**: All Chilean banks ### Argentina πŸ‡¦πŸ‡· - **Method**: Transfer to a named CVU - **Currency**: ARS - **Settlement**: 1-2 business days - **Banks**: All Argentine banks. Named accounts only β€” the beneficiary's identity has to match ### Peru πŸ‡΅πŸ‡ͺ - **Method**: Wire transfer - **Currency**: PEN, USD - **Settlement**: 1-2 business days - **Banks**: All Peruvian banks ### Bolivia πŸ‡§πŸ‡΄ - **Method**: Bank transfer - **Currency**: BOB - **Settlement**: 1-2 business days - **Banks**: All Bolivian banks ### Venezuela πŸ‡»πŸ‡ͺ - **Method**: Bank transfer - **Currency**: VES - **Settlement**: 1-2 business days - **Banks**: All Venezuelan banks - **Daily payout limit**: $3,500 USD equivalent per day ### United States πŸ‡ΊπŸ‡Έ - **Method**: Wire or ACH to a named account - **Currency**: USD - **Settlement**: 1-2 business days - **Banks**: All US banks (routing number required) - **Note**: Requires a separate KYB/KYC onboarding --- ## Quick Example Here's a simple PAYOUT flow: {/* Multi-language code examples */} {/* Node.js */} ```javascript // 1. Check balance const balance = await getBalance(token, orgId, merchantId, 'COP'); console.log('Available:', balance.availableBalance); // e.g., 1,000,000 COP // 2. Create provider contact const provider = await createContact(token, orgId, merchantId, { firstName: 'Servicios ABC SAS', countrySymbol: 'CO', documentType: 'NIT', documentNumber: '900123456-1', businessType: 'COMPANY', type: 'BUSINESS', email: 'provider@example.com' }); // 3. Add bank account const bankAccount = await addBankAccount(token, orgId, merchantId, provider.id, { name: 'Bancolombia COP', kind: 'BANK', isDefault: true, countrySymbol: 'CO', currencySymbol: 'COP', entity: 'BANCOLOMBIA', accountNumber: '1234567890', type: 'CHECKING' }); // 4. Create PAYOUT order const payout = await createPayoutOrder(token, orgId, merchantId, { type: 'PAYOUT', originCurrencySymbol: 'COP', destinationCurrencySymbol: 'COP', amountIn: 500000, // 500,000 COP contactId: provider.id, destinationAccountId: bankAccount.id, description: 'Payment for Invoice #INV-001' }); console.log('Payout created:', payout.id); console.log('Status:', payout.status); // "PROCESSING" ``` --- ## Key Differences vs PAYIN | Feature | PAYIN | PAYOUT | |---------|-------|--------| | **Direction** | Customer β†’ Your account | Your account β†’ Provider | | **Balance** | Credits your account | Debits your account | | **Payment URL** | Yes (for customer) | No | | **Bank Account** | Optional | **Required** | | **Contact** | Optional | Recommended | | **Balance Check** | Not needed | **Critical** | --- ## Payment Flow Comparison ### PAYIN (Receiving) ``` Customer pays β†’ Funds credited β†’ Webhook β†’ Fulfill order ``` ### PAYOUT (Sending) ``` Check balance β†’ Create order β†’ Funds debited β†’ Bank transfer β†’ Webhook confirmation ``` --- ## Balance Requirements ### Checking Balance Before PAYOUT {/* Multi-language code examples */} {/* Node.js */} ```javascript async function safeCreatePayout(token, orgId, merchantId, payoutData) { // 1. Get current balance const balances = await getMerchantBalances(token, orgId, merchantId); const balance = balances.find(b => b.currencySymbol === payoutData.currency); // 2. Check available balance if (!balance || balance.availableBalance < payoutData.amount) { throw new Error( `Insufficient balance. Available: ${balance?.availableBalance || 0} ${payoutData.currency}, ` + `Required: ${payoutData.amount} ${payoutData.currency}` ); } console.log(`βœ“ Sufficient balance: ${balance.availableBalance} ${payoutData.currency}`); // 3. Create payout const payout = await createPayoutOrder(token, orgId, merchantId, payoutData); return payout; } ``` --- ## Settlement Times Understanding when providers receive funds: | Country | Method | Typical Settlement | |---------|--------|-------------------| | Colombia | ACH Colombia / Bre-B | ACH by cycle; Bre-B in seconds | | Brazil | PIX | Instant (24/7) | | Mexico | SPEI | Instant ⚑ | | Chile | Bulk payment file | Effectively instant | | Argentina | Named CVU transfer | 1-2 business days | For every payout corridor, including Bolivia, Peru, Venezuela, the United States and cross-border settlement, see [Supported Corridors & Currencies](/en/core-concepts/supported-corridors). **Business Hours**: For countries with business-hour restrictions, transfers initiated after hours or on weekends may be processed the next business day. --- ## Next Steps Step-by-step implementation with code examples Common payout use cases and examples Learn about balance management Test payouts in sandbox environment --- ## Additional Resources - [Core Concepts - Orders](/en/core-concepts/orders-and-order-types) - [Contacts & Bank Accounts](/en/core-concepts/contacts-and-bank-accounts) - [Error Handling](/en/advanced/error-handling) - [API Reference](/api-reference) --- # Paying Providers - Integration Guide _Complete step-by-step integration for payouts_ Source: https://docs.koywe.com/en/paying-providers/integration-guide # Paying Providers Integration Guide This guide walks you through implementing provider payouts in your application. ## Prerequisites - [ ] API credentials (key, secret, organizationId, merchantId) - [ ] Funds in your virtual account - [ ] Provider information ready - [ ] Understanding of [Virtual Accounts](/en/core-concepts/virtual-accounts) --- ## Step 1: Check Virtual Account Balance Always check balance before creating a payout: {/* Multi-language code examples */} {/* Node.js */} ```javascript async function getBalance(token, orgId, merchantId, currencySymbol) { const response = await axios.get( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/accounts/balances`, { headers: { 'Authorization': `Bearer ${token}` } } ); const balances = response.data; const balance = balances.find(b => b.currencySymbol === currencySymbol); return balance || { availableBalance: 0, currencySymbol }; } // Usage const balance = await getBalance(token, orgId, merchantId, 'COP'); console.log('Available balance:', balance.availableBalance, 'COP'); // Check if sufficient if (balance.availableBalance < 500000) { console.error('Insufficient balance for payout'); // Handle: add funds via PAYIN or show error } ``` {/* Python */} ```python def get_balance(token, org_id, merchant_id, currency_symbol): response = requests.get( f'https://api-sandbox.koywe.com/api/v1/organizations/{org_id}/merchants/{merchant_id}/accounts/balances', headers={'Authorization': f'Bearer {token}'} ) response.raise_for_status() balances = response.json() balance = next((b for b in balances if b['currencySymbol'] == currency_symbol), None) return balance or {'availableBalance': 0, 'currencySymbol': currency_symbol} # Usage balance = get_balance(token, org_id, merchant_id, 'COP') print(f"Available: {balance['availableBalance']} COP") ``` **Critical**: If balance is insufficient, the PAYOUT order will fail. Always check balance first. --- ## Step 2: Create Provider Contact Store provider information: {/* Multi-language code examples */} {/* Node.js */} ```javascript async function createProviderContact(token, orgId, merchantId, providerData) { const response = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/contacts`, { firstName: providerData.firstName, countrySymbol: providerData.country, documentType: providerData.documentType, documentNumber: providerData.documentNumber, businessType: providerData.businessType, // 'PERSON' or 'COMPANY' type: providerData.type, // 'PERSON', 'BUSINESS', etc. email: providerData.email, phone: providerData.phone }, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } } ); return response.data; } // Usage - Company provider const provider = await createProviderContact(token, orgId, merchantId, { firstName: 'Servicios ABC SAS', country: 'CO', documentType: 'NIT', // Tax ID for companies in Colombia documentNumber: '900123456-1', businessType: 'COMPANY', type: 'BUSINESS', email: 'provider@example.com', phone: '+573001234567' }); console.log('Provider contact created:', provider.id); ``` {/* Python */} ```python def create_provider_contact(token, org_id, merchant_id, provider_data): response = requests.post( f'https://api-sandbox.koywe.com/api/v1/organizations/{org_id}/merchants/{merchant_id}/contacts', json={ 'firstName': provider_data['first_name'], 'countrySymbol': provider_data['country'], 'documentType': provider_data.get('document_type'), 'documentNumber': provider_data.get('document_number'), 'businessType': provider_data['business_type'], 'type': provider_data['type'], 'email': provider_data.get('email'), 'phone': provider_data.get('phone') }, headers={ 'Authorization': f'Bearer {token}', 'Content-Type': 'application/json' } ) response.raise_for_status() return response.json() # Usage provider = create_provider_contact(token, org_id, merchant_id, { 'first_name': 'Servicios ABC SAS', 'country': 'CO', 'document_type': 'NIT', 'document_number': '900123456-1', 'business_type': 'COMPANY', 'type': 'BUSINESS', 'email': 'provider@example.com', 'phone': '+573001234567' }) ``` --- ## Step 3: Add Provider Bank Account Link the provider's bank account details: {/* Multi-language code examples */} {/* Node.js */} ```javascript async function addProviderBankAccount(token, orgId, merchantId, contactId, bankData) { const response = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/contacts/${contactId}/accounts`, { name: bankData.name, // Account display name kind: 'BANK', // 'BANK' or 'CRYPTO' isDefault: bankData.isDefault, countrySymbol: bankData.country, currencySymbol: bankData.currency, entity: bankData.entity, // Bank code (e.g., 'BANCOLOMBIA') accountNumber: bankData.accountNumber, type: bankData.type // 'CHECKING', 'SAVINGS', or 'VIRTUAL' }, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } } ); return response.data; } // Usage const bankAccount = await addProviderBankAccount(token, orgId, merchantId, provider.id, { name: 'Bancolombia COP', country: 'CO', currency: 'COP', entity: 'BANCOLOMBIA', accountNumber: '1234567890', type: 'CHECKING', isDefault: true }); console.log('Bank account added:', bankAccount.id); ``` {/* Python */} ```python def add_provider_bank_account(token, org_id, merchant_id, contact_id, bank_data): response = requests.post( f'https://api-sandbox.koywe.com/api/v1/organizations/{org_id}/merchants/{merchant_id}/contacts/{contact_id}/accounts', json={ 'name': bank_data['name'], 'kind': 'BANK', 'isDefault': bank_data['is_default'], 'countrySymbol': bank_data['country'], 'currencySymbol': bank_data['currency'], 'entity': bank_data['entity'], 'accountNumber': bank_data['account_number'], 'type': bank_data['type'] }, headers={ 'Authorization': f'Bearer {token}', 'Content-Type': 'application/json' } ) response.raise_for_status() return response.json() # Usage bank_account = add_provider_bank_account(token, org_id, merchant_id, provider['id'], { 'name': 'Bancolombia COP', 'country': 'CO', 'currency': 'COP', 'entity': 'BANCOLOMBIA', 'account_number': '1234567890', 'type': 'CHECKING', 'is_default': True }) ``` ### Bank Codes by Country | Country | Example Banks | Code Format | |---------|--------------|-------------| | Colombia | BANCOLOMBIA, DAVIVIENDA, BOGOTA | Bank name | | Brazil | BANCO_DO_BRASIL, BRADESCO, ITAU | Bank code | | Mexico | BBVA_MEXICO, SANTANDER_MEXICO | Bank identifier | | Chile | BANCO_CHILE, BCI, SANTANDER_CHILE | Bank name | [Complete bank codes reference β†’](/api-reference) --- ## Step 4: Create PAYOUT Order Create the payout order: {/* Multi-language code examples */} {/* Node.js */} ```javascript async function createPayoutOrder(token, orgId, merchantId, payoutData) { const response = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/orders`, { type: 'PAYOUT', originCurrencySymbol: payoutData.currency, destinationCurrencySymbol: payoutData.currency, // Same currency amountIn: payoutData.amount, contactId: payoutData.contactId, destinationAccountId: payoutData.destinationAccountId, // Bank account ID description: payoutData.description, externalId: payoutData.externalId // Your internal reference }, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } } ); return response.data; } // Usage const payout = await createPayoutOrder(token, orgId, merchantId, { currency: 'COP', amount: 500000, // 500,000 COP contactId: provider.id, destinationAccountId: bankAccount.id, description: 'Payment for Invoice #INV-001', externalId: `payout-inv-001-${Date.now()}` }); console.log('Payout created:', payout.id); console.log('Status:', payout.status); // "PROCESSING" ``` {/* Python */} ```python def create_payout_order(token, org_id, merchant_id, payout_data): import time response = requests.post( f'https://api-sandbox.koywe.com/api/v1/organizations/{org_id}/merchants/{merchant_id}/orders', json={ 'type': 'PAYOUT', 'originCurrencySymbol': payout_data['currency'], 'destinationCurrencySymbol': payout_data['currency'], 'amountIn': payout_data['amount'], 'contactId': payout_data['contact_id'], 'destinationAccountId': payout_data['destination_account_id'], 'description': payout_data['description'], 'externalId': payout_data['external_id'] }, headers={ 'Authorization': f'Bearer {token}', 'Content-Type': 'application/json' } ) response.raise_for_status() return response.json() # Usage payout = create_payout_order(token, org_id, merchant_id, { 'currency': 'COP', 'amount': 500000, 'contact_id': provider['id'], 'destination_account_id': bank_account['id'], 'description': 'Payment for Invoice #INV-001', 'external_id': f'payout-inv-001-{int(time.time())}' }) print(f"Payout created: {payout['id']}") ``` {/* cURL */} ```bash curl -X POST 'https://api-sandbox.koywe.com/api/v1/organizations/YOUR_ORG_ID/merchants/YOUR_MERCHANT_ID/orders' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "type": "PAYOUT", "originCurrencySymbol": "COP", "destinationCurrencySymbol": "COP", "amountIn": 500000, "contactId": "cnt_provider123", "destinationAccountId": "ba_xyz789", "description": "Payment for Invoice #INV-001", "externalId": "payout-inv-001-1699999999" }' ``` **Response:** ```json { "id": "ord_payout123", "type": "PAYOUT", "status": "PROCESSING", "amountIn": 500000, "originCurrencySymbol": "COP", "destinationCurrencySymbol": "COP", "externalId": "payout-inv-001-1699999999", "description": "Payment for Invoice #INV-001", "createdAt": "2025-11-13T15:00:00Z" } ``` --- ## Step 5: Monitor Payout Status Track the payout via webhooks or polling: ### Via Webhooks (Recommended) {/* Multi-language code examples */} {/* Node.js Express */} ```javascript app.post('/webhooks/koywe', express.raw({type: 'application/json'}), async (req, res) => { // Convert raw body to string once const rawBody = req.body.toString(); // Verify signature with string body const signature = req.headers['koywe-signature']; if (!verifySignature(rawBody, signature, process.env.KOYWE_WEBHOOK_SECRET)) { return res.status(401).send('Invalid signature'); } // Parse JSON from string const event = JSON.parse(rawBody); switch (event.type) { case 'order.processing': console.log('Payout processing:', event.data.orderId); // Update internal status await updatePayoutStatus(event.data.externalId, 'processing'); break; case 'order.completed': console.log('Payout completed:', event.data.orderId); // Mark as paid in your system await markInvoiceAsPaid(event.data.externalId); await notifyProvider(event.data.externalId); break; case 'order.failed': console.log('Payout failed:', event.data.orderId); // Handle failure await handlePayoutFailure(event.data.externalId, event.data.errorMessage); break; } res.status(200).send('OK'); }); ``` ### Via Polling (Alternative) {/* Multi-language code examples */} {/* Node.js */} ```javascript async function waitForPayoutCompletion(token, orgId, merchantId, orderId, maxAttempts = 20) { for (let i = 0; i < maxAttempts; i++) { const order = await axios.get( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/orders/${orderId}`, { headers: { 'Authorization': `Bearer ${token}` } } ); const status = order.data.status; console.log(`Attempt ${i + 1}: Status = ${status}`); if (status === 'COMPLETED') { console.log('βœ“ Payout completed successfully'); return order.data; } if (status === 'FAILED') { throw new Error(`Payout failed: ${order.data.errorMessage}`); } // Wait 5 seconds before checking again await new Promise(resolve => setTimeout(resolve, 5000)); } throw new Error('Timeout waiting for payout completion'); } // Usage const completed = await waitForPayoutCompletion(token, orgId, merchantId, payout.id); console.log('Payout completed at:', completed.dates.deliveryDate); ``` --- ## Complete End-to-End Example {/* Multi-language code examples */} {/* Node.js */} ```javascript async function payProvider(providerData, bankData, payoutData) { try { // 1. Authenticate console.log('1. Authenticating...'); const token = await authenticate(); // 2. Check balance console.log('\n2. Checking balance...'); const balance = await getBalance(token, orgId, merchantId, payoutData.currency); console.log(`Available: ${balance.availableBalance} ${payoutData.currency}`); if (balance.availableBalance < payoutData.amount) { throw new Error(`Insufficient balance. Need ${payoutData.amount}, have ${balance.availableBalance}`); } // 3. Create or get provider contact console.log('\n3. Creating provider contact...'); const provider = await createProviderContact(token, orgId, merchantId, providerData); console.log('βœ“ Provider contact:', provider.id); // 4. Add bank account console.log('\n4. Adding bank account...'); const bankAccount = await addProviderBankAccount(token, orgId, merchantId, provider.id, bankData); console.log('βœ“ Bank account:', bankAccount.id); // 5. Create payout console.log('\n5. Creating payout order...'); const payout = await createPayoutOrder(token, orgId, merchantId, { ...payoutData, contactId: provider.id, destinationAccountId: bankAccount.id }); console.log('βœ“ Payout created:', payout.id); console.log('βœ“ Status:', payout.status); // 6. Wait for completion (or use webhooks in production) console.log('\n6. Waiting for completion...'); const completed = await waitForPayoutCompletion(token, orgId, merchantId, payout.id); console.log('βœ“ Payout completed'); // 7. Update internal systems console.log('\n7. Updating internal records...'); await markInvoiceAsPaid(payoutData.externalId); console.log('\nβœ… Success! Provider has been paid.'); return { success: true, payoutId: completed.id, amount: completed.amountIn, currency: completed.originCurrencySymbol }; } catch (error) { console.error('❌ Error:', error.message); throw error; } } // Usage await payProvider( { firstName: 'Servicios ABC SAS', countrySymbol: 'CO', documentType: 'NIT', documentNumber: '900123456-1', businessType: 'COMPANY', type: 'BUSINESS', email: 'provider@example.com' }, { name: 'Bancolombia COP', kind: 'BANK', isDefault: true, countrySymbol: 'CO', currencySymbol: 'COP', entity: 'BANCOLOMBIA', accountNumber: '1234567890', type: 'CHECKING' }, { currency: 'COP', amount: 500000, description: 'Payment for Invoice #INV-001', externalId: 'inv-001' } ); ``` --- ## Best Practices ### 1. Always Check Balance {/* Multi-language code examples */} {/* Safe Payout Creation */} ```javascript async function safeCreatePayout(token, orgId, merchantId, payoutData) { const balance = await getBalance(token, orgId, merchantId, payoutData.currency); if (balance.availableBalance < payoutData.amount) { throw new Error('INSUFFICIENT_BALANCE'); } return await createPayoutOrder(token, orgId, merchantId, payoutData); } ``` ### 2. Verify Bank Account Details - Validate bank code exists for country - Verify account number format - Confirm account holder name matches contact - Test with small amount first ### 3. Use Idempotent External IDs {/* Multi-language code examples */} {/* Idempotency */} ```javascript // Use invoice ID or unique reference const externalId = `payout-invoice-${invoiceId}`; // Safe to retry - same externalId returns same order const payout = await createPayoutOrder(token, orgId, merchantId, { ...payoutData, externalId: externalId }); ``` ### 4. Handle Failures Gracefully {/* Multi-language code examples */} {/* Error Handling */} ```javascript try { const payout = await createPayoutOrder(token, orgId, merchantId, payoutData); } catch (error) { if (error.response?.data?.code === 'INSUFFICIENT_BALANCE') { // Handle insufficient balance await notifyAdminLowBalance(); } else if (error.response?.data?.code === 'INVALID_BANK_ACCOUNT') { // Handle invalid bank details await requestBankAccountUpdate(providerId); } else { // Log and retry later await queuePayoutForRetry(payoutData); } } ``` --- ## Next Steps Common use cases and examples Test payouts in sandbox Complete webhook integration Advanced error handling --- # Balance Transfers _Currency exchange between virtual accounts_ Source: https://docs.koywe.com/en/balance-management # Balance Transfers (Currency Exchange) Transfer funds between different currency virtual accounts instantly. ## What is BALANCE_TRANSFER? **BALANCE_TRANSFER** allows you to convert funds from one currency to another within your virtual accounts - instantly and without external bank transfers. **Use cases**: - Convert COP to USD for international payments - Rebalance currency holdings - Lock in favorable exchange rates - Prepare funds for specific currency payouts --- ## How It Works |Debit| B[Exchange] B -->|Apply Rate| C[Convert] C -->|Credit| D[Virtual Account USD] style A fill:#005544,stroke:#C9FF1F,stroke-width:2px,color:#fff style D fill:#005544,stroke:#C9FF1F,stroke-width:2px,color:#fff`} /> **Process**: 1. Check source currency balance 2. Get exchange rate quote 3. Create BALANCE_TRANSFER order 4. Funds instantly transferred between accounts --- ## Quick Example {/* Multi-language code examples */} {/* Node.js */} ```javascript // Convert 1,000,000 COP to USD // 1. Get quote const quote = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/quotes`, { orderType: 'BALANCE_TRANSFER', executable: true, originCurrencySymbol: 'COP', destinationCurrencySymbol: 'USD', amountIn: 1000000 }, { headers: { 'Authorization': `Bearer ${token}` } } ); console.log('Exchange rate:', quote.data.exchangeRate); // e.g., 4000 COP per USD console.log('Will receive:', quote.data.finalAmountOut, 'USD'); // e.g., 248.75 USD console.log('Fee:', quote.data.fee, 'COP'); // e.g., 5000 COP // 2. Create transfer order const order = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/orders`, { type: 'BALANCE_TRANSFER', originCurrencySymbol: 'COP', destinationCurrencySymbol: 'USD', amountIn: 1000000, quoteId: quote.data.id // Lock the rate }, { headers: { 'Authorization': `Bearer ${token}` } } ); console.log('Transfer completed:', order.data.status); // "COMPLETED" ``` **Instant Settlement**: BALANCE_TRANSFER orders complete immediately - no waiting for bank confirmations! --- ## Step-by-Step Integration ### Step 1: Check Source Balance {/* Multi-language code examples */} {/* Check Balance */} ```javascript const balances = await axios.get( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/accounts/balances`, { headers: { 'Authorization': `Bearer ${token}` } } ); const copBalance = balances.data.find(b => b.currencySymbol === 'COP'); if (copBalance.availableBalance < 1000000) { throw new Error('Insufficient COP balance'); } ``` ### Step 2: Get Exchange Rate Quote {/* Multi-language code examples */} {/* Get Quote */} ```javascript const quote = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/quotes`, { orderType: 'BALANCE_TRANSFER', executable: true, originCurrencySymbol: 'COP', destinationCurrencySymbol: 'USD', amountIn: 1000000 }, { headers: { 'Authorization': `Bearer ${token}` } } ); // Show rate to user console.log(`Rate: 1 USD = ${quote.data.exchangeRate} COP`); console.log(`You'll receive: ${quote.data.finalAmountOut} USD`); console.log(`Fee: ${quote.data.fee} COP`); console.log(`Quote expires in: ${quote.data.validForSeconds} seconds`); ``` ### Step 3: Create Transfer Order {/* Multi-language code examples */} {/* Create Transfer */} ```javascript const order = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/orders`, { type: 'BALANCE_TRANSFER', originCurrencySymbol: 'COP', destinationCurrencySymbol: 'USD', amountIn: 1000000, quoteId: quote.data.id, // Use quote to lock rate externalId: `transfer-${Date.now()}`, description: 'COP to USD conversion' }, { headers: { 'Authorization': `Bearer ${token}` } } ); console.log('Status:', order.data.status); // "COMPLETED" (instant) ``` ### Step 4: Verify New Balances {/* Multi-language code examples */} {/* Check Balances */} ```javascript const newBalances = await axios.get( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/accounts/balances`, { headers: { 'Authorization': `Bearer ${token}` } } ); const copBalance = newBalances.data.find(b => b.currencySymbol === 'COP'); const usdBalance = newBalances.data.find(b => b.currencySymbol === 'USD'); console.log('COP balance:', copBalance.availableBalance); // Decreased console.log('USD balance:', usdBalance.availableBalance); // Increased ``` --- ## Supported Currency Pairs BALANCE_TRANSFER supports **every combination of Koywe's 8 core currencies**: **ARS Β· BOB Β· BRL Β· CLP Β· COP Β· MXN Β· PEN Β· USD** Any of these can be the origin or the destination, in either direction β€” local currency to USD (`COP β†’ USD`), USD to local currency (`USD β†’ BRL`), or local currency to local currency (`CLP β†’ MXN`). There are no restricted pairs. Most common: Local currency ↔ USD --- ## Complete Example {/* Multi-language code examples */} {/* Full Transfer Flow */} ```javascript async function transferCurrency(fromCurrency, toCurrency, amount) { try { // 1. Authenticate const authResponse = await axios.post( 'https://api-sandbox.koywe.com/api/v1/auth/sign-in', { apiKey: process.env.KOYWE_API_KEY, secret: process.env.KOYWE_SECRET } ); const token = authResponse.data.token; // 2. Check source balance const balances = await axios.get( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/accounts/balances`, { headers: { 'Authorization': `Bearer ${token}` } } ); const sourceBalance = balances.data.find(b => b.currencySymbol === fromCurrency); if (!sourceBalance || sourceBalance.availableBalance < amount) { throw new Error(`Insufficient ${fromCurrency} balance`); } console.log(`βœ“ Sufficient balance: ${sourceBalance.availableBalance} ${fromCurrency}`); // 3. Get quote const quoteResponse = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/quotes`, { orderType: 'BALANCE_TRANSFER', executable: true, originCurrencySymbol: fromCurrency, destinationCurrencySymbol: toCurrency, amountIn: amount }, { headers: { 'Authorization': `Bearer ${token}` } } ); const quote = quoteResponse.data; console.log(`βœ“ Quote received`); console.log(` Rate: 1 ${toCurrency} = ${quote.exchangeRate} ${fromCurrency}`); console.log(` Amount out: ${quote.finalAmountOut} ${toCurrency}`); console.log(` Fee: ${quote.fee} ${fromCurrency}`); // 4. Create transfer const orderResponse = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/orders`, { type: 'BALANCE_TRANSFER', originCurrencySymbol: fromCurrency, destinationCurrencySymbol: toCurrency, amountIn: amount, quoteId: quote.id }, { headers: { 'Authorization': `Bearer ${token}` } } ); const order = orderResponse.data; console.log(`βœ“ Transfer completed instantly`); console.log(` Order ID: ${order.id}`); console.log(` Status: ${order.status}`); return { success: true, orderId: order.id, amountOut: quote.finalAmountOut, currency: toCurrency }; } catch (error) { console.error('Transfer failed:', error.response?.data || error.message); throw error; } } // Usage await transferCurrency('COP', 'USD', 1000000); ``` --- ## Next Steps Learn about exchange rates and quotes Understanding multi-currency balances Test transfers in sandbox Complete API documentation --- # Deposit Accounts _Fund your merchant balance with bank deposit account details_ Source: https://docs.koywe.com/en/balance-management/deposit-accounts # Deposit Accounts Use the bank-income endpoint to retrieve the account details where your merchant should send funds to top up its balance. ## Endpoint - `GET /api/v1/organizations/{organizationId}/merchants/{merchantId}/bankIncome/accounts` This endpoint returns deposit instructions that vary by country and provider. ## What You Receive Depending on the country, the response can contain: - a virtual account number or CLABE/CVU-style identifier - an alias when supported - a routing number for US funding flows - fixed beneficiary bank details for some countries ## Typical Funding Flow ### Fetch deposit instructions Retrieve the merchant's current funding details from the bank-income endpoint. ### Send the bank transfer Initiate the transfer from your treasury or operations bank account. ### Wait for balance credit Once the transfer settles, the merchant balance becomes available for PAYOUT, transfers, or crypto operations. ### Verify balances Confirm the funds with the balances endpoints before creating downstream orders. ## Example ```bash curl -X GET 'https://api-sandbox.koywe.com/api/v1/organizations/YOUR_ORG_ID/merchants/YOUR_MERCHANT_ID/bankIncome/accounts' \ -H 'Authorization: Bearer YOUR_TOKEN' ``` ## Related Balance Endpoints - `GET /api/v1/organizations/{organizationId}/merchants/{merchantId}/accounts/balances` - `GET /api/v1/organizations/{organizationId}/balances` Use the merchant balances endpoint for execution decisions and the organization balances endpoint for a consolidated treasury view. ## Best Practices - Do not cache deposit instructions indefinitely; fetch current details when building an operational workflow. - Match the merchant and organization IDs you use for funding with the same ones you use for downstream payouts. - Wait for confirmed balance credit before creating balance-dependent flows such as PAYOUT or ONRAMP. ## Next Steps - [Balance Transfers](/en/balance-management) - [Paying Providers](/en/paying-providers) - [API Reference](/api-reference) --- # Reports & Reconciliation _Financial reporting and account reconciliation_ Source: https://docs.koywe.com/en/reports # Reports & Reconciliation Koywe provides comprehensive reporting endpoints to help you track, reconcile, and audit all financial activity in your virtual accounts. ## Why Reports Matter **Key Benefits:** - **Reconciliation**: Match your internal records with Koywe's ledger - **Audit Trails**: Maintain complete transaction history for compliance - **Accounting Integration**: Export data for your accounting systems - **Customer Support**: Quickly retrieve transaction details for inquiries --- ## Available Reports | Report | Description | Best For | |--------|-------------|----------| | **Ledger Statement** | Bank-style statement with opening/closing balances and running balance | Monthly reconciliation, balance verification | | **Orders Report** | Transaction-level view of all orders with filtering | Order analysis, status tracking, volume reporting | | **Ledger Entry Details** | Detailed receipt for a single ledger movement | Audit documentation, dispute resolution | --- ## When to Use Which Report B{Reconcile accountbalance?} B -->|Yes| C[Ledger Statement] B -->|No| D{Analyze ordersor transactions?} D -->|Yes| E[Orders Report] D -->|No| F{Proof of singletransaction?} F -->|Yes| G[Ledger Entry Details] F -->|No| H[Start with Ledger Statement] style C fill:#005544,stroke:#C9FF1F,stroke-width:2px,color:#fff style E fill:#005544,stroke:#C9FF1F,stroke-width:2px,color:#fff style G fill:#005544,stroke:#C9FF1F,stroke-width:2px,color:#fff`} /> --- ## Common Use Cases ### Monthly Bank Reconciliation Use the **Ledger Statement** to reconcile your virtual account balance with your internal records: 1. Pull ledger statement for the month 2. Compare opening balance with previous month's closing 3. Verify each movement against your records 4. Confirm closing balance matches current account balance ### Transaction Volume Analysis Use the **Orders Report** to analyze your payment activity: 1. Pull orders report for the period 2. Use summary statistics to see totals by type (PAYIN, PAYOUT, etc.) 3. Filter by status to identify failed or pending transactions 4. Export for business intelligence dashboards ### Audit Documentation Use **Ledger Entry Details** to generate proof of specific transactions: 1. Identify the transaction in your ledger statement 2. Retrieve the `ledgerEntryId` 3. Pull detailed receipt with balance before/after 4. Archive for compliance records --- ## API Hierarchy All report endpoints follow the same hierarchy: ``` /api/v1/organizations/{organizationId}/merchants/{merchantId}/accounts/{accountId}/reports/... ``` | Parameter | Description | Example | |-----------|-------------|---------| | `organizationId` | Your organization ID | `org3_8821b7b4-7f0c-45d9-aa5f-a9ce41ee2f1e` | | `merchantId` | The merchant ID | `mrc_2e8f96ab-dbd5-45f9-b4b6-645945daf340` | | `accountId` | The virtual account ID | `acc_0031c537-2301-40ab-9153-0f7c48505350` | Reports are scoped to a specific **virtual account**. Each account has its own ledger and transaction history. --- ## Authentication All report endpoints require a valid Bearer token: ```bash curl -X GET 'https://api.koywe.com/api/v1/organizations/.../reports/ledger-statement' \ -H 'Authorization: Bearer YOUR_TOKEN' ``` --- ## Next Steps Bank-style account statement with running balances Transaction-level order reporting with filters Detailed receipt for individual transactions --- # Ledger Entry Details _Transaction receipt and proof_ Source: https://docs.koywe.com/en/reports/ledger-entry-details # Ledger Entry Details The Ledger Entry Details endpoint provides a detailed receipt for a specific ledger movement, perfect for audit documentation, customer support, and accounting proof. ## What is a Ledger Entry Receipt? Each movement in your ledger statement has a unique `ledgerEntryId`. Use this ID to retrieve comprehensive details about that specific transaction, including: - **Balance before and after** the movement - **Merchant and account details** - **References** to associated orders or settlements - **Human-readable description** --- ## Use Cases **When to use Ledger Entry Details:** - **Audit documentation**: Generate proof of specific transactions - **Customer support**: Quickly retrieve transaction details for inquiries - **Accounting proof**: Provide detailed receipts for bookkeeping - **Dispute resolution**: Document transaction details for disputes - **Compliance**: Maintain detailed records for regulatory requirements --- ## API Endpoint ``` GET /api/v1/organizations/{organizationId}/merchants/{merchantId}/accounts/{accountId}/reports/ledger-entry/{ledgerEntryId} ``` ### Path Parameters | Parameter | Required | Description | |-----------|----------|-------------| | `organizationId` | Yes | Organization ID | | `merchantId` | Yes | Merchant ID | | `accountId` | Yes | Virtual account ID | | `ledgerEntryId` | Yes | Ledger entry ID (from ledger statement) | --- ## Quick Example {/* Multi-language code examples */} {/* Node.js */} ```javascript const response = await axios.get( `https://api.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/accounts/${accountId}/reports/ledger-entry/137`, { headers: { 'Authorization': `Bearer ${token}` } } ); console.log('Entry ID:', response.data.ledgerEntryId); console.log('Amount:', response.data.amount, response.data.currency); console.log('Balance Before:', response.data.balanceBefore); console.log('Balance After:', response.data.balanceAfter); console.log('Description:', response.data.description); ``` {/* Python */} ```python response = requests.get( f'https://api.koywe.com/api/v1/organizations/{org_id}/merchants/{merchant_id}/accounts/{account_id}/reports/ledger-entry/137', headers={'Authorization': f'Bearer {token}'} ) data = response.json() print(f"Entry ID: {data['ledgerEntryId']}") print(f"Amount: {data['amount']} {data['currency']}") print(f"Balance Before: {data['balanceBefore']}") print(f"Balance After: {data['balanceAfter']}") print(f"Description: {data['description']}") ``` {/* cURL */} ```bash curl -X GET 'https://api.koywe.com/api/v1/organizations/org3_xxx/merchants/mrc_xxx/accounts/acc_xxx/reports/ledger-entry/137' \ -H 'Authorization: Bearer YOUR_TOKEN' ``` --- ## Understanding the Response ```json { "ledgerEntryId": "137", "accountId": "acc_0031c537-2301-40ab-9153-0f7c48505350", "merchantId": "mrc_2e8f96ab-dbd5-45f9-b4b6-645945daf340", "merchantName": "Acme Corporation", "type": "credit", "amount": "1000000.00", "currency": "CLP", "postedAt": "2025-01-15T10:30:00.000Z", "description": "PAYIN from Juan PΓ©rez - Bank transfer received", "category": "PAYIN", "references": { "orderId": "ord_abc123", "settlementId": null }, "balanceBefore": "4000000.00", "balanceAfter": "5000000.00", "generatedAt": "2025-01-31T12:00:00.000Z" } ``` ### Response Fields Explained | Field | Description | |-------|-------------| | `ledgerEntryId` | Unique identifier for this ledger entry | | `merchantId` | Merchant ID associated with the account | | `merchantName` | Human-readable merchant name | | `type` | `credit` (increases balance) or `debit` (decreases balance) | | `amount` | Transaction amount | | `currency` | Currency symbol | | `postedAt` | Timestamp when movement was recorded | | `description` | Human-readable description | | `category` | Movement category (PAYIN, PAYOUT, SETTLEMENT, etc.) | | `references.orderId` | Associated order ID (if applicable) | | `references.settlementId` | Associated settlement ID (if applicable) | | `balanceBefore` | Account balance before this movement | | `balanceAfter` | Account balance after this movement | The `balanceBefore` and `balanceAfter` fields provide audit-ready proof of how the transaction affected the account balance. --- ## Workflow: From Statement to Details The typical workflow is to first retrieve a ledger statement, then get details for specific entries: >API: GET /reports/ledger-statement API-->>App: Statement with movements[] Note over App: Find entry of interestledgerEntryId: "137" App->>API: GET /reports/ledger-entry/137 API-->>App: Detailed receipt Note over App: Use for audit/customer support`} /> --- ## Complete Example: Statement to Details {/* Multi-language code examples */} {/* Node.js */} ```javascript async function getTransactionReceipt(orgId, merchantId, accountId, date, token) { // Step 1: Get ledger statement for the date const statementResponse = await axios.get( `https://api.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/accounts/${accountId}/reports/ledger-statement`, { params: { from: date, to: date }, headers: { 'Authorization': `Bearer ${token}` } } ); const movements = statementResponse.data.movements; console.log(`Found ${movements.length} movements on ${date}`); // Step 2: Get details for each movement const receipts = []; for (const movement of movements) { console.log(`\nFetching details for entry ${movement.ledgerEntryId}...`); const detailResponse = await axios.get( `https://api.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/accounts/${accountId}/reports/ledger-entry/${movement.ledgerEntryId}`, { headers: { 'Authorization': `Bearer ${token}` } } ); const receipt = detailResponse.data; receipts.push(receipt); // Print receipt console.log('='.repeat(50)); console.log('TRANSACTION RECEIPT'); console.log('='.repeat(50)); console.log(`Entry ID: ${receipt.ledgerEntryId}`); console.log(`Date: ${receipt.postedAt}`); console.log(`Merchant: ${receipt.merchantName}`); console.log(`Type: ${receipt.type.toUpperCase()}`); console.log(`Category: ${receipt.category}`); console.log(`Amount: ${receipt.amount} ${receipt.currency}`); console.log('-'.repeat(50)); console.log(`Balance Before: ${receipt.balanceBefore} ${receipt.currency}`); console.log(`Balance After: ${receipt.balanceAfter} ${receipt.currency}`); console.log('-'.repeat(50)); console.log(`Description: ${receipt.description}`); if (receipt.references.orderId) { console.log(`Order ID: ${receipt.references.orderId}`); } if (receipt.references.settlementId) { console.log(`Settlement ID: ${receipt.references.settlementId}`); } console.log('='.repeat(50)); } return receipts; } // Usage: Get all receipts for January 15, 2025 const receipts = await getTransactionReceipt( 'org3_xxx', 'mrc_xxx', 'acc_xxx', '2025-01-15', token ); ``` {/* Python */} ```python def get_transaction_receipt(org_id, merchant_id, account_id, date, token): # Step 1: Get ledger statement for the date statement_response = requests.get( f'https://api.koywe.com/api/v1/organizations/{org_id}/merchants/{merchant_id}/accounts/{account_id}/reports/ledger-statement', params={'from': date, 'to': date}, headers={'Authorization': f'Bearer {token}'} ) movements = statement_response.json()['movements'] print(f"Found {len(movements)} movements on {date}") # Step 2: Get details for each movement receipts = [] for movement in movements: print(f"\nFetching details for entry {movement['ledgerEntryId']}...") detail_response = requests.get( f"https://api.koywe.com/api/v1/organizations/{org_id}/merchants/{merchant_id}/accounts/{account_id}/reports/ledger-entry/{movement['ledgerEntryId']}", headers={'Authorization': f'Bearer {token}'} ) receipt = detail_response.json() receipts.append(receipt) # Print receipt print('=' * 50) print('TRANSACTION RECEIPT') print('=' * 50) print(f"Entry ID: {receipt['ledgerEntryId']}") print(f"Date: {receipt['postedAt']}") print(f"Merchant: {receipt['merchantName']}") print(f"Type: {receipt['type'].upper()}") print(f"Category: {receipt['category']}") print(f"Amount: {receipt['amount']} {receipt['currency']}") print('-' * 50) print(f"Balance Before: {receipt['balanceBefore']} {receipt['currency']}") print(f"Balance After: {receipt['balanceAfter']} {receipt['currency']}") print('-' * 50) print(f"Description: {receipt['description']}") if receipt['references'].get('orderId'): print(f"Order ID: {receipt['references']['orderId']}") if receipt['references'].get('settlementId'): print(f"Settlement ID: {receipt['references']['settlementId']}") print('=' * 50) return receipts # Usage receipts = get_transaction_receipt( 'org3_xxx', 'mrc_xxx', 'acc_xxx', '2025-01-15', token ) ``` --- ## Finding the Ledger Entry ID The `ledgerEntryId` is available in the ledger statement response. Here's how to find a specific entry: {/* Multi-language code examples */} {/* By Order ID */} ```javascript // Find ledger entry by associated order const statement = await getLedgerStatement(orgId, merchantId, accountId, from, to, token); const targetOrderId = 'ord_abc123'; const entry = statement.movements.find(m => m.orderId === targetOrderId); if (entry) { console.log(`Found entry ${entry.ledgerEntryId} for order ${targetOrderId}`); // Now fetch details const receipt = await getLedgerEntryDetails(orgId, merchantId, accountId, entry.ledgerEntryId, token); } ``` {/* By Amount and Date */} ```javascript // Find ledger entry by amount and approximate date const statement = await getLedgerStatement(orgId, merchantId, accountId, from, to, token); const targetAmount = '1000000.00'; const targetDate = '2025-01-15'; const entry = statement.movements.find(m => m.amount === targetAmount && m.postedAt.startsWith(targetDate) ); if (entry) { console.log(`Found entry ${entry.ledgerEntryId}`); } ``` --- ## Error Handling **Common Errors:** - `404 Not Found`: Ledger entry ID doesn't exist or doesn't belong to the specified account - `403 Forbidden`: Entry doesn't belong to the merchant or insufficient permissions - `401 Unauthorized`: Invalid or expired token {/* Multi-language code examples */} {/* Error Handling */} ```javascript async function safeGetLedgerEntry(orgId, merchantId, accountId, entryId, token) { try { const response = await axios.get( `https://api.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/accounts/${accountId}/reports/ledger-entry/${entryId}`, { headers: { 'Authorization': `Bearer ${token}` } } ); return response.data; } catch (error) { if (error.response?.status === 404) { console.error(`Ledger entry ${entryId} not found`); return null; } if (error.response?.status === 403) { console.error(`Access denied to ledger entry ${entryId}`); return null; } throw error; } } ``` --- ## Next Steps Get the full account statement Transaction-level reporting --- # Ledger Statement _Bank-style account statement_ Source: https://docs.koywe.com/en/reports/ledger-statement # Ledger Statement The Ledger Statement provides a bank-style account statement showing all movements that affected your virtual account balance over a specified period. ## What is a Ledger Statement? Think of it like a **bank statement** for your virtual account: - **Opening Balance**: Your balance at the start of the period - **Movements**: Every debit and credit that occurred - **Running Balance**: Balance after each movement - **Closing Balance**: Your balance at the end of the period $1,000] --> B[+$500 PAYIN] B --> C[Running: $1,500] C --> D[-$200 PAYOUT] D --> E[Running: $1,300] E --> F[Closing Balance$1,300] style A fill:#4a90e2,stroke:#2e5f8a,stroke-width:2px,color:#fff style F fill:#005544,stroke:#C9FF1F,stroke-width:2px,color:#fff`} /> --- ## Key Concepts ### Movement Types | Type | Description | Effect on Balance | |------|-------------|-------------------| | **credit** | Funds added to account | Increases balance | | **debit** | Funds removed from account | Decreases balance | ### Movement Categories | Category | Description | |----------|-------------| | `PAYIN` | Customer payment received | | `PAYOUT` | Provider payment sent | | `BALANCE_TRANSFER` | Currency exchange between accounts | | `SETTLEMENT` | Automatic withdrawal to bank | | `ADJUSTMENT` | Manual balance correction | | `FEE` | Service fee charged | | `TAX` | Tax withholding | | `ONRAMP` | Fiat used to buy crypto | | `OFFRAMP` | Crypto sold for fiat | | `REVERSE` | Transaction reversal | | `OTHER` | Other movement types | --- ## API Endpoint ``` GET /api/v1/organizations/{organizationId}/merchants/{merchantId}/accounts/{accountId}/reports/ledger-statement ``` ### Path Parameters | Parameter | Required | Description | |-----------|----------|-------------| | `organizationId` | Yes | Organization ID | | `merchantId` | Yes | Merchant ID | | `accountId` | Yes | Virtual account ID | ### Query Parameters | Parameter | Required | Default | Description | |-----------|----------|---------|-------------| | `from` | Yes | - | Start date (YYYY-MM-DD) | | `to` | Yes | - | End date (YYYY-MM-DD) | | `granularity` | No | `daily` | Date cutoff: `daily` or `monthly` | | `cursor` | No | - | Pagination cursor from previous response | | `limit` | No | `50` | Items per page (1-100) | --- ## Quick Example {/* Multi-language code examples */} {/* Node.js */} ```javascript const response = await axios.get( `https://api.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/accounts/${accountId}/reports/ledger-statement`, { params: { from: '2025-01-01', to: '2025-01-31' }, headers: { 'Authorization': `Bearer ${token}` } } ); console.log('Opening Balance:', response.data.openingBalance); console.log('Closing Balance:', response.data.closingBalance); console.log('Movements:', response.data.movements.length); ``` {/* Python */} ```python response = requests.get( f'https://api.koywe.com/api/v1/organizations/{org_id}/merchants/{merchant_id}/accounts/{account_id}/reports/ledger-statement', params={ 'from': '2025-01-01', 'to': '2025-01-31' }, headers={'Authorization': f'Bearer {token}'} ) data = response.json() print(f"Opening Balance: {data['openingBalance']}") print(f"Closing Balance: {data['closingBalance']}") print(f"Movements: {len(data['movements'])}") ``` {/* cURL */} ```bash curl -X GET 'https://api.koywe.com/api/v1/organizations/org3_xxx/merchants/mrc_xxx/accounts/acc_xxx/reports/ledger-statement?from=2025-01-01&to=2025-01-31' \ -H 'Authorization: Bearer YOUR_TOKEN' ``` --- ## Understanding the Response ```json { "accountId": "acc_0031c537-2301-40ab-9153-0f7c48505350", "currency": "CLP", "merchantId": "mrc_2e8f96ab-dbd5-45f9-b4b6-645945daf340", "periodStart": "2025-01-01T00:00:00.000Z", "periodEnd": "2025-01-31T23:59:59.999Z", "openingBalance": "4000000.00", "closingBalance": "5500000.00", "movements": [ { "ledgerEntryId": "137", "postedAt": "2025-01-15T10:30:00.000Z", "type": "credit", "amount": "1000000.00", "currency": "CLP", "runningBalance": "5000000.00", "description": "PAYIN from Juan PΓ©rez - Bank transfer received", "orderId": "ord_abc123", "category": "PAYIN" }, { "ledgerEntryId": "142", "postedAt": "2025-01-20T14:15:00.000Z", "type": "debit", "amount": "500000.00", "currency": "CLP", "runningBalance": "4500000.00", "description": "PAYOUT to Supplier Co - Invoice payment", "orderId": "ord_def456", "category": "PAYOUT" } ], "pagination": { "cursor": "eyJpZCI6IjE0MiIsImRhdGUiOiIyMDI1LTAxLTIwVDE0OjE1OjAwLjAwMFoifQ==", "hasMore": true, "limit": 50 }, "generatedAt": "2025-01-31T12:00:00.000Z" } ``` ### Response Fields | Field | Description | |-------|-------------| | `openingBalance` | Balance at period start | | `closingBalance` | Balance at period end | | `movements[]` | Array of individual movements | | `movements[].ledgerEntryId` | Unique ID (use for detailed receipt) | | `movements[].runningBalance` | Balance after this movement | | `movements[].type` | `credit` or `debit` | | `movements[].category` | Movement category (PAYIN, PAYOUT, etc.) | | `pagination.cursor` | Use for next page request | | `pagination.hasMore` | Whether more pages exist | --- ## Pagination The ledger statement uses **cursor-based pagination**. To retrieve all movements: {/* Multi-language code examples */} {/* Node.js */} ```javascript async function getAllMovements(orgId, merchantId, accountId, from, to, token) { const allMovements = []; let cursor = null; do { const response = await axios.get( `https://api.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/accounts/${accountId}/reports/ledger-statement`, { params: { from, to, limit: 100, ...(cursor && { cursor }) }, headers: { 'Authorization': `Bearer ${token}` } } ); const data = response.data; allMovements.push(...data.movements); cursor = data.pagination.hasMore ? data.pagination.cursor : null; console.log(`Fetched ${data.movements.length} movements, total: ${allMovements.length}`); } while (cursor); return allMovements; } // Usage const movements = await getAllMovements(orgId, merchantId, accountId, '2025-01-01', '2025-01-31', token); console.log('Total movements:', movements.length); ``` {/* Python */} ```python def get_all_movements(org_id, merchant_id, account_id, from_date, to_date, token): all_movements = [] cursor = None while True: params = { 'from': from_date, 'to': to_date, 'limit': 100 } if cursor: params['cursor'] = cursor response = requests.get( f'https://api.koywe.com/api/v1/organizations/{org_id}/merchants/{merchant_id}/accounts/{account_id}/reports/ledger-statement', params=params, headers={'Authorization': f'Bearer {token}'} ) data = response.json() all_movements.extend(data['movements']) print(f"Fetched {len(data['movements'])} movements, total: {len(all_movements)}") if not data['pagination']['hasMore']: break cursor = data['pagination']['cursor'] return all_movements # Usage movements = get_all_movements(org_id, merchant_id, account_id, '2025-01-01', '2025-01-31', token) print(f'Total movements: {len(movements)}') ``` --- ## Granularity Options The `granularity` parameter controls how date boundaries are applied: | Granularity | Period Start | Period End | |-------------|--------------|------------| | `daily` | Start of day (00:00:00) | End of day (23:59:59) | | `monthly` | First day of month | Last day of month | Use `monthly` granularity for month-end reconciliation reports to ensure consistent period boundaries. --- ## Complete Integration Example {/* Multi-language code examples */} {/* Node.js */} ```javascript async function generateMonthlyStatement(orgId, merchantId, accountId, year, month, token) { // Calculate date range for the month const from = `${year}-${String(month).padStart(2, '0')}-01`; const lastDay = new Date(year, month, 0).getDate(); const to = `${year}-${String(month).padStart(2, '0')}-${lastDay}`; console.log(`Generating statement for ${from} to ${to}`); // Fetch first page const response = await axios.get( `https://api.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/accounts/${accountId}/reports/ledger-statement`, { params: { from, to, granularity: 'monthly', limit: 100 }, headers: { 'Authorization': `Bearer ${token}` } } ); const statement = response.data; // Summary console.log('='.repeat(50)); console.log('MONTHLY STATEMENT'); console.log('='.repeat(50)); console.log(`Account: ${statement.accountId}`); console.log(`Currency: ${statement.currency}`); console.log(`Period: ${statement.periodStart} to ${statement.periodEnd}`); console.log('-'.repeat(50)); console.log(`Opening Balance: ${statement.openingBalance}`); console.log(`Closing Balance: ${statement.closingBalance}`); console.log('-'.repeat(50)); // Calculate totals let totalCredits = 0; let totalDebits = 0; statement.movements.forEach(m => { const amount = parseFloat(m.amount); if (m.type === 'credit') { totalCredits += amount; } else { totalDebits += amount; } console.log(`${m.postedAt} | ${m.type.toUpperCase().padEnd(6)} | ${m.amount.padStart(15)} | ${m.category} | ${m.description.substring(0, 30)}`); }); console.log('-'.repeat(50)); console.log(`Total Credits: ${totalCredits.toFixed(2)}`); console.log(`Total Debits: ${totalDebits.toFixed(2)}`); console.log(`Net Change: ${(totalCredits - totalDebits).toFixed(2)}`); console.log('='.repeat(50)); return statement; } // Usage: Generate January 2025 statement const statement = await generateMonthlyStatement( 'org3_xxx', 'mrc_xxx', 'acc_xxx', 2025, 1, token ); ``` --- ## Next Steps Transaction-level order reporting Get detailed receipt for a movement Understanding account balances --- # Orders Report _Transaction-level order reporting_ Source: https://docs.koywe.com/en/reports/orders-report # Orders Report The Orders Report provides a transaction-level view of all orders associated with a virtual account, with powerful filtering and summary statistics. ## What is the Orders Report? While the **Ledger Statement** shows balance movements, the **Orders Report** shows the underlying **orders/transactions** that caused those movements. | Ledger Statement | Orders Report | |------------------|---------------| | Balance-focused | Transaction-focused | | Shows debits/credits | Shows order details | | Running balance | Order status tracking | | For reconciliation | For transaction analysis | --- ## Key Concepts ### Order Types | Type | Description | |------|-------------| | `PAYIN` | Customer payment received | | `PAYOUT` | Provider payment sent | | `ONRAMP` | Fiat converted to crypto | | `OFFRAMP` | Crypto converted to fiat | | `BALANCE_TRANSFER` | Currency exchange between accounts | | `PAYMENT_LINK` | Payment via payment link | ### Order Statuses | Status | Description | |--------|-------------| | `DRAFT` | Order created but not submitted | | `PENDING` | Awaiting payment or processing | | `PAID` | Payment received, processing | | `PROCESSING` | Being processed | | `COMPLETED` | Successfully completed | | `FAILED` | Failed to complete | | `CANCELLED` | Cancelled by user or system | | `REFUNDED` | Payment refunded | | `REFUND_REQUESTED` | Refund in progress | | `EXPIRED` | Order expired | | `ON_HOLD` | Temporarily held | --- ## API Endpoint ``` GET /api/v1/organizations/{organizationId}/merchants/{merchantId}/accounts/{accountId}/reports/orders ``` ### Path Parameters | Parameter | Required | Description | |-----------|----------|-------------| | `organizationId` | Yes | Organization ID | | `merchantId` | Yes | Merchant ID | | `accountId` | Yes | Virtual account ID | ### Query Parameters | Parameter | Required | Default | Description | |-----------|----------|---------|-------------| | `from` | Yes | - | Start date (YYYY-MM-DD) | | `to` | Yes | - | End date (YYYY-MM-DD) | | `type` | No | - | Filter by order type | | `status` | No | - | Filter by order status | | `cursor` | No | - | Pagination cursor | | `limit` | No | `50` | Items per page (1-100) | --- ## Quick Example {/* Multi-language code examples */} {/* Node.js */} ```javascript const response = await axios.get( `https://api.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/accounts/${accountId}/reports/orders`, { params: { from: '2025-01-01', to: '2025-01-31' }, headers: { 'Authorization': `Bearer ${token}` } } ); console.log('Total Orders:', response.data.summary.totalOrders); console.log('By Type:', response.data.summary.byType); console.log('By Status:', response.data.summary.byStatus); ``` {/* Python */} ```python response = requests.get( f'https://api.koywe.com/api/v1/organizations/{org_id}/merchants/{merchant_id}/accounts/{account_id}/reports/orders', params={ 'from': '2025-01-01', 'to': '2025-01-31' }, headers={'Authorization': f'Bearer {token}'} ) data = response.json() print(f"Total Orders: {data['summary']['totalOrders']}") print(f"By Type: {data['summary']['byType']}") print(f"By Status: {data['summary']['byStatus']}") ``` {/* cURL */} ```bash curl -X GET 'https://api.koywe.com/api/v1/organizations/org3_xxx/merchants/mrc_xxx/accounts/acc_xxx/reports/orders?from=2025-01-01&to=2025-01-31' \ -H 'Authorization: Bearer YOUR_TOKEN' ``` --- ## Filtering by Type Retrieve only specific order types: {/* Multi-language code examples */} {/* Node.js */} ```javascript // Get only PAYIN orders const payinOrders = await axios.get( `https://api.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/accounts/${accountId}/reports/orders`, { params: { from: '2025-01-01', to: '2025-01-31', type: 'PAYIN' }, headers: { 'Authorization': `Bearer ${token}` } } ); console.log('PAYIN orders:', payinOrders.data.orders.length); // Get only PAYOUT orders const payoutOrders = await axios.get( `https://api.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/accounts/${accountId}/reports/orders`, { params: { from: '2025-01-01', to: '2025-01-31', type: 'PAYOUT' }, headers: { 'Authorization': `Bearer ${token}` } } ); console.log('PAYOUT orders:', payoutOrders.data.orders.length); ``` {/* cURL */} ```bash # Get only PAYIN orders curl -X GET 'https://api.koywe.com/api/v1/organizations/org3_xxx/merchants/mrc_xxx/accounts/acc_xxx/reports/orders?from=2025-01-01&to=2025-01-31&type=PAYIN' \ -H 'Authorization: Bearer YOUR_TOKEN' ``` --- ## Filtering by Status Retrieve orders with specific statuses: {/* Multi-language code examples */} {/* Node.js */} ```javascript // Get completed orders only const completedOrders = await axios.get( `https://api.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/accounts/${accountId}/reports/orders`, { params: { from: '2025-01-01', to: '2025-01-31', status: 'COMPLETED' }, headers: { 'Authorization': `Bearer ${token}` } } ); console.log('Completed orders:', completedOrders.data.orders.length); // Get failed orders for investigation const failedOrders = await axios.get( `https://api.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/accounts/${accountId}/reports/orders`, { params: { from: '2025-01-01', to: '2025-01-31', status: 'FAILED' }, headers: { 'Authorization': `Bearer ${token}` } } ); console.log('Failed orders to investigate:', failedOrders.data.orders.length); ``` {/* cURL */} ```bash # Get failed orders curl -X GET 'https://api.koywe.com/api/v1/organizations/org3_xxx/merchants/mrc_xxx/accounts/acc_xxx/reports/orders?from=2025-01-01&to=2025-01-31&status=FAILED' \ -H 'Authorization: Bearer YOUR_TOKEN' ``` --- ## Understanding the Response ```json { "accountId": "acc_0031c537-2301-40ab-9153-0f7c48505350", "currency": "CLP", "periodStart": "2025-01-01T00:00:00.000Z", "periodEnd": "2025-01-31T23:59:59.999Z", "orders": [ { "orderId": "ord_abc123", "type": "PAYIN", "status": "COMPLETED", "amountIn": "1000000.00", "amountOut": "1000000.00", "originCurrency": "CLP", "destinationCurrency": "CLP", "counterparty": { "name": "Juan PΓ©rez", "identifier": "12345678-9" }, "createdAt": "2025-01-15T10:30:00.000Z", "completedAt": "2025-01-15T10:35:00.000Z", "externalId": "INV-2025-001" }, { "orderId": "ord_def456", "type": "PAYOUT", "status": "COMPLETED", "amountIn": "500000.00", "amountOut": "500000.00", "originCurrency": "CLP", "destinationCurrency": "CLP", "counterparty": { "name": "Supplier Co", "identifier": "98765432-1" }, "createdAt": "2025-01-20T14:00:00.000Z", "completedAt": "2025-01-20T14:15:00.000Z", "externalId": "PO-2025-042" } ], "summary": { "totalOrders": 150, "byType": { "PAYIN": 100, "PAYOUT": 50 }, "byStatus": { "COMPLETED": 140, "PENDING": 5, "FAILED": 5 } }, "pagination": { "cursor": "eyJvcmRlcklkIjoib3JkX2RlZjQ1NiJ9", "hasMore": true, "limit": 50 }, "generatedAt": "2025-01-31T12:00:00.000Z" } ``` ### Response Fields | Field | Description | |-------|-------------| | `orders[]` | Array of order objects | | `orders[].orderId` | Unique order ID | | `orders[].counterparty` | Contact/payer information | | `orders[].externalId` | Your reference ID (if provided) | | `summary.totalOrders` | Total orders in period | | `summary.byType` | Count breakdown by order type | | `summary.byStatus` | Count breakdown by status | --- ## Understanding the Summary The `summary` object provides aggregate statistics for quick analysis: {/* Multi-language code examples */} {/* Node.js */} ```javascript const response = await axios.get( `https://api.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/accounts/${accountId}/reports/orders`, { params: { from: '2025-01-01', to: '2025-01-31' }, headers: { 'Authorization': `Bearer ${token}` } } ); const { summary } = response.data; // Volume analysis console.log('=== Monthly Volume ==='); console.log(`Total Orders: ${summary.totalOrders}`); // By type console.log('\n=== By Type ==='); Object.entries(summary.byType).forEach(([type, count]) => { console.log(`${type}: ${count} orders`); }); // By status console.log('\n=== By Status ==='); Object.entries(summary.byStatus).forEach(([status, count]) => { const percentage = ((count / summary.totalOrders) * 100).toFixed(1); console.log(`${status}: ${count} (${percentage}%)`); }); // Success rate const completed = summary.byStatus.COMPLETED || 0; const failed = summary.byStatus.FAILED || 0; const successRate = (completed / (completed + failed) * 100).toFixed(1); console.log(`\nSuccess Rate: ${successRate}%`); ``` --- ## Reconciliation Tip **Matching Orders to Ledger Entries**: Each ledger movement includes an `orderId` field. Use this to cross-reference between the Orders Report and Ledger Statement. ```javascript // Get ledger statement const ledger = await getLedgerStatement(orgId, merchantId, accountId, from, to, token); // Get orders report const orders = await getOrdersReport(orgId, merchantId, accountId, from, to, token); // Cross-reference ledger.movements.forEach(movement => { if (movement.orderId) { const order = orders.orders.find(o => o.orderId === movement.orderId); if (order) { console.log(`Movement ${movement.ledgerEntryId} matches Order ${order.orderId} (${order.type})`); } } }); ``` --- ## Complete Integration Example {/* Multi-language code examples */} {/* Node.js */} ```javascript async function generateOrdersAnalysis(orgId, merchantId, accountId, from, to, token) { // Fetch all orders (handling pagination) const allOrders = []; let cursor = null; let summary = null; do { const response = await axios.get( `https://api.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/accounts/${accountId}/reports/orders`, { params: { from, to, limit: 100, ...(cursor && { cursor }) }, headers: { 'Authorization': `Bearer ${token}` } } ); allOrders.push(...response.data.orders); summary = response.data.summary; cursor = response.data.pagination.hasMore ? response.data.pagination.cursor : null; } while (cursor); // Generate analysis console.log('='.repeat(60)); console.log('ORDERS ANALYSIS REPORT'); console.log(`Period: ${from} to ${to}`); console.log('='.repeat(60)); // Summary statistics console.log('\nπŸ“Š SUMMARY'); console.log(`Total Orders: ${summary.totalOrders}`); // Type breakdown console.log('\nπŸ“ˆ BY TYPE'); Object.entries(summary.byType).forEach(([type, count]) => { const pct = ((count / summary.totalOrders) * 100).toFixed(1); console.log(` ${type.padEnd(20)} ${String(count).padStart(5)} (${pct}%)`); }); // Status breakdown console.log('\nπŸ“‹ BY STATUS'); Object.entries(summary.byStatus).forEach(([status, count]) => { const pct = ((count / summary.totalOrders) * 100).toFixed(1); console.log(` ${status.padEnd(20)} ${String(count).padStart(5)} (${pct}%)`); }); // Calculate totals by type console.log('\nπŸ’° VOLUME BY TYPE'); const volumeByType = {}; allOrders.forEach(order => { if (!volumeByType[order.type]) { volumeByType[order.type] = 0; } volumeByType[order.type] += parseFloat(order.amountIn); }); Object.entries(volumeByType).forEach(([type, volume]) => { console.log(` ${type.padEnd(20)} ${volume.toLocaleString()}`); }); // Failed orders detail const failedOrders = allOrders.filter(o => o.status === 'FAILED'); if (failedOrders.length > 0) { console.log('\n⚠️ FAILED ORDERS'); failedOrders.forEach(order => { console.log(` ${order.orderId} | ${order.type} | ${order.amountIn} | ${order.createdAt}`); }); } console.log('\n' + '='.repeat(60)); return { orders: allOrders, summary }; } // Usage const analysis = await generateOrdersAnalysis( 'org3_xxx', 'mrc_xxx', 'acc_xxx', '2025-01-01', '2025-01-31', token ); ``` --- ## Next Steps Bank-style balance reconciliation Detailed receipt for transactions --- # Offramp - Sell Crypto _Convert cryptocurrency to fiat_ Source: https://docs.koywe.com/en/crypto-operations/offramp # Offramp - Sell Cryptocurrency Convert cryptocurrency to fiat currency in your virtual account. ## What is OFFRAMP? **OFFRAMP** allows you to sell cryptocurrency and receive fiat funds in your virtual account. **Use cases**: - Convert crypto holdings to fiat - Realize crypto gains - Prepare fiat for operations - Accept crypto payments and convert to fiat ### Deals vs Orders **Important**: OFFRAMP operations use the `/deals` endpoint, not `/orders`. A **deal** represents the intent to sell crypto and **must be paid completely** (no partial payments). The deal creates **orders** that execute the sale. **Key differences from ONRAMP**: - **Deal**: The crypto sale intent (must be funded completely) - **Order**: The actual execution of the sale (created automatically by the deal) - **No partial payments**: OFFRAMP deals must be paid in full, unlike ONRAMP - **Destination**: Deals only need the destination virtual account --- ## Quick Example {/* Multi-language code examples */} {/* Node.js */} ```javascript // Sell 10 USDC for COP // 1. Get quote const quote = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/quotes`, { orderType: 'OFFRAMP', executable: true, originCurrencySymbol: 'USDC', destinationCurrencySymbol: 'COP', amountIn: 10, // 10 USDC network: 'ETHEREUM' // Required for ONRAMP/OFFRAMP }, { headers: { 'Authorization': `Bearer ${token}` } } ); console.log('Will receive:', quote.data.finalAmountOut, 'COP'); // e.g., 39,500 COP console.log('Quote valid for:', quote.data.validForSeconds, 'seconds'); // 2. Create deal (not order!) const deal = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/deals`, { destinationAccountId: 'va_cop_12345', // Your COP virtual account quoteId: quote.data.id }, { headers: { 'Authorization': `Bearer ${token}` } } ); console.log('Deal created:', deal.data.id); console.log('Deal must be paid completely'); // Funds will be credited to COP virtual account after confirmation ``` **Complete Payment Required**: Unlike ONRAMP, OFFRAMP deals must be funded completely. Partial payments are not supported. --- ## Step-by-Step Integration ### Step 1: Get Quote {/* Multi-language code examples */} {/* Get Quote */} ```javascript const quote = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/quotes`, { orderType: 'OFFRAMP', executable: true, originCurrencySymbol: 'USDC', destinationCurrencySymbol: 'COP', amountIn: 10, network: 'ETHEREUM' // Required for ONRAMP/OFFRAMP }, { headers: { 'Authorization': `Bearer ${token}` } } ); // Display to user console.log('Selling:', quote.data.finalAmountIn, 'USDC'); console.log('Will receive:', quote.data.finalAmountOut, 'COP'); console.log('Exchange rate:', quote.data.exchangeRate); console.log('Fee:', quote.data.fee, 'COP'); console.log('Quote expires in:', quote.data.validForSeconds, 'seconds'); ``` ### Step 2: Create Deal {/* Multi-language code examples */} {/* Create Deal */} ```javascript const deal = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/deals`, { destinationAccountId: 'va_cop_12345', // Only need destination virtual account quoteId: quote.data.id }, { headers: { 'Authorization': `Bearer ${token}` } } ); console.log('Deal created:', deal.data.id); console.log('Status:', deal.data.status); // "PENDING" console.log('Deposit address:', deal.data.cryptoDestinationWallet); // Where to send crypto ``` **Simplified Request**: Deals only need the `destinationAccountId` (virtual account) and `quoteId`. The crypto amount and currencies are already defined in the quote. ### Step 3: Send Cryptocurrency After creating the deal, send the cryptocurrency to the provided deposit address: {/* Multi-language code examples */} {/* Get Deposit Info */} ```javascript const dealDetails = await axios.get( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/deals/${deal.data.id}`, { headers: { 'Authorization': `Bearer ${token}` } } ); console.log('Send crypto to:', dealDetails.data.cryptoDestinationWallet); console.log('Amount to send:', dealDetails.data.amountIn, dealDetails.data.originCurrencySymbol); console.log('Network:', dealDetails.data.network); console.log('Must send complete amount - partial payments not allowed'); ``` ### Step 4: Wait for Confirmation {/* Multi-language code examples */} {/* Monitor via Webhooks */} ```javascript app.post('/webhooks/koywe', (req, res) => { const event = JSON.parse(req.body); if (event.type === 'order.paid' && event.data.type === 'OFFRAMP') { console.log('Crypto received and confirmed!'); } if (event.type === 'order.completed' && event.data.type === 'OFFRAMP') { console.log('Fiat credited to virtual account!'); console.log('Amount:', event.data.amountOut, event.data.destinationCurrencySymbol); } res.status(200).send('OK'); }); ``` --- ## Order Flow >K: 1. Create OFFRAMP order K-->>You: 2. Return deposit address You->>B: 3. Send crypto to address B->>K: 4. Confirm transaction K->>V: 5. Credit COP virtual account K->>You: 6. Webhook: order.completed`} /> --- ## Complete Example {/* Multi-language code examples */} {/* Sell USDC */} ```javascript async function sellUSDC(amount) { try { const token = await authenticate(); // 1. Get quote const quote = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/quotes`, { orderType: 'OFFRAMP', executable: true, originCurrencySymbol: 'USDC', destinationCurrencySymbol: 'COP', amountIn: amount, network: 'ETHEREUM' // Required for ONRAMP/OFFRAMP }, { headers: { 'Authorization': `Bearer ${token}` } } ); console.log(`Selling ${amount} USDC for ${quote.data.finalAmountOut} COP`); console.log(`Rate: 1 USDC = ${quote.data.exchangeRate} COP`); // 2. Create offramp order const order = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/orders`, { type: 'OFFRAMP', originCurrencySymbol: 'USDC', destinationCurrencySymbol: 'COP', amountIn: amount, quoteId: quote.data.id }, { headers: { 'Authorization': `Bearer ${token}` } } ); console.log('βœ“ Offramp order created:', order.data.id); console.log('πŸ“ Send USDC to:', order.data.depositAddress); console.log('πŸ’° Amount:', order.data.amountIn, 'USDC'); console.log('🌐 Network:', order.data.network || 'Ethereum'); return { orderId: order.data.id, depositAddress: order.data.depositAddress, amount: order.data.amountIn, currency: order.data.originCurrencySymbol }; } catch (error) { console.error('Error:', error.response?.data || error.message); throw error; } } // Usage const offramp = await sellUSDC(10); console.log('Send your USDC to the deposit address above'); ``` --- ## Important Notes **Blockchain Confirmations**: OFFRAMP orders require blockchain confirmations. Settlement time varies by network: - **Ethereum**: 12-15 minutes (12 confirmations) - **Polygon**: 2-5 minutes (128 confirmations) - **BSC**: 3-5 minutes (15 confirmations) - **Bitcoin**: 30-60 minutes (3 confirmations) **One-time Address**: Each OFFRAMP order generates a unique deposit address. Don't reuse addresses from previous orders. --- ## Next Steps Buy cryptocurrency with fiat Understanding balance management Test crypto operations in sandbox Complete API documentation --- # Onramp - Buy Crypto _Convert fiat to cryptocurrency_ Source: https://docs.koywe.com/en/crypto-operations/onramp # Onramp - Buy Cryptocurrency Convert fiat currency to cryptocurrency using your virtual balance. ## What is ONRAMP? **ONRAMP** allows you to purchase cryptocurrency using fiat funds from your virtual account. **Supported cryptocurrencies**: see [Supported Networks](#supported-networks) below for the exact symbol/network pairs the API accepts. ### Deals vs Orders **Important**: ONRAMP operations use the `/deals` endpoint, not `/orders`. A **deal** represents the intent to buy crypto and can be paid in **full or partially**. Each payment creates one or more **orders** that execute the purchase. **Key differences**: - **Deal**: The crypto purchase intent (can be partially funded) - **Order**: The actual execution of the purchase (created automatically by the deal) - **Partial payments**: You can pay a deal in multiple installments for ONRAMP - **Destination**: Deals only need the destination wallet/account ### Automatic Payment Execution **Balance Required**: By default, you need sufficient balance in your virtual account to close an ONRAMP deal. ONRAMP payments execute **automatically** when there's a credit to your currency balance. **How it works**: 1. Create an ONRAMP deal for X amount of crypto 2. Deal requires Y fiat in your virtual account 3. When your virtual account receives funds (PAYIN, etc.), the deal **automatically executes** 4. Orders are created and crypto is purchased **Pre-approved Merchants**: - Some merchants can operate deals without requiring upfront balance - This allows creating deals before funds are available - Contact Koywe to request pre-approval for this feature --- ## Quick Example {/* Multi-language code examples */} {/* Node.js */} ```javascript // Buy USDC with COP // 1. Get quote const quote = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/quotes`, { orderType: 'ONRAMP', executable: true, originCurrencySymbol: 'COP', destinationCurrencySymbol: 'USDC', amountIn: 50000, // 50,000 COP network: 'ETHEREUM' // Required for ONRAMP/OFFRAMP }, { headers: { 'Authorization': `Bearer ${token}` } } ); console.log('Will receive:', quote.data.finalAmountOut, 'USDC'); // e.g., 12.34 USDC console.log('Fee:', quote.data.fee, 'COP'); console.log('Quote valid for:', quote.data.validForSeconds, 'seconds'); // 2. Create deal (not order!) const deal = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/deals`, { // Id of your CRYPTO account in K3 β€” not a wallet address destinationAccountId: 'acc_77a80cf9-496c-4d72-9c33-fca3c8d5bcfe', quoteId: quote.data.id }, { headers: { 'Authorization': `Bearer ${token}` } } ); console.log('Deal created:', deal.data.id); console.log('Deal will create orders automatically as it executes'); // Monitor deal.status: PENDING β†’ PROCESSING β†’ COMPLETED ``` --- ## Step-by-Step Integration ### Step 1: Get Your Crypto Destination Account ONRAMP deals deliver crypto to a K3 account of `kind: "CRYPTO"`. What the deal needs is that account's **id** (`acc_…`), so look it up first: {/* Multi-language code examples */} {/* Find Crypto Account */} ```javascript // Find your CRYPTO account for the currency and network you're buying const accounts = await axios.get( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/accounts/search`, { params: { kind: 'CRYPTO', currencySymbol: 'USDC', network: 'ETHEREUM' }, headers: { 'Authorization': `Bearer ${token}` } } ); const cryptoAccount = accounts.data[0]; if (!cryptoAccount) { throw new Error('No USDC account on ETHEREUM β€” register or provision one first'); } console.log('Destination account id:', cryptoAccount.id); // acc_… β€” this is what the deal takes console.log('Wallet address:', cryptoAccount.address); // 0x… β€” informational only console.log('Network:', cryptoAccount.network); ``` **Pass the id, not the address**: `destinationAccountId` is a K3 account id (`acc_…`). A wallet address (`0x…`) is not a valid value. **No crypto account yet?** Register your own wallet as an external crypto account with `POST /accounts` and `kind: "CRYPTO"` β€” see [Merchant External Accounts](/en/core-concepts/merchant-external-accounts#adding-an-external-crypto-wallet). Koywe-managed embedded wallets are provisioned with a passkey instead β€” see [Passkeys & Approvals](/en/advanced/passkeys-and-approvals). ### Step 2: Check Fiat Balance **Balance Required**: Unless your merchant is pre-approved, you need sufficient balance to close the deal. The deal will execute automatically when funds are available. {/* Multi-language code examples */} {/* Check Balance */} ```javascript const balances = await axios.get( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/accounts/balances`, { headers: { 'Authorization': `Bearer ${token}` } } ); const copBalance = balances.data.find(b => b.currencySymbol === 'COP'); if (copBalance.availableBalance < 50000) { console.log('Insufficient balance. Deal will execute when funds arrive.'); // For pre-approved merchants, you can continue // For others, you may need to wait for PAYIN to credit the account } ``` ### Step 3: Get Quote {/* Multi-language code examples */} {/* Get Quote */} ```javascript const quote = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/quotes`, { orderType: 'ONRAMP', executable: true, originCurrencySymbol: 'COP', destinationCurrencySymbol: 'USDC', amountIn: 50000, network: 'ETHEREUM' // Required for ONRAMP/OFFRAMP }, { headers: { 'Authorization': `Bearer ${token}` } } ); // Display to user console.log('Exchange rate:', quote.data.exchangeRate); console.log('Will receive:', quote.data.finalAmountOut, 'USDC'); console.log('Fee:', quote.data.fee, 'COP'); console.log('Total cost:', quote.data.finalAmountIn, 'COP'); console.log('Quote expires in:', quote.data.validForSeconds, 'seconds'); ``` ### Step 4: Create Deal {/* Multi-language code examples */} {/* Create Deal */} ```javascript const deal = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/deals`, { destinationAccountId: cryptoAccount.id, // Only need destination (from Step 1) quoteId: quote.data.id }, { headers: { 'Authorization': `Bearer ${token}` } } ); console.log('Deal created:', deal.data.id); console.log('Status:', deal.data.status); // "PENDING" console.log('Deals can be paid in full or partially'); ``` **What happens next**: The deal will automatically create orders as payments are processed. You can fund the deal in one payment or multiple partial payments. ### Step 5: Monitor Deal and Orders {/* Multi-language code examples */} {/* Monitor via Webhooks */} ```javascript // Via webhooks (recommended) app.post('/webhooks/koywe', (req, res) => { const event = JSON.parse(req.body); // Deal status updates if (event.type === 'deal.completed' && event.data.type === 'ONRAMP') { console.log('Deal completed!'); console.log('Deal ID:', event.data.id); } // Orders created by the deal if (event.type === 'order.completed' && event.data.type === 'ONRAMP') { console.log('Crypto received!'); console.log('Order ID:', event.data.id); console.log('Amount:', event.data.amountOut, event.data.destinationCurrencySymbol); } res.status(200).send('OK'); }); ``` {/* Poll Deal Status */} ```javascript // Or poll deal status async function checkDealStatus(dealId) { const deal = await axios.get( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/deals/${dealId}`, { headers: { 'Authorization': `Bearer ${token}` } } ); console.log('Deal status:', deal.data.status); console.log('Orders created:', deal.data.orders); // Array of order IDs return deal.data; } ``` --- ## Supported Networks | Network | Chain | Currencies | | --- | --- | --- | | `ETHEREUM` | Ethereum | `ETH` Β· `USDC` Β· `USDT` | | `POLYGON` | Polygon | `MATIC` Β· `USDC` Β· `USDT` | | `SOLANA` | Solana | `SOL` Β· `USDC` Β· `USDT` | | `BASE` | Base | `ETH` Β· `USDC` Β· `USDT` Β· `EURC` | | `ALGORAND` | Algorand | `USDC` Β· `USDT` | | `TRON` | Tron | `TRX` Β· `USDT` | | `BSC` | BNB Smart Chain | `USDT` | | `BITCOIN` | Bitcoin | `BTC` | Only these pairs are valid. Any other symbol/network combination is rejected by the API. **Network Selection**: Pass `network` on the quote, and make sure your destination crypto account is registered on that same network. --- ## Partial Payments (ONRAMP Only) **Unique to ONRAMP**: Unlike OFFRAMP (which must be paid completely), ONRAMP deals can be paid in **multiple partial payments**. Each partial payment creates an order that purchases the corresponding amount of crypto. ### How Partial Payments Work for 100 USDC] --> B[Pay 50% Now] A --> C[Pay 50% Later] B --> D[Order 1: 50 USDC] C --> E[Order 2: 50 USDC] D --> F[Total: 100 USDC] E --> F style A fill:#005544,stroke:#C9FF1F,stroke-width:2px,color:#fff style F fill:#51cf66,stroke:#2f9e44,stroke-width:2px,color:#fff`} /> ### Example: Partial Payment Deal {/* Multi-language code examples */} {/* Node.js */} ```javascript // Create a deal for 100 USDC const quote = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/quotes`, { orderType: 'ONRAMP', executable: true, originCurrencySymbol: 'COP', destinationCurrencySymbol: 'USDC', amountIn: 400000, // 400,000 COP for ~100 USDC network: 'ETHEREUM' // Required for ONRAMP/OFFRAMP }, { headers: { 'Authorization': `Bearer ${token}` } } ); const deal = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/deals`, { destinationAccountId: 'acc_77a80cf9-496c-4d72-9c33-fca3c8d5bcfe', // Your CRYPTO account id quoteId: quote.data.id }, { headers: { 'Authorization': `Bearer ${token}` } } ); console.log('Deal created for 100 USDC'); console.log('You can now pay this deal in full or partially'); console.log('Each payment will create an order and purchase crypto proportionally'); // Payment 1: Pay 50% now (200,000 COP β†’ ~50 USDC) // Payment 2: Pay 50% later (200,000 COP β†’ ~50 USDC) // Result: 2 orders created, 100 USDC total received ``` **Use Case**: Partial payments are useful for: - Dollar-cost averaging (DCA) strategies - Staged purchases over time - Managing cash flow while building crypto position - Allowing customers to pay in installments --- ## Complete Example {/* Multi-language code examples */} {/* Buy USDC */} ```javascript async function buyUSDC(amount) { try { const token = await authenticate(); // 1. Check COP balance const balances = await getBalances(token, orgId, merchantId); const copBalance = balances.find(b => b.currencySymbol === 'COP'); if (copBalance.availableBalance < amount) { throw new Error('Insufficient COP balance'); } // 2. Find the CRYPTO account that will receive the crypto const accounts = await axios.get( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/accounts/search`, { params: { kind: 'CRYPTO', currencySymbol: 'USDC', network: 'ETHEREUM' }, headers: { 'Authorization': `Bearer ${token}` } } ); const cryptoAccount = accounts.data[0]; if (!cryptoAccount) { throw new Error('No USDC account on ETHEREUM β€” register or provision one first'); } // 3. Get quote const quote = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/quotes`, { orderType: 'ONRAMP', executable: true, originCurrencySymbol: 'COP', destinationCurrencySymbol: 'USDC', amountIn: amount, network: 'ETHEREUM' // Required for ONRAMP/OFFRAMP }, { headers: { 'Authorization': `Bearer ${token}` } } ); console.log(`Converting ${amount} COP to ${quote.data.finalAmountOut} USDC`); console.log(`Rate: 1 USDC = ${quote.data.exchangeRate} COP`); // 4. Create deal const deal = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/deals`, { destinationAccountId: cryptoAccount.id, // Account id, not the address quoteId: quote.data.id }, { headers: { 'Authorization': `Bearer ${token}` } } ); console.log('USDC purchase deal created:', deal.data.id); console.log('Deal will create orders as it executes'); return deal.data; } catch (error) { console.error('Error:', error.response?.data || error.message); throw error; } } // Usage await buyUSDC(50000); // Buy USDC with 50,000 COP ``` --- ## Next Steps Sell cryptocurrency for fiat Understanding crypto order types Test crypto operations in sandbox Complete API documentation --- # Koywe CLI _Command-line tool for the Koywe Platform API β€” designed for developers and AI agents_ Source: https://docs.koywe.com/en/cli # Koywe CLI The **Koywe CLI** (`@koyweforest/cli`) is a command-line tool that covers every operation in the Koywe Platform API: accounts, orders, quotes, deals, contacts, webhooks, policy, reports, and more. It's designed for both **human developers** and **AI agents** (Claude Code and similar). Every command returns a structured JSON envelope, most mutating commands accept a `--schema` flag that prints the full request-body JSON schema, and the CLI exposes its own command catalog via `--commands`. > **Looking for the full command catalog?** Every command is listed on the [CLI Command Reference](/en/cli/reference) page, generated directly from the CLI's own metadata β€” run `npx @koyweforest/cli --commands` for the raw JSON. Or flip the index: the [API ↔ CLI Cross-Reference](/en/cli/api-cross-reference) lets you look up a CLI command by REST endpoint. ## Install or run The fastest way to get started β€” no install needed: ```bash npx @koyweforest/cli --help ``` `npx` is what you'll usually want in CI, sandboxed agents, and one-off terminals β€” it pins the version per invocation and leaves nothing behind. For regular local use, install globally and use the short `koywe` alias: ```bash npm install -g @koyweforest/cli koywe --help ``` Requires Node.js >= 18. **`npx koywe` is not a valid short form.** The npm registry doesn't publish a bare `koywe` package, so `npx koywe …` will 404 on a cold cache. Use the full `npx @koyweforest/cli …` form, or install globally and call `koywe` directly. The rest of this page uses `koywe …` to keep examples readable. If you haven't installed globally, read every `koywe` as `npx @koyweforest/cli`. ## Quick start for agents Zero-to-first-order in three commands: ```bash koywe init # Browser login + auto-create API credentials koywe config set organizationId koywe flow order # Guided: quote β†’ create order β†’ wait for approval ``` `init` and `setup` both open a browser to sign you in on localhost and then provision credentials via the API β€” no copying secrets out of a dashboard. Use `setup` if you don't yet have a merchant; use `init` if you already do. ## Authentication Three lanes, pick one: ### Browser login (humans) ```bash koywe auth browser-login ``` ### CLI starts a local listener The CLI binds a short-lived HTTP server on `127.0.0.1:` and generates a CSRF `state` nonce. ### CLI opens the browser It opens `https://api.koywe.com/api/v1/cli-auth?callback=&state=` in your default browser. ### API validates and redirects `GET /api/v1/cli-auth` verifies that `callback` is a `http://127.0.0.1:/...` URL and redirects the browser to the Koywe dashboard's `/cli-auth` page, carrying the `callback` and `state` through. ### Dashboard authorizes and posts back If you're already signed in, the dashboard page posts a short-lived token to the loopback `callback`, echoing the `state` so the CLI can verify it matches the one it generated. ### CLI caches the token On match, the CLI writes `~/.koywe/.cache/token.json` and the HTTP listener shuts down. You're signed in. The loopback handshake keeps the credential off the clipboard and out of shell history β€” the token never leaves `localhost`. ### API key + secret (CI, agents, scripts) ```bash koywe auth login --api-key YOUR_KEY --secret YOUR_SECRET # target production explicitly: koywe auth login --api-key YOUR_KEY --secret YOUR_SECRET --env production ``` ### Environment variables (zero-config CI / agents) | Variable | Description | |----------|-------------| | `KOYWE_API_KEY` | API key (overrides config) | | `KOYWE_SECRET` | Secret (overrides config) | | `KOYWE_BASE_URL` | Full API base URL override | | `KOYWE_ENV` | Environment: `sandbox` or `production` | | `KOYWE_ORG_ID` | Organization ID | | `KOYWE_MERCHANT_ID` | Merchant ID | The token is cached at `~/.koywe/.cache/token.json` and refreshed automatically. ## Command groups at a glance `setup`, `init`, `flow order`, `flow deal` β€” onboarding and guided multi-step operations `auth login/browser-login/me/rotate-secret`, `auth credentials create/create-org/create-pos`, `auth mfa ...`, `config ...` `org list/select/balances/feature-flags`, `merchant list/select/info/create/update` `orders list/info/create/update/confirm/notify` for PAYIN, PAYOUT, ONRAMP, OFFRAMP, BALANCE_TRANSFER, PAYMENT_LINK, INTER_MERCHANT_TRANSFER `quotes create/info`, `deals create/info/list/transfer/confirm` `contacts ...` with sub-commands for accounts and documents, plus `bank-accounts ...` `policy info/create/delete/audit`, `policy rules create/update/delete/reorder`, `policy approvals list/info/approve/reject` β€” required for BALANCE_TRANSFER and MFA-gated operations `webhooks list/info/create/rotate-secret/ping/pause/resume/delete/event-types`, plus `webhooks events list/info/deliveries/replay` and `webhooks subscriptions update` `reports ledger`, `reports orders`, `reports ledger-entry` with cursor pagination `users me/list/org-list/update-roles/org-update-roles`, `users invitations list/create`, `users org-invitations list/create`, `notifications send/send-personal` `pre-onboarding ...`, `onboarding ...`, `documents list` `currencies`, `banks`, `countries`, `payment-methods`, `reference` lookups [See the full command reference β†’](/en/cli/reference) ## Schema-first discovery for agents **Many β€” but not all β€” create/update commands accept `--schema`.** The flag prints the complete JSON schema for the request body. Use this before constructing a payload instead of guessing fields. 26 of the 142 commands in CLI v0.14.1 are `--schema`-capable (look for the πŸ“ marker in the [full command reference](/en/cli/reference)). Among them: ```bash koywe orders create --schema koywe contacts create --schema koywe bank-accounts create --schema koywe policy rules create --schema ``` Note that not every `create` accepts `--schema` β€” for example, `auth credentials create` and `users invitations create` don't (they have bespoke option flags instead). Run `npx @koyweforest/cli --commands | jq '.data.commands[] | select(.hasSchema) | .name'` for the authoritative list. The output includes field names, types, required flags, enums, and per-field descriptions β€” it's the same shape the API validates against. If the schema disagrees with [the OpenAPI spec](/openapi.json), the schema is authoritative (and that's a bug worth reporting). ## Machine-readable command catalog ```bash koywe --commands ``` Emits the full command tree as JSON, with every command's underlying `api.method` and `api.path`, its options, and whether it supports `--schema`. This is the same JSON used to generate the [CLI Command Reference](/en/cli/reference) page, and it's also published at [`/cli-commands.json`](/cli-commands.json). Use it to translate between CLI invocations and direct REST calls, or to auto-generate tooling around the CLI. ## Guided flows `flow` chains multiple API calls into a single invocation. Two flows today: ### `flow order` β€” quote + create + wait ```bash # PAYIN: receive CLP via Khipu koywe flow order \ --type PAYIN \ --origin CLP --destination CLP \ --amount-in 100000 \ --payment-method KHIPU # PAYOUT: send CLP to a pre-registered bank account koywe flow order \ --type PAYOUT \ --origin CLP --destination CLP \ --amount-out 50000 \ --destination-account-id bacc_... # BALANCE_TRANSFER: convert between currencies inside a merchant koywe flow order \ --type BALANCE_TRANSFER \ --origin CLP --destination USDC \ --amount-in 50000 \ --wait ``` Pass `--wait` to poll until the order leaves `ON_HOLD` (useful when a policy rule sends the order into approval). Pass `--mfa-token ` to confirm immediately instead of waiting. ### `flow deal` β€” crypto ONRAMP/OFFRAMP in one go ```bash koywe flow deal \ --type ONRAMP \ --origin USD --destination USDC \ --network ETHEREUM \ --amount-in 100 \ --transfer \ --destination-address 0x... ``` ## Policy β€” required for BALANCE_TRANSFER and MFA-gated operations Many mutating operations (BALANCE_TRANSFER, transferring funds, sensitive config changes) require an active policy with at least one matching rule. Without a policy, the API returns `POL00002` (zero-privilege deny) or `POL00007` (approval required). Minimum viable setup: ```bash # Create an organization-level policy koywe policy create --data '{"name":"default"}' # Add a rule that allows BALANCE_TRANSFER orders koywe policy rules create --schema # see the exact schema koywe policy rules create --data '{ "name": "allow-balance-transfer", "scope": "ORDER", "match": { "orderType": ["BALANCE_TRANSFER"] }, "decision": { "action": "ALLOW" } }' ``` Operations gated by `MFA_REQUIRED` approvals return a pending approval ID. Approve via `koywe policy approvals approve ` or with a pre-minted `--mfa-token`. ## Usage patterns ### Create an order from inline JSON ```bash koywe orders create --data '{ "type": "PAYIN", "originCurrencySymbol": "CLP", "destinationCurrencySymbol": "USDC", "amountIn": 100000, "paymentMethods": [{ "method": "KHIPU" }] }' ``` ### From a file ```bash koywe orders create --file order.json ``` ### From stdin (pipe-friendly) ```bash cat order.json | koywe orders create ``` ### List with filters ```bash koywe orders list \ --status COMPLETED --type PAYIN \ --start-date 2025-01-01 \ --format table ``` ## Output formats | Format | Flag | Best for | |--------|------|----------| | **JSON** (default) | `--format json` | Scripts, agents, automation | | **Table** | `--format table` | Human-readable terminal output | JSON output follows a consistent envelope: ```json { "ok": true, "data": { ... }, "meta": { "page": 1, "limit": 20, "total": 42 } } ``` On error, the process exits non-zero and writes a JSON error envelope to **stderr**: ```json { "ok": false, "error": { "code": "BAA00008", "message": "The destination account currency does not match the order destination currency." } } ``` See [Error codes](/en/advanced/error-codes) for the full catalog. ## Exit codes | Code | Meaning | |------|---------| | 0 | Success | | 1 | General error | | 2 | Authentication error | | 3 | Validation error | | 4 | Resource not found | | 5 | Config/context error | ## Global flags | Flag | Description | |------|-------------| | `--format json\|table` | Output format (default: json) | | `--commands` | Print the full command catalog as JSON | | `--schema` | (on create/update commands) Print request-body JSON schema | | `--org-id ` | Organization override | | `--merchant-id ` | Merchant override | ## Profile management Switch between environments without re-authenticating: ```bash koywe config profile add staging --env sandbox koywe config profile add prod --env production koywe config profile use staging koywe config profile list ``` --- # API ↔ CLI Cross-Reference _Every Koywe REST endpoint mapped to the CLI command that calls it_ Source: https://docs.koywe.com/en/cli/api-cross-reference {/* AUTOGENERATED from public/cli-commands.json by scripts/build-api-cross-reference.ts. Do not edit by hand. */} # API ↔ CLI Cross-Reference Every Koywe REST endpoint exposed by `@koyweforest/cli` v0.14.1, indexed by resource. Start here if you already know which HTTP endpoint you want to call and need the matching CLI invocation. The reverse view (indexed by CLI command) is on the [CLI Command Reference](/en/cli/reference) page. The underlying JSON is published at [`/cli-commands.json`](/cli-commands.json). 126 commands map directly onto REST endpoints. 16 commands are CLI-only (local auth, guided flows, config) and are listed at the end. ## Root endpoints ### `applicant-company-roles` | Method | Path | CLI | Description | | --- | --- | --- | --- | | `GET` | `/applicant-company-roles` | `koywe reference company-roles` | List applicant company roles | ### `auth` | Method | Path | CLI | Description | | --- | --- | --- | --- | | `POST` | `/auth/credentials/rotate-secret` | `koywe auth rotate-secret` | Rotate API secret for the current API user | | `GET` | `/auth/me` | `koywe auth me` | Get current user information | | `POST` | `/auth/organizations/{orgId}/api-users/mfa/prepare` | `koywe auth mfa prepare` | Start delegated MFA enrollment by sending your public key | | `POST` | `/auth/organizations/{orgId}/credentials` | `koywe auth credentials create-org` | Create API credentials for an organization | | `POST` | `/auth/organizations/{orgId}/merchants/{merchantId}/credentials` | `koywe auth credentials create` | Create API credentials for a merchant | | `POST` | `/auth/organizations/{orgId}/merchants/{merchantId}/pos/credentials/create` | `koywe auth credentials create-pos` | Create POS credentials for a merchant | | `POST` | `/auth/sign-in` | `koywe auth login` | Authenticate with API key and secret | ### `banks` | Method | Path | CLI | Description | | --- | --- | --- | --- | | `GET` | `/banks` | `koywe banks list` | List available banks | ### `countries` | Method | Path | CLI | Description | | --- | --- | --- | --- | | `GET` | `/countries` | `koywe countries list` | List available countries | | `GET` | `/countries/{countrySymbol}/economic-activities` | `koywe countries economic-activities` | List economic activities for a country | | `GET` | `/countries/{countrySymbol}/states` | `koywe countries states` | List states for a country | | `GET` | `/countries/{countrySymbol}/states/{stateId}` | `koywe countries state-info` | Get state details | | `GET` | `/countries/{countrySymbol}/states/{stateId}/counties` | `koywe countries counties` | List counties for a state | | `GET` | `/countries/{countrySymbol}/states/{stateId}/counties/{countyId}` | `koywe countries county-info` | Get county details | ### `currencies` | Method | Path | CLI | Description | | --- | --- | --- | --- | | `GET` | `/currencies` | `koywe currencies list` | List available currencies | | `GET` | `/currencies/{id}` | `koywe currencies info` | Get currency details by ID | | `GET` | `/currencies/symbol/{symbol}` | `koywe currencies by-symbol` | Get currency by symbol | ### `documents` | Method | Path | CLI | Description | | --- | --- | --- | --- | | `GET` | `/documents` | `koywe documents list` | List available document types | ### `economic-activities` | Method | Path | CLI | Description | | --- | --- | --- | --- | | `GET` | `/economic-activities` | `koywe countries economic-activities-all` | List all economic activities (global, not country-specific) | ### `global-feature-flags` | Method | Path | CLI | Description | | --- | --- | --- | --- | | `GET` | `/global-feature-flags` | `koywe reference feature-flags` | List global feature flags | ### `me` | Method | Path | CLI | Description | | --- | --- | --- | --- | | `POST` | `/me/notifications` | `koywe notifications send-personal` | Send a personal notification | ### `onboarding` | Method | Path | CLI | Description | | --- | --- | --- | --- | | `GET` | `/onboarding/me` | `koywe onboarding me` | Get your onboarding status | ### `organizations` | Method | Path | CLI | Description | | --- | --- | --- | --- | | `GET` | `/organizations` | `koywe org list` | List all organizations | ### `payment-method` | Method | Path | CLI | Description | | --- | --- | --- | --- | | `GET` | `/payment-method` | `koywe payment-methods list` | List available payment methods | | `GET` | `/payment-method/card/merchants/{merchantId}/verify` | `koywe payment-methods verify-card` | Verify card payment method for a merchant | ### `pre-onboarding` | Method | Path | CLI | Description | | --- | --- | --- | --- | | `POST` | `/pre-onboarding/registration-draft` | `koywe pre-onboarding drafts create` | Create a new registration draft | | `GET` | `/pre-onboarding/registration-draft/{draftId}` | `koywe pre-onboarding drafts info` | Get a registration draft by ID | | `PUT` | `/pre-onboarding/registration-draft/{draftId}` | `koywe pre-onboarding drafts update` | Update an existing registration draft (auto-merges with existing data) | | `POST` | `/pre-onboarding/registration-form/{draftId}` | `koywe pre-onboarding drafts submit` | Submit a registration draft for review | | `GET` | `/pre-onboarding/registrations` | `koywe pre-onboarding registrations list` | List all registrations | | `GET` | `/pre-onboarding/registrations/{registrationId}/status` | `koywe pre-onboarding registrations status` | Get registration status | ### `users` | Method | Path | CLI | Description | | --- | --- | --- | --- | | `GET` | `/users/me` | `koywe users me` | Get the authenticated user profile | | `GET` | `/users/organizations/{orgId}/invitations` | `koywe users org-invitations list` | List invitations at the organization level | | `POST` | `/users/organizations/{orgId}/invitations` | `koywe users org-invitations create` | Invite a user at the organization level | | `GET` | `/users/organizations/{orgId}/merchants/{merchantId}/invitations` | `koywe users invitations list` | List invitations for the current merchant | | `POST` | `/users/organizations/{orgId}/merchants/{merchantId}/invitations` | `koywe users invitations create` | Invite a user to the merchant | ## Organization-scoped endpoints Prefix: `/organizations/{orgId}/` ### `balances` | Method | Path | CLI | Description | | --- | --- | --- | --- | | `GET` | `/organizations/{orgId}/balances` | `koywe org balances` | Get organization balances | ### `feature-flags` | Method | Path | CLI | Description | | --- | --- | --- | --- | | `GET` | `/organizations/{orgId}/feature-flags` | `koywe org feature-flags` | Get organization feature flags | ### `merchants` | Method | Path | CLI | Description | | --- | --- | --- | --- | | `GET` | `/organizations/{orgId}/merchants` | `koywe merchant list` | List merchants in the current organization | | `POST` | `/organizations/{orgId}/merchants` | `koywe merchant create` | Create a new merchant | | `DELETE` | `/organizations/{orgId}/merchants/{merchantId}` | `koywe merchant delete` | Delete a merchant | | `GET` | `/organizations/{orgId}/merchants/{merchantId}` | `koywe merchant info` | Get current merchant details | | `PUT` | `/organizations/{orgId}/merchants/{merchantId}` | `koywe merchant update` | Update merchant details | ### `policy` | Method | Path | CLI | Description | | --- | --- | --- | --- | | `DELETE` | `/organizations/{orgId}/policy` | `koywe policy delete` | Delete the current policy | | `GET` | `/organizations/{orgId}/policy` | `koywe policy info` | Get current policy configuration | | `POST` | `/organizations/{orgId}/policy` | `koywe policy create` | Create a new policy | | `GET` | `/organizations/{orgId}/policy/approvals` | `koywe policy approvals list` | List pending policy approvals | | `GET` | `/organizations/{orgId}/policy/approvals/{approvalId}` | `koywe policy approvals info` | Get approval details | | `POST` | `/organizations/{orgId}/policy/approvals/{approvalId}/approve` | `koywe policy approvals approve` | Approve a pending approval (requires MFA) | | `POST` | `/organizations/{orgId}/policy/approvals/{approvalId}/reject` | `koywe policy approvals reject` | Reject a pending approval (requires MFA) | | `GET` | `/organizations/{orgId}/policy/audit` | `koywe policy audit` | View policy audit log | | `POST` | `/organizations/{orgId}/policy/rules` | `koywe policy rules create` | Add a rule to the policy | | `DELETE` | `/organizations/{orgId}/policy/rules/{ruleId}` | `koywe policy rules delete` | Delete a policy rule | | `PUT` | `/organizations/{orgId}/policy/rules/{ruleId}` | `koywe policy rules update` | Update a policy rule | | `PUT` | `/organizations/{orgId}/policy/rules/reorder` | `koywe policy rules reorder` | Reorder policy rules | ### `users` | Method | Path | CLI | Description | | --- | --- | --- | --- | | `GET` | `/organizations/{orgId}/users` | `koywe users org-list` | List users at the organization level | | `PUT` | `/organizations/{orgId}/users` | `koywe users org-update-roles` | Update roles for a user at the organization level | ### `webauthn` | Method | Path | CLI | Description | | --- | --- | --- | --- | | `POST` | `/organizations/{orgId}/webauthn/challenge` | `koywe auth mfa challenge` | Request an MFA challenge nonce | | `POST` | `/organizations/{orgId}/webauthn/challenge``/verify` | `koywe auth mfa challenge-sign` | Full MFA flow: challenge β†’ sign β†’ verify β†’ return mfaToken | | `POST` | `/organizations/{orgId}/webauthn/verify` | `koywe auth mfa verify` | Submit a signed stamp to verify MFA and receive an mfaToken | ### `webhook-event-types` | Method | Path | CLI | Description | | --- | --- | --- | --- | | `GET` | `/organizations/{orgId}/webhook-event-types` | `koywe webhooks event-types` | List available webhook event types, grouped by category | ### `webhook-events` | Method | Path | CLI | Description | | --- | --- | --- | --- | | `GET` | `/organizations/{orgId}/webhook-events` | `koywe webhooks events list` | List webhook events | | `GET` | `/organizations/{orgId}/webhook-events/{eventId}` | `koywe webhooks events info` | Get webhook event details | | `GET` | `/organizations/{orgId}/webhook-events/{eventId}/deliveries` | `koywe webhooks events deliveries` | Get deliveries for a webhook event | | `POST` | `/organizations/{orgId}/webhook-events/{eventId}/replay` | `koywe webhooks events replay` | Replay a webhook event delivery | ### `webhooks` | Method | Path | CLI | Description | | --- | --- | --- | --- | | `GET` | `/organizations/{orgId}/webhooks` | `koywe webhooks list` | List webhooks for the current organization | | `POST` | `/organizations/{orgId}/webhooks` | `koywe webhooks create` | Create a new webhook | | `DELETE` | `/organizations/{orgId}/webhooks/{webhookId}` | `koywe webhooks delete` | Delete a webhook | | `GET` | `/organizations/{orgId}/webhooks/{webhookId}` | `koywe webhooks info` | Get webhook details | | `POST` | `/organizations/{orgId}/webhooks/{webhookId}/pause` | `koywe webhooks pause` | Pause a webhook | | `POST` | `/organizations/{orgId}/webhooks/{webhookId}/ping` | `koywe webhooks ping` | Send a test ping to a webhook | | `POST` | `/organizations/{orgId}/webhooks/{webhookId}/resume` | `koywe webhooks resume` | Resume a paused webhook | | `POST` | `/organizations/{orgId}/webhooks/{webhookId}/rotate-secret` | `koywe webhooks rotate-secret` | Rotate webhook secret | | `PATCH` | `/organizations/{orgId}/webhooks/{webhookId}/subscriptions` | `koywe webhooks subscriptions update` | Update the event types a webhook endpoint is subscribed to | ## Merchant-scoped endpoints Prefix: `/organizations/{orgId}/merchants/{merchantId}/` ### `accounts` | Method | Path | CLI | Description | | --- | --- | --- | --- | | `GET` | `/organizations/{orgId}/merchants/{merchantId}/accounts` | `koywe bank-accounts list` | List external bank accounts | | `POST` | `/organizations/{orgId}/merchants/{merchantId}/accounts` | `koywe bank-accounts create` | Create an external bank account | | `DELETE` | `/organizations/{orgId}/merchants/{merchantId}/accounts/{accountId}` | `koywe bank-accounts delete` | Delete a bank account | | `GET` | `/organizations/{orgId}/merchants/{merchantId}/accounts/{accountId}` | `koywe bank-accounts info` | Get bank account details | | `PUT` | `/organizations/{orgId}/merchants/{merchantId}/accounts/{accountId}` | `koywe bank-accounts update` | Update a bank account | | `GET` | `/organizations/{orgId}/merchants/{merchantId}/accounts/{accountId}/balance` | `koywe bank-accounts balance` | Get balance for a specific account | | `GET` | `/organizations/{orgId}/merchants/{merchantId}/accounts/{accountId}/reports/ledger-entry/{entryId}` | `koywe reports ledger-entry` | Get a specific ledger entry | | `GET` | `/organizations/{orgId}/merchants/{merchantId}/accounts/{accountId}/reports/ledger-statement` | `koywe reports ledger` | Get ledger statement for an account | | `GET` | `/organizations/{orgId}/merchants/{merchantId}/accounts/{accountId}/reports/orders` | `koywe reports orders` | Get orders report for an account | | `GET` | `/organizations/{orgId}/merchants/{merchantId}/accounts/balances` | `koywe bank-accounts balances` | Get balances for all accounts | | `POST` | `/organizations/{orgId}/merchants/{merchantId}/accounts/confirm-mfa` | `koywe bank-accounts confirm-mfa` | Confirm MFA for a pending account approval | | `GET` | `/organizations/{orgId}/merchants/{merchantId}/accounts/search` | `koywe bank-accounts search` | Search bank accounts | ### `bankIncome` | Method | Path | CLI | Description | | --- | --- | --- | --- | | `GET` | `/organizations/{orgId}/merchants/{merchantId}/bankIncome/accounts` | `koywe bank-accounts deposit-info` | Get deposit account information (bank income) | ### `contacts` | Method | Path | CLI | Description | | --- | --- | --- | --- | | `GET` | `/organizations/{orgId}/merchants/{merchantId}/contacts` | `koywe contacts list` | List contacts for the current merchant | | `POST` | `/organizations/{orgId}/merchants/{merchantId}/contacts` | `koywe contacts create` | Create a new contact | | `DELETE` | `/organizations/{orgId}/merchants/{merchantId}/contacts/{contactId}` | `koywe contacts delete` | Delete a contact | | `GET` | `/organizations/{orgId}/merchants/{merchantId}/contacts/{contactId}` | `koywe contacts info` | Get contact details | | `PUT` | `/organizations/{orgId}/merchants/{merchantId}/contacts/{contactId}` | `koywe contacts update` | Update an existing contact | | `GET` | `/organizations/{orgId}/merchants/{merchantId}/contacts/{contactId}/accounts` | `koywe contacts accounts list` | List accounts for a contact | | `POST` | `/organizations/{orgId}/merchants/{merchantId}/contacts/{contactId}/accounts` | `koywe contacts accounts create` | Create an account for a contact | | `DELETE` | `/organizations/{orgId}/merchants/{merchantId}/contacts/{contactId}/accounts/{accountId}` | `koywe contacts accounts delete` | Delete an account for a contact | | `GET` | `/organizations/{orgId}/merchants/{merchantId}/contacts/{contactId}/accounts/{accountId}` | `koywe contacts accounts info` | Get account details for a contact | | `PUT` | `/organizations/{orgId}/merchants/{merchantId}/contacts/{contactId}/accounts/{accountId}` | `koywe contacts accounts update` | Update an account for a contact | | `POST` | `/organizations/{orgId}/merchants/{merchantId}/contacts/{contactId}/accounts/confirm-mfa` | `koywe contacts accounts confirm-mfa` | Confirm MFA for a pending contact account approval | | `DELETE` | `/organizations/{orgId}/merchants/{merchantId}/contacts/{contactId}/documents` | `koywe contacts documents delete` | Delete documents for a contact | ### `deals` | Method | Path | CLI | Description | | --- | --- | --- | --- | | `GET` | `/organizations/{orgId}/merchants/{merchantId}/deals` | `koywe deals list` | List deals for the current merchant | | `POST` | `/organizations/{orgId}/merchants/{merchantId}/deals` | `koywe deals create` | Create a new deal | | `GET` | `/organizations/{orgId}/merchants/{merchantId}/deals/{dealId}` | `koywe deals info` | Get deal details by ID | | `POST` | `/organizations/{orgId}/merchants/{merchantId}/deals/{dealId}/confirm` | `koywe deals confirm` | Confirm MFA for a pending deal approval | | `POST` | `/organizations/{orgId}/merchants/{merchantId}/deals/{dealId}/transfer` | `koywe deals transfer` | Transfer a scheduled deal | ### `features` | Method | Path | CLI | Description | | --- | --- | --- | --- | | `GET` | `/organizations/{orgId}/merchants/{merchantId}/features` | `koywe merchant features` | List features for the current merchant | ### `notifications` | Method | Path | CLI | Description | | --- | --- | --- | --- | | `POST` | `/organizations/{orgId}/merchants/{merchantId}/notifications` | `koywe notifications send` | Send a notification for a merchant | ### `onboarding` | Method | Path | CLI | Description | | --- | --- | --- | --- | | `GET` | `/organizations/{orgId}/merchants/{merchantId}/onboarding/kyb` | `koywe onboarding kyb list` | List KYB processes for a merchant | | `POST` | `/organizations/{orgId}/merchants/{merchantId}/onboarding/kyb` | `koywe onboarding kyb trigger` | Trigger a KYB process for a merchant | | `GET` | `/organizations/{orgId}/merchants/{merchantId}/onboarding/kyb/{kybId}` | `koywe onboarding kyb status` | Get KYB process status | ### `orders` | Method | Path | CLI | Description | | --- | --- | --- | --- | | `GET` | `/organizations/{orgId}/merchants/{merchantId}/orders` | `koywe orders list` | List orders for the current merchant | | `POST` | `/organizations/{orgId}/merchants/{merchantId}/orders` | `koywe orders create` | Create a new order (PAYIN, PAYOUT, ONRAMP, OFFRAMP, BALANCE_TRANSFER, PAYMENT_LINK, INTER_MERCHANT_TRANSFER) | | `GET` | `/organizations/{orgId}/merchants/{merchantId}/orders/{orderId}` | `koywe orders info` | Get order details by ID | | `PATCH` | `/organizations/{orgId}/merchants/{merchantId}/orders/{orderId}` | `koywe orders update` | Update an order (e.g. associate a blockchain tx hash) | | `POST` | `/organizations/{orgId}/merchants/{merchantId}/orders/{orderId}/confirm` | `koywe orders confirm` | Confirm an ON_HOLD order via MFA | | `POST` | `/organizations/{orgId}/merchants/{merchantId}/orders/{orderId}/notification` | `koywe orders notify` | Send a notification for an order | | `GET` | `/organizations/{orgId}/merchants/{merchantId}/orders/{orderId}/sign` | `koywe orders sign prepare` | Get the signing payload for an ON_HOLD order | | `POST` | `/organizations/{orgId}/merchants/{merchantId}/orders/{orderId}/sign/confirm` | `koywe orders sign confirm` | Submit a stamped signature for an order | | `GET` | `/organizations/{orgId}/merchants/{merchantId}/orders/external/{externalId}` | `koywe orders get-by-external-id` | Get order by external (merchant-provided) ID | ### `quotes` | Method | Path | CLI | Description | | --- | --- | --- | --- | | `POST` | `/organizations/{orgId}/merchants/{merchantId}/quotes` | `koywe quotes create` | Create a new quote | | `GET` | `/organizations/{orgId}/merchants/{merchantId}/quotes/{quoteId}` | `koywe quotes info` | Get quote details | ### `users` | Method | Path | CLI | Description | | --- | --- | --- | --- | | `GET` | `/organizations/{orgId}/merchants/{merchantId}/users` | `koywe users list` | List users for the current merchant | | `PUT` | `/organizations/{orgId}/merchants/{merchantId}/users` | `koywe users update-roles` | Update roles for a user at the merchant level | ### `virtual-accounts` | Method | Path | CLI | Description | | --- | --- | --- | --- | | `GET` | `/organizations/{orgId}/merchants/{merchantId}/virtual-accounts` | `koywe bank-accounts virtual list` | List virtual bank accounts | | `POST` | `/organizations/{orgId}/merchants/{merchantId}/virtual-accounts` | `koywe bank-accounts virtual create` | Create a virtual bank account | | `PATCH` | `/organizations/{orgId}/merchants/{merchantId}/virtual-accounts/{accountId}/alias` | `koywe bank-accounts virtual update-alias` | Update virtual account alias | ## CLI-only commands (no REST endpoint) These commands are local to the CLI β€” browser-based auth handshakes, guided multi-step flows, and config management. They orchestrate multiple API calls or manipulate local state, so they don't map one-to-one to a single endpoint. | CLI | Description | | --- | --- | | `koywe auth browser-login` | Authenticate via browser-based login | | `koywe auth mfa keygen` | Generate a P-256 keypair for delegated signing | | `koywe auth mfa sign` | Sign a body with your delegated P-256 key (local operation, no API call) | | `koywe config profile add` | Create a new profile | | `koywe config profile list` | List all profiles | | `koywe config profile remove` | Remove a profile (cannot remove the active profile) | | `koywe config profile use` | Switch to a named profile | | `koywe config reset` | Reset all configuration and cached tokens | | `koywe config set` | Set a configuration value in the active profile | | `koywe config show` | Display current configuration (active profile) | | `koywe flow deal` | Create a crypto deal: get quote β†’ create deal β†’ optionally transfer | | `koywe flow order` | Create an order: get quote β†’ create order β†’ wait for approval if needed | | `koywe init` | Set up CLI credentials: log in via browser, then create API keys via the API | | `koywe merchant select` | Set the active merchant context | | `koywe org select` | Set the active organization context | | `koywe setup` | New user setup: sign up via browser, then submit a pre-onboarding registration | --- # CLI Command Reference _Complete catalog of all 142 commands in @koyweforest/cli v0.14.1, generated from the CLI itself_ Source: https://docs.koywe.com/en/cli/reference {/* AUTOGENERATED from public/cli-commands.json by scripts/build-cli-reference.ts. Do not edit by hand. */} # CLI Command Reference This page lists every command in `@koyweforest/cli` v0.14.1 (142 commands total). It is generated from the CLI's own `--commands` output, so it cannot drift from the shipped tool. > **Tip for agents:** The same JSON that generates this page is published at [`/cli-commands.json`](/cli-commands.json). It includes the underlying API method and path for every command β€” useful when translating between CLI invocations and direct REST calls. Commands marked with a πŸ“ accept `--schema` to print the full JSON schema for their request body. Run the command with `--schema` before constructing a payload: ```bash npx @koyweforest/cli orders create --schema ``` See also: [CLI overview & installation](/en/cli), [API ↔ CLI Cross-Reference](/en/cli/api-cross-reference) (same data indexed by REST endpoint), [Error codes](/en/advanced/error-codes). ## Setup & Init ### `koywe init` Set up CLI credentials: log in via browser, then create API keys via the API | Flag | Description | | --- | --- | | `--env ` | Environment (sandbox\|production) | | `--port ` | Local port for the callback server (default: random) | | `--timeout ` | Timeout in seconds waiting for browser auth (default: 120) | | `--org-id ` | Organization ID (skip auto-selection) | | `--merchant-id ` | Merchant ID (create merchant-scoped credentials instead of org-scoped) | | `--name ` | Credential name (default: "CLI") | | `--roles ` | Roles to assign (default: k3r_operator) | ### `koywe setup` New user setup: sign up via browser, then submit a pre-onboarding registration | Flag | Description | | --- | --- | | `--env ` | Environment (sandbox\|production) | | `--port ` | Local port for the callback server (default: random) | | `--timeout ` | Timeout in seconds waiting for browser auth (default: 120) | | `--data ` | Pre-onboarding draft JSON body (applicant + company) | | `--file ` | JSON file with pre-onboarding draft body | | `--skip-onboarding` | Only authenticate, skip draft creation | | `--schema` | Print expected JSON schema for the pre-onboarding draft body | ## Guided Flows ### `koywe flow deal` Create a crypto deal: get quote β†’ create deal β†’ optionally transfer | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | | `--type ` | Deal type (ONRAMP\|OFFRAMP) | | `--origin ` | Origin currency symbol (e.g. CLP, USDC) | | `--destination ` | Destination currency symbol (e.g. USDC, CLP) | | `--network ` | Blockchain network (ETHEREUM\|POLYGON\|SOLANA\|TRON\|BSC\|BITCOIN\|BASE\|ALGORAND) | | `--destination-account-id ` | Destination crypto account ID | | `--amount-in ` | Amount in origin currency | | `--amount-out ` | Amount in destination currency | | `--contact-id ` | Contact ID | | `--payment-method ` | Payment method for the quote | | `--mfa-token ` | MFA token for deal creation | | `--transfer` | Immediately trigger the transfer after deal creation | | `--destination-email ` | Recipient email (required with --transfer) | | `--destination-address ` | Destination address (required with --transfer) | | `--document-number ` | Recipient document number (optional with --transfer) | ### `koywe flow order` Create an order: get quote β†’ create order β†’ wait for approval if needed | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | | `--type ` | Order type (PAYIN\|PAYOUT\|BALANCE_TRANSFER\|PAYMENT_LINK\|INTER_MERCHANT_TRANSFER) | | `--origin ` | Origin currency symbol (e.g. CLP, USD) | | `--destination ` | Destination currency symbol (e.g. USDC, CLP) | | `--amount-in ` | Amount in origin currency | | `--amount-out ` | Amount in destination currency | | `--destination-account-id ` | Destination account ID (required for PAYOUT) | | `--contact-id ` | Contact ID | | `--payment-method ` | Payment method (e.g. KHIPU, PSE, PIX) | | `--external-id ` | Idempotent external identifier | | `--description ` | Order description | | `--mfa-token ` | MFA token (skip waiting, confirm immediately) | | `--wait` | Poll until order leaves ON_HOLD status (default: false) | | `--poll-interval ` | Seconds between status polls (default: 5) | | `--poll-timeout ` | Max seconds to wait for approval (default: 300) | ## Authentication ### `koywe auth browser-login` Authenticate via browser-based login | Flag | Description | | --- | --- | | `--env ` | Environment (sandbox\|production) | | `--port ` | Local port for the callback server (default: random) | | `--timeout ` | Timeout in seconds waiting for browser auth (default: 120) | ### `koywe auth credentials create` Create API credentials for a merchant **Endpoint:** `POST /auth/organizations/{orgId}/merchants/{merchantId}/credentials` Β· context: `merchant` Β· auth: `mfa-optional` | Flag | Description | | --- | --- | | `--name ` | Credential name | | `--roles ` | Merchant role IDs to assign | | `--org-id ` | Organization ID (overrides context) | | `--merchant-id ` | Merchant ID (overrides context) | | `--mfa-token ` | MFA token for policy-protected operations | ### `koywe auth credentials create-org` Create API credentials for an organization **Endpoint:** `POST /auth/organizations/{orgId}/credentials` Β· context: `org` Β· auth: `mfa-optional` | Flag | Description | | --- | --- | | `--name ` | Credential name | | `--roles ` | Organization role IDs to assign | | `--org-id ` | Organization ID (overrides context) | | `--mfa-token ` | MFA token for policy-protected operations | ### `koywe auth credentials create-pos` πŸ“ Create POS credentials for a merchant **Endpoint:** `POST /auth/organizations/{orgId}/merchants/{merchantId}/pos/credentials/create` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID (overrides context) | | `--merchant-id ` | Merchant ID (overrides context) | | `--data ` | POS credentials JSON body (alias, password) | | `--file ` | JSON file with POS credentials body | | `--schema` | Print expected JSON schema for the request body | Run with `--schema` to print the request-body JSON schema. ### `koywe auth login` Authenticate with API key and secret **Endpoint:** `POST /auth/sign-in` Β· auth: `none` | Flag | Description | | --- | --- | | `--api-key ` | API key (required) | | `--secret ` | API secret (required) | | `--env ` | Environment (sandbox\|production) | ### `koywe auth me` Get current user information **Endpoint:** `GET /auth/me` _No options_ ### `koywe auth mfa challenge` Request an MFA challenge nonce **Endpoint:** `POST /organizations/{orgId}/webauthn/challenge` Β· context: `org` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | ### `koywe auth mfa challenge-sign` Full MFA flow: challenge β†’ sign β†’ verify β†’ return mfaToken **Endpoint:** `POST /organizations/{orgId}/webauthn/challenge + /verify` Β· context: `org` | Flag | Description | | --- | --- | | `--private-key-file ` | Path to PEM private key file | | `--public-key-file ` | Path to PEM public key file | | `--org-id ` | Organization ID | ### `koywe auth mfa keygen` Generate a P-256 keypair for delegated signing | Flag | Description | | --- | --- | | `--private-key-file ` | Private key output path | | `--public-key-file ` | Public key output path | ### `koywe auth mfa prepare` Start delegated MFA enrollment by sending your public key **Endpoint:** `POST /auth/organizations/{orgId}/api-users/mfa/prepare` Β· context: `org` | Flag | Description | | --- | --- | | `--public-key-file ` | Path to PEM public key file | | `--org-id ` | Organization ID | ### `koywe auth mfa sign` Sign a body with your delegated P-256 key (local operation, no API call) | Flag | Description | | --- | --- | | `--private-key-file ` | Path to PEM private key file | | `--public-key-file ` | Path to PEM public key file | ### `koywe auth mfa verify` Submit a signed stamp to verify MFA and receive an mfaToken **Endpoint:** `POST /organizations/{orgId}/webauthn/verify` Β· context: `org` | Flag | Description | | --- | --- | | `--challenge ` | Challenge nonce from the challenge command | | `--stamp-header-name ` | Stamp header name (e.g. X-Stamp-Api-Key) | | `--stamp-header-value ` | Base64url-encoded stamp value | | `--org-id ` | Organization ID | ### `koywe auth rotate-secret` Rotate API secret for the current API user **Endpoint:** `POST /auth/credentials/rotate-secret` _No options_ ## Config & Profiles ### `koywe config profile add` Create a new profile | Flag | Description | | --- | --- | | `--env ` | Environment (sandbox\|production) | | `--api-key ` | API key | | `--secret ` | API secret | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | | `--copy-from ` | Copy settings from an existing profile | ### `koywe config profile list` List all profiles _No options_ ### `koywe config profile remove` Remove a profile (cannot remove the active profile) _No options_ ### `koywe config profile use` Switch to a named profile _No options_ ### `koywe config reset` Reset all configuration and cached tokens _No options_ ### `koywe config set` Set a configuration value in the active profile _No options_ ### `koywe config show` Display current configuration (active profile) _No options_ ## Organizations ### `koywe org balances` Get organization balances **Endpoint:** `GET /organizations/{orgId}/balances` Β· context: `org` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-ids ` | Comma-separated merchant IDs | | `--date ` | Date (ISO 8601) | | `--base-currency ` | Base currency | | `--period ` | Period | | `--interval ` | Interval | | `--include-merchant-breakdown` | Include merchant breakdown | | `--exclude-today` | Exclude today | ### `koywe org feature-flags` Get organization feature flags **Endpoint:** `GET /organizations/{orgId}/feature-flags` Β· context: `org` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | ### `koywe org list` List all organizations **Endpoint:** `GET /organizations` _No options_ ### `koywe org select` Set the active organization context _No options_ ## Merchants ### `koywe merchant create` πŸ“ Create a new merchant **Endpoint:** `POST /organizations/{orgId}/merchants` Β· context: `org` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID (overrides context) | | `--data ` | Merchant JSON body | | `--file ` | JSON file with merchant body | | `--schema` | Print expected JSON schema for the request body | Run with `--schema` to print the request-body JSON schema. ### `koywe merchant delete` Delete a merchant **Endpoint:** `DELETE /organizations/{orgId}/merchants/{merchantId}` Β· context: `org` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID (overrides context) | ### `koywe merchant features` List features for the current merchant **Endpoint:** `GET /organizations/{orgId}/merchants/{merchantId}/features` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID (overrides context) | | `--merchant-id ` | Merchant ID (overrides context) | ### `koywe merchant info` Get current merchant details **Endpoint:** `GET /organizations/{orgId}/merchants/{merchantId}` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID (overrides context) | | `--merchant-id ` | Merchant ID (overrides context) | ### `koywe merchant list` List merchants in the current organization **Endpoint:** `GET /organizations/{orgId}/merchants` Β· context: `org` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID (overrides context) | ### `koywe merchant select` Set the active merchant context _No options_ ### `koywe merchant update` πŸ“ Update merchant details **Endpoint:** `PUT /organizations/{orgId}/merchants/{merchantId}` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID (overrides context) | | `--merchant-id ` | Merchant ID (overrides context) | | `--data ` | JSON body | | `--file ` | JSON file path | | `--schema` | Print expected JSON schema for the request body | Run with `--schema` to print the request-body JSON schema. ## Orders ### `koywe orders confirm` Confirm an ON_HOLD order via MFA **Endpoint:** `POST /organizations/{orgId}/merchants/{merchantId}/orders/{orderId}/confirm` Β· context: `merchant` Β· auth: `mfa-optional` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | | `--mfa-token ` | MFA token | | `--pending-approval-id ` | Pending approval ID | ### `koywe orders create` πŸ“ Create a new order (PAYIN, PAYOUT, ONRAMP, OFFRAMP, BALANCE_TRANSFER, PAYMENT_LINK, INTER_MERCHANT_TRANSFER) **Endpoint:** `POST /organizations/{orgId}/merchants/{merchantId}/orders` Β· context: `merchant` Β· auth: `mfa-optional` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | | `--data ` | Order JSON body | | `--file ` | JSON file with order body | | `--mfa-token ` | MFA token for policy-protected operations | | `--schema` | Print expected JSON schema for the request body | Run with `--schema` to print the request-body JSON schema. ### `koywe orders get-by-external-id` Get order by external (merchant-provided) ID **Endpoint:** `GET /organizations/{orgId}/merchants/{merchantId}/orders/external/{externalId}` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | ### `koywe orders info` Get order details by ID **Endpoint:** `GET /organizations/{orgId}/merchants/{merchantId}/orders/{orderId}` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | ### `koywe orders list` List orders for the current merchant **Endpoint:** `GET /organizations/{orgId}/merchants/{merchantId}/orders` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | | `--status ` | Filter by status (PENDING\|PROCESSING\|COMPLETED\|FAILED\|CANCELLED) | | `--type ` | Filter by type (PAYIN\|PAYOUT\|ONRAMP\|OFFRAMP\|BALANCE_TRANSFER\|PAYMENT_LINK\|INTER_MERCHANT_TRANSFER) | | `--start-date ` | Filter from date (ISO 8601) | | `--end-date ` | Filter to date (ISO 8601) | | `--currency-id ` | Filter by currency ID | | `--account-id ` | Filter by account ID (origin or destination) | | `--origin-account-id ` | Filter by origin account ID | | `--destination-account-id ` | Filter by destination account ID | | `--page ` | Page number | | `--page-size ` | Page size | | `--limit ` | Alias for --page-size | | `--sort ` | Sort order (e.g. createdAt:desc) | ### `koywe orders notify` Send a notification for an order **Endpoint:** `POST /organizations/{orgId}/merchants/{merchantId}/orders/{orderId}/notification` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | | `--type ` | Notification type | ### `koywe orders sign confirm` Submit a stamped signature for an order **Endpoint:** `POST /organizations/{orgId}/merchants/{merchantId}/orders/{orderId}/sign/confirm` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | | `--stamped-body ` | Exact body from sign prepare | | `--stamp-header-name ` | Stamp header name (e.g. X-Stamp-Api-Key) | | `--stamp-header-value ` | Base64url-encoded stamp value | | `--private-key-file ` | Auto-sign: path to PEM private key (replaces --stamp-* flags) | | `--public-key-file ` | Auto-sign: path to PEM public key (replaces --stamp-* flags) | ### `koywe orders sign prepare` Get the signing payload for an ON_HOLD order **Endpoint:** `GET /organizations/{orgId}/merchants/{merchantId}/orders/{orderId}/sign` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | ### `koywe orders update` πŸ“ Update an order (e.g. associate a blockchain tx hash) **Endpoint:** `PATCH /organizations/{orgId}/merchants/{merchantId}/orders/{orderId}` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | | `--tx-hash ` | Blockchain transaction hash | | `--data ` | Order update JSON body | | `--file ` | JSON file with update body | | `--schema` | Print expected JSON schema for the request body | Run with `--schema` to print the request-body JSON schema. ## Quotes ### `koywe quotes create` πŸ“ Create a new quote **Endpoint:** `POST /organizations/{orgId}/merchants/{merchantId}/quotes` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | | `--data ` | Quote JSON body | | `--file ` | JSON file with quote body | | `--schema` | Print expected JSON schema for the request body | Run with `--schema` to print the request-body JSON schema. ### `koywe quotes info` Get quote details **Endpoint:** `GET /organizations/{orgId}/merchants/{merchantId}/quotes/{quoteId}` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | ## Deals ### `koywe deals confirm` Confirm MFA for a pending deal approval **Endpoint:** `POST /organizations/{orgId}/merchants/{merchantId}/deals/{dealId}/confirm` Β· context: `merchant` Β· auth: `mfa-required` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | | `--pending-approval-id ` | Pending approval ID | | `--mfa-token ` | MFA token | ### `koywe deals create` πŸ“ Create a new deal **Endpoint:** `POST /organizations/{orgId}/merchants/{merchantId}/deals` Β· context: `merchant` Β· auth: `mfa-optional` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | | `--data ` | Deal JSON body | | `--file ` | JSON file with deal body | | `--mfa-token ` | MFA token | | `--schema` | Print expected JSON schema for the request body | Run with `--schema` to print the request-body JSON schema. ### `koywe deals info` Get deal details by ID **Endpoint:** `GET /organizations/{orgId}/merchants/{merchantId}/deals/{dealId}` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | ### `koywe deals list` List deals for the current merchant **Endpoint:** `GET /organizations/{orgId}/merchants/{merchantId}/deals` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | | `--page ` | Page number | | `--page-size ` | Page size | | `--limit ` | Alias for --page-size | | `--status ` | Filter by status | ### `koywe deals transfer` πŸ“ Transfer a scheduled deal **Endpoint:** `POST /organizations/{orgId}/merchants/{merchantId}/deals/{dealId}/transfer` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | | `--data ` | Transfer body JSON | | `--file ` | JSON file with transfer body | | `--schema` | Print expected JSON schema for the request body | Run with `--schema` to print the request-body JSON schema. ## Contacts ### `koywe contacts accounts confirm-mfa` Confirm MFA for a pending contact account approval **Endpoint:** `POST /organizations/{orgId}/merchants/{merchantId}/contacts/{contactId}/accounts/confirm-mfa` Β· context: `merchant` Β· auth: `mfa-required` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | | `--pending-approval-id ` | Pending approval ID | | `--mfa-token ` | MFA token | ### `koywe contacts accounts create` πŸ“ Create an account for a contact **Endpoint:** `POST /organizations/{orgId}/merchants/{merchantId}/contacts/{contactId}/accounts` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | | `--data ` | Account JSON body | | `--file ` | JSON file with account body | | `--schema` | Print expected JSON schema for the request body | Run with `--schema` to print the request-body JSON schema. ### `koywe contacts accounts delete` Delete an account for a contact **Endpoint:** `DELETE /organizations/{orgId}/merchants/{merchantId}/contacts/{contactId}/accounts/{accountId}` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | ### `koywe contacts accounts info` Get account details for a contact **Endpoint:** `GET /organizations/{orgId}/merchants/{merchantId}/contacts/{contactId}/accounts/{accountId}` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | ### `koywe contacts accounts list` List accounts for a contact **Endpoint:** `GET /organizations/{orgId}/merchants/{merchantId}/contacts/{contactId}/accounts` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | ### `koywe contacts accounts update` πŸ“ Update an account for a contact **Endpoint:** `PUT /organizations/{orgId}/merchants/{merchantId}/contacts/{contactId}/accounts/{accountId}` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | | `--data ` | Account JSON body | | `--file ` | JSON file with account body | | `--schema` | Print expected JSON schema for the request body | Run with `--schema` to print the request-body JSON schema. ### `koywe contacts create` πŸ“ Create a new contact **Endpoint:** `POST /organizations/{orgId}/merchants/{merchantId}/contacts` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | | `--data ` | Contact JSON body | | `--file ` | JSON file with contact body | | `--schema` | Print expected JSON schema for the request body | Run with `--schema` to print the request-body JSON schema. ### `koywe contacts delete` Delete a contact **Endpoint:** `DELETE /organizations/{orgId}/merchants/{merchantId}/contacts/{contactId}` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | ### `koywe contacts documents delete` πŸ“ Delete documents for a contact **Endpoint:** `DELETE /organizations/{orgId}/merchants/{merchantId}/contacts/{contactId}/documents` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | | `--data ` | Document IDs JSON body | | `--file ` | JSON file with document IDs | | `--schema` | Print expected JSON schema for the request body | Run with `--schema` to print the request-body JSON schema. ### `koywe contacts info` Get contact details **Endpoint:** `GET /organizations/{orgId}/merchants/{merchantId}/contacts/{contactId}` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | ### `koywe contacts list` List contacts for the current merchant **Endpoint:** `GET /organizations/{orgId}/merchants/{merchantId}/contacts` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | | `--search ` | Search by name or identifier | | `--page ` | Page number | | `--page-size ` | Page size | | `--limit ` | Alias for --page-size | ### `koywe contacts update` πŸ“ Update an existing contact **Endpoint:** `PUT /organizations/{orgId}/merchants/{merchantId}/contacts/{contactId}` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | | `--data ` | Contact JSON body | | `--file ` | JSON file with contact body | | `--schema` | Print expected JSON schema for the request body | Run with `--schema` to print the request-body JSON schema. ## Bank Accounts ### `koywe bank-accounts balance` Get balance for a specific account **Endpoint:** `GET /organizations/{orgId}/merchants/{merchantId}/accounts/{accountId}/balance` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | | `--date ` | Date (ISO 8601) | ### `koywe bank-accounts balances` Get balances for all accounts **Endpoint:** `GET /organizations/{orgId}/merchants/{merchantId}/accounts/balances` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | | `--currency ` | Filter by currency | | `--date ` | Date (ISO 8601) | ### `koywe bank-accounts confirm-mfa` Confirm MFA for a pending account approval **Endpoint:** `POST /organizations/{orgId}/merchants/{merchantId}/accounts/confirm-mfa` Β· context: `merchant` Β· auth: `mfa-required` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | | `--pending-approval-id ` | Pending approval ID | | `--mfa-token ` | MFA token | ### `koywe bank-accounts create` πŸ“ Create an external bank account **Endpoint:** `POST /organizations/{orgId}/merchants/{merchantId}/accounts` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | | `--data ` | Bank account JSON body | | `--file ` | JSON file | | `--schema` | Print expected JSON schema for the request body | Run with `--schema` to print the request-body JSON schema. ### `koywe bank-accounts delete` Delete a bank account **Endpoint:** `DELETE /organizations/{orgId}/merchants/{merchantId}/accounts/{accountId}` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | ### `koywe bank-accounts deposit-info` Get deposit account information (bank income) **Endpoint:** `GET /organizations/{orgId}/merchants/{merchantId}/bankIncome/accounts` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | ### `koywe bank-accounts info` Get bank account details **Endpoint:** `GET /organizations/{orgId}/merchants/{merchantId}/accounts/{accountId}` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | ### `koywe bank-accounts list` List external bank accounts **Endpoint:** `GET /organizations/{orgId}/merchants/{merchantId}/accounts` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | ### `koywe bank-accounts search` Search bank accounts **Endpoint:** `GET /organizations/{orgId}/merchants/{merchantId}/accounts/search` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | | `--name ` | Filter by account name | | `--entity ` | Filter by entity | | `--kind ` | Filter by account kind | | `--country-symbol ` | Filter by country symbol | ### `koywe bank-accounts update` πŸ“ Update a bank account **Endpoint:** `PUT /organizations/{orgId}/merchants/{merchantId}/accounts/{accountId}` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | | `--data ` | Account JSON body | | `--file ` | JSON file with account body | | `--schema` | Print expected JSON schema for the request body | Run with `--schema` to print the request-body JSON schema. ### `koywe bank-accounts virtual create` πŸ“ Create a virtual bank account **Endpoint:** `POST /organizations/{orgId}/merchants/{merchantId}/virtual-accounts` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | | `--data ` | Virtual account JSON body | | `--file ` | JSON file | | `--schema` | Print expected JSON schema for the request body | Run with `--schema` to print the request-body JSON schema. ### `koywe bank-accounts virtual list` List virtual bank accounts **Endpoint:** `GET /organizations/{orgId}/merchants/{merchantId}/virtual-accounts` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | ### `koywe bank-accounts virtual update-alias` πŸ“ Update virtual account alias **Endpoint:** `PATCH /organizations/{orgId}/merchants/{merchantId}/virtual-accounts/{accountId}/alias` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | | `--data ` | Alias JSON body | | `--file ` | JSON file | | `--schema` | Print expected JSON schema for the request body | Run with `--schema` to print the request-body JSON schema. ## Policy (MFA & Approvals) ### `koywe policy approvals approve` Approve a pending approval (requires MFA) **Endpoint:** `POST /organizations/{orgId}/policy/approvals/{approvalId}/approve` Β· context: `org` Β· auth: `mfa-required` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--mfa-token ` | MFA token | | `--data ` | Approval JSON body | | `--file ` | JSON file with approval body | ### `koywe policy approvals info` Get approval details **Endpoint:** `GET /organizations/{orgId}/policy/approvals/{approvalId}` Β· context: `org` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | ### `koywe policy approvals list` List pending policy approvals **Endpoint:** `GET /organizations/{orgId}/policy/approvals` Β· context: `org` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Filter by merchant ID | | `--scope ` | Filter by role: mine (approvals awaiting my vote, default), requested (approvals I submitted), all (all org approvals) | | `--operation-type ` | Filter by operation type (e.g. PAYOUT_FIAT, DESTINATION_EDIT) | ### `koywe policy approvals reject` Reject a pending approval (requires MFA) **Endpoint:** `POST /organizations/{orgId}/policy/approvals/{approvalId}/reject` Β· context: `org` Β· auth: `mfa-required` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--mfa-token ` | MFA token | ### `koywe policy audit` View policy audit log **Endpoint:** `GET /organizations/{orgId}/policy/audit` Β· context: `org` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | ### `koywe policy create` πŸ“ Create a new policy **Endpoint:** `POST /organizations/{orgId}/policy` Β· context: `org` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--data ` | Policy JSON body | | `--file ` | JSON file | | `--schema` | Print expected JSON schema for the request body | Run with `--schema` to print the request-body JSON schema. ### `koywe policy delete` Delete the current policy **Endpoint:** `DELETE /organizations/{orgId}/policy` Β· context: `org` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | ### `koywe policy info` Get current policy configuration **Endpoint:** `GET /organizations/{orgId}/policy` Β· context: `org` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | ### `koywe policy rules create` πŸ“ Add a rule to the policy **Endpoint:** `POST /organizations/{orgId}/policy/rules` Β· context: `org` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--data ` | Rule JSON body | | `--file ` | JSON file | | `--schema` | Print expected JSON schema for the request body | Run with `--schema` to print the request-body JSON schema. ### `koywe policy rules delete` Delete a policy rule **Endpoint:** `DELETE /organizations/{orgId}/policy/rules/{ruleId}` Β· context: `org` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | ### `koywe policy rules reorder` πŸ“ Reorder policy rules **Endpoint:** `PUT /organizations/{orgId}/policy/rules/reorder` Β· context: `org` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--data ` | Reorder JSON body | | `--file ` | JSON file | | `--schema` | Print expected JSON schema for the request body | Run with `--schema` to print the request-body JSON schema. ### `koywe policy rules update` πŸ“ Update a policy rule **Endpoint:** `PUT /organizations/{orgId}/policy/rules/{ruleId}` Β· context: `org` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--data ` | Rule JSON body | | `--file ` | JSON file | | `--schema` | Print expected JSON schema for the request body | Run with `--schema` to print the request-body JSON schema. ## Webhooks ### `koywe webhooks create` πŸ“ Create a new webhook **Endpoint:** `POST /organizations/{orgId}/webhooks` Β· context: `org` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--data ` | Webhook JSON body | | `--file ` | JSON file with webhook body | | `--schema` | Print expected JSON schema for the request body | Run with `--schema` to print the request-body JSON schema. ### `koywe webhooks delete` Delete a webhook **Endpoint:** `DELETE /organizations/{orgId}/webhooks/{webhookId}` Β· context: `org` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | ### `koywe webhooks event-types` List available webhook event types, grouped by category **Endpoint:** `GET /organizations/{orgId}/webhook-event-types` Β· context: `org` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | ### `koywe webhooks events deliveries` Get deliveries for a webhook event **Endpoint:** `GET /organizations/{orgId}/webhook-events/{eventId}/deliveries` Β· context: `org` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | ### `koywe webhooks events info` Get webhook event details **Endpoint:** `GET /organizations/{orgId}/webhook-events/{eventId}` Β· context: `org` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | ### `koywe webhooks events list` List webhook events **Endpoint:** `GET /organizations/{orgId}/webhook-events` Β· context: `org` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--page ` | Page number | | `--limit ` | Page size | | `--type ` | Filter by event type (e.g. order.created) | | `--from ` | From date (ISO 8601, inclusive) | | `--to ` | To date (ISO 8601, inclusive) | | `--merchant-id ` | Filter by merchant ID | | `--resource-type ` | Filter by resource type (e.g. order, contact) | | `--resource-id ` | Filter by resource ID (e.g. ord_..., con_...) | ### `koywe webhooks events replay` Replay a webhook event delivery **Endpoint:** `POST /organizations/{orgId}/webhook-events/{eventId}/replay` Β· context: `org` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | ### `koywe webhooks info` Get webhook details **Endpoint:** `GET /organizations/{orgId}/webhooks/{webhookId}` Β· context: `org` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | ### `koywe webhooks list` List webhooks for the current organization **Endpoint:** `GET /organizations/{orgId}/webhooks` Β· context: `org` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | ### `koywe webhooks pause` Pause a webhook **Endpoint:** `POST /organizations/{orgId}/webhooks/{webhookId}/pause` Β· context: `org` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | ### `koywe webhooks ping` Send a test ping to a webhook **Endpoint:** `POST /organizations/{orgId}/webhooks/{webhookId}/ping` Β· context: `org` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | ### `koywe webhooks resume` Resume a paused webhook **Endpoint:** `POST /organizations/{orgId}/webhooks/{webhookId}/resume` Β· context: `org` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | ### `koywe webhooks rotate-secret` Rotate webhook secret **Endpoint:** `POST /organizations/{orgId}/webhooks/{webhookId}/rotate-secret` Β· context: `org` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | ### `koywe webhooks subscriptions update` πŸ“ Update the event types a webhook endpoint is subscribed to **Endpoint:** `PATCH /organizations/{orgId}/webhooks/{webhookId}/subscriptions` Β· context: `org` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--events [events...]` | Event types to subscribe to (space-separated). Pass --events with no values to subscribe to all events. | | `--data ` | Subscription JSON body | | `--file ` | JSON file with subscription body | | `--schema` | Print expected JSON schema for the request body | Run with `--schema` to print the request-body JSON schema. ## Reports ### `koywe reports ledger` Get ledger statement for an account **Endpoint:** `GET /organizations/{orgId}/merchants/{merchantId}/accounts/{accountId}/reports/ledger-statement` Β· context: `merchant` | Flag | Description | | --- | --- | | `--account-id ` | (required) Account ID | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | | `--from ` | Start date (ISO 8601) | | `--to ` | End date (ISO 8601) | | `--cursor ` | Pagination cursor from the previous response | | `--limit ` | Page size limit | ### `koywe reports ledger-entry` Get a specific ledger entry **Endpoint:** `GET /organizations/{orgId}/merchants/{merchantId}/accounts/{accountId}/reports/ledger-entry/{entryId}` Β· context: `merchant` | Flag | Description | | --- | --- | | `--account-id ` | (required) Account ID | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | ### `koywe reports orders` Get orders report for an account **Endpoint:** `GET /organizations/{orgId}/merchants/{merchantId}/accounts/{accountId}/reports/orders` Β· context: `merchant` | Flag | Description | | --- | --- | | `--account-id ` | (required) Account ID | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | | `--from ` | Start date (ISO 8601) | | `--to ` | End date (ISO 8601) | ## Users & Invitations ### `koywe users invitations create` Invite a user to the merchant **Endpoint:** `POST /users/organizations/{orgId}/merchants/{merchantId}/invitations` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | | `--data ` | Invitation JSON body (email, roles) | | `--file ` | JSON file with invitation body | ### `koywe users invitations list` List invitations for the current merchant **Endpoint:** `GET /users/organizations/{orgId}/merchants/{merchantId}/invitations` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | | `--page ` | Page number | | `--page-size ` | Page size | | `--limit ` | Alias for --page-size | ### `koywe users list` List users for the current merchant **Endpoint:** `GET /organizations/{orgId}/merchants/{merchantId}/users` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | | `--page ` | Page number | | `--page-size ` | Page size | | `--limit ` | Alias for --page-size | ### `koywe users me` Get the authenticated user profile **Endpoint:** `GET /users/me` _No options_ ### `koywe users org-invitations create` Invite a user at the organization level **Endpoint:** `POST /users/organizations/{orgId}/invitations` Β· context: `org` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--data ` | Invitation JSON body (email, roles) | | `--file ` | JSON file with invitation body | ### `koywe users org-invitations list` List invitations at the organization level **Endpoint:** `GET /users/organizations/{orgId}/invitations` Β· context: `org` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--page ` | Page number | | `--page-size ` | Page size | | `--limit ` | Alias for --page-size | | `--sort ` | Sort order | ### `koywe users org-list` List users at the organization level **Endpoint:** `GET /organizations/{orgId}/users` Β· context: `org` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--page ` | Page number | | `--page-size ` | Page size | | `--limit ` | Alias for --page-size | ### `koywe users org-update-roles` Update roles for a user at the organization level **Endpoint:** `PUT /organizations/{orgId}/users` Β· context: `org` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--user-id ` | User ID | | `--roles ` | Role names to assign | ### `koywe users update-roles` Update roles for a user at the merchant level **Endpoint:** `PUT /organizations/{orgId}/merchants/{merchantId}/users` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | | `--user-id ` | User ID | | `--roles ` | Role names to assign | ## Notifications ### `koywe notifications send` Send a notification for a merchant **Endpoint:** `POST /organizations/{orgId}/merchants/{merchantId}/notifications` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | | `--data ` | Notification JSON body | | `--file ` | JSON file | ### `koywe notifications send-personal` Send a personal notification **Endpoint:** `POST /me/notifications` | Flag | Description | | --- | --- | | `--data ` | Notification JSON body | | `--file ` | JSON file | ## Pre-Onboarding ### `koywe pre-onboarding drafts create` πŸ“ Create a new registration draft **Endpoint:** `POST /pre-onboarding/registration-draft` | Flag | Description | | --- | --- | | `--data ` | Draft JSON body | | `--file ` | JSON file with draft body | | `--schema` | Print expected JSON schema for the request body | Run with `--schema` to print the request-body JSON schema. ### `koywe pre-onboarding drafts info` Get a registration draft by ID **Endpoint:** `GET /pre-onboarding/registration-draft/{draftId}` _No options_ ### `koywe pre-onboarding drafts submit` Submit a registration draft for review **Endpoint:** `POST /pre-onboarding/registration-form/{draftId}` _No options_ ### `koywe pre-onboarding drafts update` πŸ“ Update an existing registration draft (auto-merges with existing data) **Endpoint:** `PUT /pre-onboarding/registration-draft/{draftId}` | Flag | Description | | --- | --- | | `--data ` | Draft JSON body | | `--file ` | JSON file with draft body | | `--replace` | Replace entire draft instead of merging with existing data | | `--schema` | Print expected JSON schema for the request body | Run with `--schema` to print the request-body JSON schema. ### `koywe pre-onboarding registrations list` List all registrations **Endpoint:** `GET /pre-onboarding/registrations` _No options_ ### `koywe pre-onboarding registrations status` Get registration status **Endpoint:** `GET /pre-onboarding/registrations/{registrationId}/status` _No options_ ## Onboarding ### `koywe onboarding kyb list` List KYB processes for a merchant **Endpoint:** `GET /organizations/{orgId}/merchants/{merchantId}/onboarding/kyb` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | ### `koywe onboarding kyb status` Get KYB process status **Endpoint:** `GET /organizations/{orgId}/merchants/{merchantId}/onboarding/kyb/{kybId}` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | ### `koywe onboarding kyb trigger` πŸ“ Trigger a KYB process for a merchant **Endpoint:** `POST /organizations/{orgId}/merchants/{merchantId}/onboarding/kyb` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | | `--force-retrigger` | Force re-trigger of KYB process | | `--data ` | Request JSON body | | `--file ` | JSON file with request body | | `--schema` | Print expected JSON schema for the request body | Run with `--schema` to print the request-body JSON schema. ### `koywe onboarding me` Get your onboarding status **Endpoint:** `GET /onboarding/me` | Flag | Description | | --- | --- | | `--evaluation-status ` | Filter by evaluation status | | `--setup-status ` | Filter by setup status | ## Documents ### `koywe documents list` List available document types **Endpoint:** `GET /documents` | Flag | Description | | --- | --- | | `--entity-type ` | Filter by entity type | | `--country ` | Filter by country code | | `--scope ` | Filter by scope | ## Payment Methods ### `koywe payment-methods list` List available payment methods **Endpoint:** `GET /payment-method` | Flag | Description | | --- | --- | | `--country-symbol ` | Filter by country code (e.g. CL, BR, MX) | | `--currency-symbol ` | Filter by currency symbol (e.g. CLP, BRL, USDC) | | `--payment-type ` | Filter by payment type (QR, PAYMENT_LINK, PUSH_NOTIFICATION, BANK_TRANSFER) | ### `koywe payment-methods verify-card` Verify card payment method for a merchant **Endpoint:** `GET /payment-method/card/merchants/{merchantId}/verify` Β· context: `merchant` | Flag | Description | | --- | --- | | `--org-id ` | Organization ID | | `--merchant-id ` | Merchant ID | ## Banks ### `koywe banks list` List available banks **Endpoint:** `GET /banks` | Flag | Description | | --- | --- | | `--country ` | Filter by country | ## Countries ### `koywe countries counties` List counties for a state **Endpoint:** `GET /countries/{countrySymbol}/states/{stateId}/counties` _No options_ ### `koywe countries county-info` Get county details **Endpoint:** `GET /countries/{countrySymbol}/states/{stateId}/counties/{countyId}` _No options_ ### `koywe countries economic-activities` List economic activities for a country **Endpoint:** `GET /countries/{countrySymbol}/economic-activities` _No options_ ### `koywe countries economic-activities-all` List all economic activities (global, not country-specific) **Endpoint:** `GET /economic-activities` _No options_ ### `koywe countries list` List available countries **Endpoint:** `GET /countries` | Flag | Description | | --- | --- | | `--operational-status ` | Filter by operational status | | `--scope ` | Filter by scope | ### `koywe countries state-info` Get state details **Endpoint:** `GET /countries/{countrySymbol}/states/{stateId}` _No options_ ### `koywe countries states` List states for a country **Endpoint:** `GET /countries/{countrySymbol}/states` _No options_ ## Currencies ### `koywe currencies by-symbol` Get currency by symbol **Endpoint:** `GET /currencies/symbol/{symbol}` _No options_ ### `koywe currencies info` Get currency details by ID **Endpoint:** `GET /currencies/{id}` _No options_ ### `koywe currencies list` List available currencies **Endpoint:** `GET /currencies` | Flag | Description | | --- | --- | | `--is-fiat ` | Filter by fiat (true/false) | | `--country ` | Filter by country | | `--scope ` | Filter by scope | ## Reference Data ### `koywe reference company-roles` List applicant company roles **Endpoint:** `GET /applicant-company-roles` _No options_ ### `koywe reference feature-flags` List global feature flags **Endpoint:** `GET /global-feature-flags` _No options_ --- # Error Code Catalog _All 685 error codes returned by the Koywe Platform API_ Source: https://docs.koywe.com/en/advanced/error-codes {/* AUTOGENERATED from tmp/api-error-codes.md by scripts/build-error-codes.ts. Do not edit by hand. */} # Error Code Catalog Every error returned by the Koywe Platform API carries a unique `error.code` like `MC00015` or `DC00010`. The uppercase prefix identifies the **domain** (orders, merchants, policy, …); the numeric suffix is the specific error within that domain. This catalog lists all 685 codes currently registered across 51 domains. Use the prefix table below to jump to a domain, or Ctrl-F for a specific code. Start with **Common errors** for the ones integrators hit first. ## Error response shape Every error response follows the same envelope: ```json { "ok": false, "error": { "code": "BAA00008", "message": "The destination account currency does not match the order destination currency." } } ``` Always branch on `error.code` rather than the human-readable `error.message`. Messages may be reworded without notice; codes are stable. ## Prefix β†’ domain | Prefix | Domain | Codes | | --- | --- | ---: | | `AUTH` | Authentication & credentials | 33 | | `BAA` | Bank accounts | 41 | | `BI` | Bank income (inbound deposits) | 7 | | `BLC` | Blockchain | 14 | | `BT` | Balance transfer | 4 | | `CACC` | Crypto accounts | 6 | | `CAPI` | Crypto API | 9 | | `CNT` | Countries | 5 | | `CO` | Compliance | 7 | | `COM` | Common / internal | 2 | | `CRE` | Crypto credentials | 7 | | `CSIG` | Crypto signing | 3 | | `CT` | Contacts | 41 | | `CU` | Currencies | 8 | | `CW` | Crypto whitelist | 3 | | `CWHOOK` | Crypto webhooks | 3 | | `DC` | Documents (validation) | 11 | | `DL` | Deals | 30 | | `EM` | Email delivery | 5 | | `EWW` | Embedded wallet | 20 | | `FC` | Feature configuration | 3 | | `FE` | Fees & commissions | 18 | | `FF` | Merchant feature flags | 4 | | `GFF` | Global feature flags | 3 | | `IMT` | Inter-merchant transfer | 8 | | `INV` | Invitations | 17 | | `KAC` | Koywe account | 4 | | `LE` | Ledger | 17 | | `MC` | Merchants | 42 | | `NE` | Notifications | 8 | | `OB` | Organization balance | 14 | | `OK` | Onboarding KYB | 20 | | `OR` | Orders | 30 | | `ORG` | Organizations | 5 | | `PER` | Permissions | 1 | | `PIVBA` | Pay-in virtual bank accounts | 16 | | `PL` | Payment links | 33 | | `PM` | Payment methods | 2 | | `PMP` | Payment-method providers | 23 | | `PO` | Pre-onboarding | 26 | | `POL` | Policy (MFA & approvals) | 30 | | `PSPC` | PSP conciliation | 12 | | `QE` | Quotes | 23 | | `RE` | Reports | 12 | | `SBX` | Sandbox simulator | 5 | | `SD` | Suite Domestic (Koywe 3) | 7 | | `TK` | Turnkey (embedded wallet provider) | 6 | | `TX` | Transactions | 5 | | `UE` | Users | 1 | | `WA` | WebAuthn / passkeys | 27 | | `WE` | Webhooks | 4 | ## Common errors you'll hit first | Code | Status | Message | What to do | | --- | ---: | --- | --- | | `MC00015` | 403 | Merchant does not belong to the organization | The merchant ID does not belong to the organization in your config/credentials. Re-run `koywe init` or double-check `organizationId` / `merchantId` in your profile. Note: GETs succeed silently; only POSTs fail β€” this is the common sandbox confusion. | | `DC00010` | 400 | Invalid document number for the specified document type and country | The document number does not match the format for the country and document type. Use valid test values in sandbox (see the testing guide for a per-country list). | | `BAA00008` | 400 | The destination account currency does not match the order destination currency. | The destination account currency does not match the order destination currency. Either pick a destination account whose currency matches, or change the order currency. | | `BAA00014` | 409 | Insufficient balance for payout | The merchant balance does not have enough funds in the origin currency for this payout. Fund the virtual account or reduce the amount. | | `POL00002` | 403 | No policy defined for organization - zero privilege enforced | No policy exists on the organization β€” zero-privilege deny. Create one with `npx koywe policy create` and add a matching rule before retrying. | | `POL00003` | 403 | Operation denied by policy rule | An active rule denied this operation. Inspect `npx koywe policy show` / `policy rules list` and either add a matching ALLOW rule or relax the existing DENY. | | `POL00006` | 428 | MFA verification required | MFA verification is required. Run the policy approval / MFA flow (passkey signing or `--mfa-token`), then retry. | | `POL00007` | 428 | Approval required for this operation | A human approval is required before this order executes. The order is held in `ON_HOLD`; pass `--wait` in `flow order` or poll and approve via the dashboard. | | `QE00003` | 410 | Quote timeOut, You need to create a new Quote | The quote expired (they are short-lived). Create a new quote and pass the fresh `quoteId` to the order. | | `QE00004` | 409 | Quote already used in another order | The quote was already consumed by another order. Quotes are single-use; create a new one. | | `BT00002` | 400 | Insufficient balance in origin account | The origin virtual account has insufficient balance for the balance transfer. Fund it or use a smaller amount. | | `OR00015` | 400 | Order has expired and cannot be processed | The order expired before being completed. Create a new order with a later `expirationDate` (or omit it to use the default). | | `PL00011` | 410 | The payment link has expired | The payment link has expired. Create a new PAYMENT_LINK order. | | `PMP00007` | 400 | Payment method amount is greater than the maximum amount | The amount exceeds the payment method provider maximum. Check the min/max bounds for the provider in your region. | | `PIVBA00014` | 409 | Alias modification limit exceeded. You must wait 24 hours before modifying it again | You tried to change the virtual-account alias within 24 h of the last change. Wait or use a different account. | ## Full catalog ### AUTH β€” Authentication & credentials | Code | Status | Message | | --- | ---: | --- | | `AUTH00001` | 401 | Unauthorized | | `AUTH00002` | 409 | Email already exists | | `AUTH00003` | 500 | Failed to create credentials | | `AUTH00004` | 400 | Invalid email format | | `AUTH00005` | 400 | Invalid password | | `AUTH00006` | 409 | UID already exists | | `AUTH00007` | 503 | Authentication service unavailable | | `AUTH00008` | 400 | One or more specified roles are invalid or not applicable for credentials creation. | | `AUTH00009` | 409 | API name is already in use for this merchant | | `AUTH00010` | 403 | Cannot rotate secret for credentials you do not own | | `AUTH00011` | 403 | Only API users can rotate their secret | | `AUTH00012` | 404 | API user not found in authentication system | | `AUTH00013` | 500 | Failed to generate password reset link | | `AUTH00014` | 500 | Failed to generate email verification link | | `AUTH00015` | 400 | CLI auth callback is required | | `AUTH00016` | 400 | CLI auth callback must be a valid URL | | `AUTH00017` | 400 | CLI auth callback must use http protocol | | `AUTH00018` | 400 | CLI auth callback must target 127.0.0.1 | | `AUTH00019` | 400 | CLI auth callback must include a valid port | | `AUTH00020` | 400 | CLI auth state is required | | `AUTH00021` | 500 | CLI auth frontend URL is not configured | | `AUTH00022` | 400 | Target user is not an API user | | `AUTH00023` | 403 | Target API user does not belong to the organization | | `AUTH00024` | 404 | Organization is not initialized for passkeys | | `AUTH00025` | 400 | Invalid delegated MFA stamped request | | `AUTH00026` | 500 | Failed to register delegated MFA credential with the signing provider | | `AUTH00027` | 404 | API user MFA credential not found | | `AUTH00028` | 400 | Invalid delegated MFA public key format. Send a PEM-encoded P-256 PUBLIC KEY. | | `AUTH00029` | 400 | Invalid delegated API key signature format. Send X-Stamp-Api-Key with a base64url JSON payload containing a compressed or uncompressed P-256 publicKey, SIGNATURE_SCHEME_TK_API_P256, and a hex signature. | | `AUTH00030` | 400 | API user already has an active delegated MFA credential. Revoke the existing key before enrolling a new one. | | `AUTH00031` | 400 | Invalid delegated MFA revocation request | | `AUTH00032` | 500 | Failed to revoke delegated MFA credential with the signing provider | | `AUTH00033` | 400 | Invalid wallet signing access approval request | ### BAA β€” Bank accounts | Code | Status | Message | | --- | ---: | --- | | `BAA00001` | 404 | Account not found | | `BAA00002` | 404 | Destination account not found for the merchant with the provided ID. | | `BAA00003` | 404 | Origin account not found for the merchant with the provided ID. | | `BAA00004` | 400 | The destination account cannot be a virtual account. | | `BAA00005` | 400 | The destination account must belong to the contact specified in the order. | | `BAA00006` | 400 | The destination account must be a virtual account for orders of type PAYIN. | | `BAA00007` | 400 | The destination account must be a virtual account for orders of type PAYMENT_LINK. | | `BAA00008` | 400 | The destination account currency does not match the order destination currency. | | `BAA00009` | 404 | Merchant virtual origin account with the selected currency not found. | | `BAA00010` | 404 | Origin account ID not found for payout. | | `BAA00011` | 404 | Missing ledger debit transaction for credit-back | | `BAA00012` | 409 | Account already exists | | `BAA00013` | 400 | One and only one of contactId, or merchantId must be provided. | | `BAA00014` | 409 | Insufficient balance for payout | | `BAA00015` | 400 | Invalid CLABE format for Mexico. Must be 18 numeric digits. | | `BAA00016` | 400 | Invalid CLABE check digit. | | `BAA00017` | 400 | Invalid Argentina account format. Must be 22-digit CBU/CVU or alias (6-20 characters). | | `BAA00018` | 400 | Invalid CBU/CVU check digits. | | `BAA00019` | 404 | Bank not found in country bank list. | | `BAA00020` | 400 | Account type not supported by bank. | | `BAA00021` | 400 | Account number length invalid for bank and account type. | | `BAA00022` | 400 | Invalid CCI format for Peru. Must be 20 numeric digits. | | `BAA00023` | 400 | Bank entity is required for Chile accounts. | | `BAA00024` | 400 | Bank entity is required for Colombia accounts. | | `BAA00025` | 400 | Account type is required for Colombia accounts. | | `BAA00026` | 400 | Account type is required for Peru accounts. | | `BAA00027` | 400 | Routing number is required for US accounts. | | `BAA00028` | 400 | Unable to determine payout type from bank account kind. | | `BAA00029` | 400 | PAYOUT destination account cannot be a crypto wallet. Use ONRAMP order type for fiat-to-crypto conversions. | | `BAA00030` | 400 | Country symbol is required for fiat bank accounts | | `BAA00031` | 400 | Currency symbol is required | | `BAA00032` | 400 | Invalid country symbol. Must be one of the supported countries | | `BAA00033` | 404 | Country not found in database | | `BAA00034` | 400 | Invalid currency symbol | | `BAA00035` | 400 | Account number is required for fiat bank accounts | | `BAA00036` | 400 | For merchant self-order payouts, the destination account must belong to the merchant. | | `BAA00037` | 400 | entity (bank id) is required for Hong Kong accounts. | | `BAA00038` | 400 | Bank swift code is required for Hong Kong accounts. | | `BAA00039` | 400 | Invalid bank account type provided. Valid options: SAVINGS, CHECKING, VIRTUAL | | `BAA00040` | 400 | Bank entity is required for bank accounts. | | `BAA00041` | 400 | Embedded-wallet-only destination account must be a crypto wallet. | ### BI β€” Bank income (inbound deposits) | Code | Status | Message | | --- | ---: | --- | | `BI00001` | 400 | Missing externalSystemNotifierId | | `BI00002` | 500 | Failed to create embedded wallet onramp order | | `BI00003` | 500 | Failed to persist embedded wallet onramp assignment | | `BI00004` | 500 | Failed to process bank income | | `BI00005` | 500 | Failed to register bank income ledger credit | | `BI00006` | 500 | Failed to persist bank income ledger assignment | | `BI00007` | 500 | Bank income is stuck with a legacy processing marker | ### BLC β€” Blockchain | Code | Status | Message | | --- | ---: | --- | | `BLC00001` | 500 | Blockchain RPC URL is not configured | | `BLC00002` | 500 | Unsupported blockchain network | | `BLC00003` | 500 | Failed to prepare blockchain transaction payload | | `BLC00004` | 500 | Failed to submit blockchain transaction | | `BLC00005` | 500 | Blockchain transaction request is missing from order metadata | | `BLC00006` | 500 | Blockchain transaction status is not available yet | | `BLC00007` | 500 | Blockchain payout transaction not found for order | | `BLC00008` | 500 | Ledger debit transaction not found for blockchain payout order | | `BLC00009` | 500 | Ledger debit transaction is not completed for blockchain payout order | | `BLC00010` | 500 | Blockchain transaction confirmation timed out | | `BLC00011` | 500 | Blockchain transaction reverted on-chain | | `BLC00012` | 500 | Blockchain send transaction status id is missing from order metadata | | `BLC00013` | 500 | Blockchain signer configuration is incomplete for blockchain monitoring | | `BLC00014` | 500 | Ledger reversal reference is missing for embedded wallet transaction rollback | ### BT β€” Balance transfer | Code | Status | Message | | --- | ---: | --- | | `BT00001` | 400 | Currency origin cannot be the same as currency destination | | `BT00002` | 400 | Insufficient balance in origin account | | `BT00003` | 503 | An Error occurred while transforming the balance | | `BT00004` | 400 | Currency combination not allowed | ### CACC β€” Crypto accounts | Code | Status | Message | | --- | ---: | --- | | `CACC00001` | 400 | Invalid crypto address format | | `CACC00002` | 400 | Unsupported network | | `CACC00003` | 409 | Crypto account already exists | | `CACC00004` | 404 | Crypto account not found | | `CACC00005` | 400 | Address validation failed | | `CACC00006` | 400 | Currency not supported on network | ### CAPI β€” Crypto API | Code | Status | Message | | --- | ---: | --- | | `CAPI00001` | 401 | Crypto API authentication failed | | `CAPI00002` | 500 | Crypto API quote creation failed | | `CAPI00003` | 404 | Crypto API quote not found | | `CAPI00004` | 400 | Crypto API quote expired | | `CAPI00005` | 500 | Crypto API deal creation failed | | `CAPI00006` | 404 | Crypto API deal not found | | `CAPI00007` | 500 | Crypto API order creation failed | | `CAPI00008` | 500 | Crypto API request failed | | `CAPI00009` | 500 | Crypto API invalid response | ### CNT β€” Countries | Code | Status | Message | | --- | ---: | --- | | `CNT00001` | 404 | Country not found | | `CNT00002` | 404 | Country three-letter ISO code not found | | `CNT00003` | 404 | County not found | | `CNT00004` | 422 | County does not belong to the provided state | | `CNT00005` | 422 | County does not belong to the provided country | ### CO β€” Compliance | Code | Status | Message | | --- | ---: | --- | | `CO00001` | 404 | Order not found for compliance registration | | `CO00002` | 404 | Order not found for compliance event | | `CO00003` | 404 | Order not found for compliance sync | | `CO00004` | 500 | Retry compliance registration for order | | `CO00005` | 500 | Retry compliance event for order | | `CO00006` | 500 | Retry compliance sync for order | | `CO00007` | 400 | Unknown compliance job name | ### COM β€” Common / internal | Code | Status | Message | | --- | ---: | --- | | `COM00001` | 500 | Internal server error | | `COM00002` | 500 | Unexpected error | ### CRE β€” Crypto credentials | Code | Status | Message | | --- | ---: | --- | | `CRE00001` | 404 | Credentials not found | | `CRE00002` | 401 | Invalid Signature | | `CRE00003` | 500 | unknown error | | `CRE00004` | 401 | Failed to authenticate with Crypto API | | `CRE00005` | 500 | Crypto API credentials not configured | | `CRE00006` | 400 | Partner organization does not have crypto credentials configured. Please contact support to setup partner credentials. | | `CRE00007` | 503 | Failed to retrieve partner credentials. Please try again later. | ### CSIG β€” Crypto signing | Code | Status | Message | | --- | ---: | --- | | `CSIG00001` | 500 | Private key not configured for signing requests | | `CSIG00002` | 500 | Request signing failed | | `CSIG00003` | 500 | KOYWE_CRYPTO_API_URL not configured for domestic requests | ### CT β€” Contacts | Code | Status | Message | | --- | ---: | --- | | `CT00001` | 400 | Invalid preferredCurrency: currency not available or wrong ID | | `CT00002` | 404 | Contact not found | | `CT00003` | 409 | Contact operation blocked in the current contact lifecycle state. | | `CT00004` | 409 | Contact documents cannot be modified in the current contact lifecycle state. | | `CT00005` | 409 | Contact with same tax info already exists | | `CT00006` | 409 | Contact with same document info already exists | | `CT00007` | 409 | Contact with same email or phone already exists | | `CT00008` | 400 | Contact email or phone are required | | `CT00009` | 400 | Contact taxIdNumber is required when taxIdType is informed | | `CT00010` | 400 | Contact documentNumber is required when documentType is informed | | `CT00011` | 400 | Invalid contact information. Please reach out to support. | | `CT00012` | 400 | Invalid phone number | | `CT00013` | 400 | The client attempting to use the service has a different email than the one entered. | | `CT00014` | 400 | The phone number entered does not belong to the contact. | | `CT00015` | 400 | There was an issue with the current contact. Please reach out to support. | | `CT00020` | 400 | Duplicate document found in documents array | | `CT00021` | 400 | Only one document can be marked as default | | `CT00022` | 400 | One document must be marked as default when multiple documents are provided | | `CT00023` | 409 | Document already exists for another contact in this merchant | | `CT00024` | 400 | Cannot delete all documents, contact must have at least one document | | `CT00025` | 404 | One or more documents not found for this contact | | `CT00026` | 400 | Document does not belong to the specified contact | | `CT00027` | 400 | Contact information is incomplete for payment link email | | `CT00028` | 409 | Contact with same external ID already exists for this merchant | | `CT00029` | 400 | firstName (BENEFICIARY_NAME) is required for Hong Kong contacts | | `CT00030` | 400 | addressLine1 (BENEFICIARY_ADDRESS) is required for Hong Kong contacts | | `CT00031` | 400 | email (BENEFICIARY_EMAIL) is required for Hong Kong contacts | | `CT00032` | 400 | phone (BENEFICIARY_PHONE_NUMBER) is required for Hong Kong contacts | | `CT00033` | 400 | documentType and documentNumber (BENEFICIARY_NATIONAL_IDENTIFICATION_NUMBER) are required for Hong Kong contacts | | `CT00034` | 400 | dateOfBirth (BENEFICIARY_DATE_OF_BIRTH) is required for Hong Kong contacts | | `CT00035` | 400 | documentExpirationDate (BENEFICIARY_NATIONAL_IDENTIFICATION_EXPIRATION_DATE) is required for Hong Kong contacts | | `CT00036` | 400 | Contact is required for PAYIN orders | | `CT00037` | 400 | Contact information is incomplete. Missing required field: firstName | | `CT00038` | 400 | Contact information is incomplete. Missing required field: lastName | | `CT00039` | 400 | Contact information is incomplete. Missing required field: email | | `CT00040` | 400 | Contact information is incomplete. Missing required field: addressLine1 | | `CT00041` | 400 | Contact information is incomplete. Missing required field: addressCity | | `CT00042` | 400 | Contact information is incomplete. Missing required field: addressState | | `CT00043` | 400 | Contact information is incomplete. Missing required field: addressPostalCode | | `CT00044` | 400 | Contact information is incomplete. Missing required field: country | | `CT00045` | 400 | Invalid county: county not found. | ### CU β€” Currencies | Code | Status | Message | | --- | ---: | --- | | `CU00001` | 404 | Currency not found | | `CU00002` | 404 | Origin currency symbol not found or invalid. | | `CU00003` | 404 | Destination currency symbol not found or invalid. | | `CU00004` | 400 | Network is required for crypto currency | | `CU00005` | 400 | Unsupported cryptocurrency | | `CU00006` | 400 | Invalid network for crypto operations | | `CU00007` | 400 | Currency is not supported on the specified network | | `CU00008` | 400 | No country code mapping found for currency | ### CW β€” Crypto whitelist | Code | Status | Message | | --- | ---: | --- | | `CW00002` | 500 | Unknown crypto whitelist job name | | `CW00003` | 500 | All whitelist attempts failed | | `CW00004` | 500 | Failed to enqueue crypto address whitelist job | ### CWHOOK β€” Crypto webhooks | Code | Status | Message | | --- | ---: | --- | | `CWHOOK00001` | 401 | Invalid webhook signature | | `CWHOOK00002` | 404 | Order not found for crypto order ID | | `CWHOOK00003` | 400 | Invalid event type | ### DC β€” Documents (validation) | Code | Status | Message | | --- | ---: | --- | | `DC00001` | 500 | Could not create document, please contact support | | `DC00002` | 404 | Document not found | | `DC00003` | 500 | Could not retrieve the requested document | | `DC00004` | 500 | Could not retrieve the requested documents | | `DC00005` | 500 | Could not retrieve the requested received documents | | `DC00006` | 500 | Could not retrieve the requested received document | | `DC00007` | 500 | Could not update the requested received document | | `DC00008` | 400 | Document type is required when a document number is provided | | `DC00009` | 400 | Country is required when a document number is provided | | `DC00010` | 400 | Invalid document number for the specified document type and country | | `DC00011` | 400 | Document type not supported for the specified country | ### DL β€” Deals | Code | Status | Message | | --- | ---: | --- | | `DL00001` | 500 | Failed to retrieve scheduled deals from external service | | `DL00002` | 400 | Failed to stop scheduled deal | | `DL00003` | 400 | Deal has no pending amount available for transfer | | `DL00004` | 400 | Deal has expired and cannot be transferred | | `DL00005` | 400 | Failed to create quote for transfer | | `DL00006` | 500 | Failed to create new deal for transferDeal | | `DL00007` | 400 | Failed creating a new deal | | `DL00008` | 400 | Invalid destination address | | `DL00009` | 409 | Insufficient Balance to create a deal | | `DL00010` | 404 | Scheduled deal not found | | `DL00011` | 404 | Quote not found or expired | | `DL00012` | 409 | Quote already used | | `DL00013` | 403 | Merchant crypto feature not enabled | | `DL00014` | 409 | USD credit limit exceeded | | `DL00015` | 403 | Destination account does not belong to merchant | | `DL00016` | 500 | Crypto API deal creation failed | | `DL00017` | 400 | Merchant crypto account credentials not configured | | `DL00020` | 409 | Credit limit exceeded. Active deals plus new deal exceeds maximum allowed credit | | `DL00021` | 400 | Credit limit not configured for merchant with credit line enabled | | `DL00022` | 404 | Merchant crypto account not found | | `DL00023` | 500 | Failed to create merchant crypto account | | `DL00024` | 400 | Merchant information incomplete for crypto account creation | | `DL00025` | 400 | Destination account is not a bank | | `DL00026` | 400 | Destination account is not a crypto | | `DL00027` | 400 | Destination account currency does not match deal destination currency | | `DL00028` | 400 | For OFFRAMP deals, destination account must be a virtual bank account | | `DL00029` | 500 | Ledger credit response is missing the transaction id | | `DL00030` | 404 | Crypto provider order not found | | `DL00031` | 500 | USD payment method is not configured for executable onramp quotes | | `DL00032` | 500 | Invalid deal amounts after suite sync | ### EM β€” Email delivery | Code | Status | Message | | --- | ---: | --- | | `EM00001` | 500 | Failed to send email via external service | | `EM00002` | 503 | Email service is temporarily unavailable | | `EM00003` | 401 | Email service authentication failed | | `EM00004` | 400 | Invalid email payload format | | `EM00005` | 429 | Email service rate limit exceeded | ### EWW β€” Embedded wallet | Code | Status | Message | | --- | ---: | --- | | `EWW00001` | 500 | Failed to create embedded wallet with the signing provider | | `EWW00002` | 500 | Failed to create embedded wallet account with the signing provider | | `EWW00003` | 500 | Unable to resolve embedded wallet after creation | | `EWW00004` | 500 | Unable to resolve embedded wallet account after creation | | `EWW00005` | 400 | Invalid stamped embedded wallet request | | `EWW00006` | 404 | Merchant does not belong to the organization | | `EWW00007` | 404 | USD virtual bank account not found for merchant | | `EWW00008` | 404 | Organization has no embedded wallet signing organization yet | | `EWW00009` | 400 | Unsupported embedded wallet stablecoin asset | | `EWW00010` | 403 | Wallet signing access is pending root approval | | `EWW00011` | 409 | Cannot provision a wallet while the merchant has active USD orders | | `EWW00012` | 401 | x-webhook-signature header is missing | | `EWW00013` | 403 | Webhook secret is not configured | | `EWW00014` | 401 | Invalid webhook signature | | `EWW00015` | 500 | Embedded wallet settlement is missing required ledger credit transaction | | `EWW00016` | 500 | Native payout settlement is missing required fiat funding transactions | | `EWW00017` | 500 | Native payout settlement is missing FIAT_OUT transaction for durable enqueue | | `EWW00018` | 500 | Embedded wallet settlement requires manual reconciliation before retrying external side effects | | `EWW00019` | 409 | Embedded wallet broadcast attempt transition rejected | | `EWW00020` | 503 | Payout enqueue probe failed transiently; retry required | ### FC β€” Feature configuration | Code | Status | Message | | --- | ---: | --- | | `FC00001` | 404 | Feature definition not found | | `FC00002` | 409 | Feature configuration already exists | | `FC00004` | 400 | Invalid feature code | ### FE β€” Fees & commissions | Code | Status | Message | | --- | ---: | --- | | `FE00001` | 400 | Failed while calculating cashout fee | | `FE00002` | 404 | Fee definition not found | | `FE00003` | 409 | Fee configurations already exist | | `FE00004` | 404 | Fee configuration not found | | `FE00005` | 409 | Fee configuration already exists with the same merchant, fee definition, currency, order type and apply order | | `FE00006` | 400 | Invalid foreign key reference (merchant, fee definition, or currency not found) | | `FE00007` | 400 | Database error while creating fee configuration | | `FE00008` | 400 | calculationType PERCENTAGE requires basePercentageRate > 0 and no baseFixedAmount | | `FE00009` | 400 | calculationType FIXED_AMOUNT requires baseFixedAmount > 0 and no basePercentageRate | | `FE00010` | 400 | minChargeAmount must be less than maxChargeAmount | | `FE00011` | 400 | Fee values must be non-negative | | `FE00012` | 400 | basePercentageRate must be between 0 and 1 | | `FE00013` | 400 | orderType ONRAMP/PAYIN/PAYOUT/PAYMENT_LINK requires fiat currency | | `FE00014` | 400 | orderType OFFRAMP requires crypto currency | | `FE00015` | 400 | Duplicate entry in payload (same currencyId, orderType, applyOrder) | | `FE00016` | 400 | Only BASE_FEE is allowed for new fee configurations | | `FE00017` | 400 | applyOrder must be 0 for BASE_FEE configurations | | `FE00018` | 400 | Merchant does not have COMMISSIONS_V2 feature flag enabled | ### FF β€” Merchant feature flags | Code | Status | Message | | --- | ---: | --- | | `FF00001` | 404 | Feature flag not found | | `FF00002` | 409 | Feature flag already exists for this merchant | | `FF00003` | 400 | Invalid feature flag code | | `FF00004` | 400 | Feature flag is already deleted | ### GFF β€” Global feature flags | Code | Status | Message | | --- | ---: | --- | | `GFF00001` | 404 | Global feature flag not found | | `GFF00002` | 409 | Global feature flag already exists | | `GFF00003` | 400 | Invalid global feature flag code | ### IMT β€” Inter-merchant transfer | Code | Status | Message | | --- | ---: | --- | | `IMT00001` | 404 | Destination merchant not found | | `IMT00002` | 403 | Destination merchant does not belong to the same organization | | `IMT00003` | 400 | Source and destination merchant cannot be the same | | `IMT00004` | 404 | Destination merchant is disabled | | `IMT00005` | 409 | Insufficient balance in source merchant account | | `IMT00006` | 503 | An error occurred while executing the transfer in the Ledger | | `IMT00007` | 400 | destinationMerchantId is required for INTER_MERCHANT_TRANSFER orders | | `IMT00008` | 400 | Origin and destination currency must be the same for INTER_MERCHANT_TRANSFER (MVP) | ### INV β€” Invitations | Code | Status | Message | | --- | ---: | --- | | `INV00001` | 404 | Invitation not found | | `INV00002` | 410 | Invitation has expired | | `INV00003` | 409 | Invitation has already been accepted | | `INV00004` | 400 | Invitation is not pending | | `INV00005` | 409 | A pending invitation already exists for this email address and merchant. | | `INV00006` | 400 | Invalid token format | | `INV00007` | 400 | Roles must be provided for an invitation. | | `INV00008` | 400 | One or more specified roles are invalid or not applicable for merchant invitations. | | `INV00009` | 500 | Failed to create the invitation. Please contact support. | | `INV00010` | 409 | A pending invitation already exists for this email address and organization. | | `INV00011` | 400 | Role not found | | `INV00012` | 400 | Role is a merchant role and cannot be assigned in organization invitations | | `INV00013` | 404 | User not found | | `INV00014` | 403 | This invitation was sent to a different email address. Please use the account associated with the invited email or contact the administrator to resend the invitation. | | `INV00015` | 400 | No valid roles were found in the invitation after filtering. At least one role must be assignable. | | `INV00016` | 422 | This invitation includes sensitive roles that require policy approval, but the organization has not bootstrapped passkeys or has no active policy. Please bootstrap the organization first. | | `INV00017` | 500 | Unknown invitation type in approved operation payload | ### KAC β€” Koywe account | Code | Status | Message | | --- | ---: | --- | | `KAC00001` | 404 | Account not found | | `KAC00002` | 400 | Account with email already exists | | `KAC00003` | 400 | Failed to create account | | `KAC00004` | 400 | Country or document number are not provided | ### LE β€” Ledger | Code | Status | Message | | --- | ---: | --- | | `LE00001` | 400 | Failed to check balance with Ledger | | `LE00002` | 400 | Failed to get assets with Ledger | | `LE00003` | 400 | Failed to register credit transaction with Ledger | | `LE00004` | 400 | Failed to register debit transaction with Ledger | | `LE00005` | 400 | Failed to transform transaction with Ledger | | `LE00006` | 400 | Failed to transfer transaction with Ledger | | `LE00007` | 400 | Failed to transfer with Ledger | | `LE00008` | 400 | Failed to reverse transaction with Ledger | | `LE00009` | 400 | Failed to settle accounts receivable with Ledger | | `LE00010` | 503 | Unable to retrieve account balances at this time. Please try again later. | | `LE00011` | 409 | Ledger rejected credit transaction due to duplicate externalId | | `LE00012` | 409 | Ledger rejected debit transaction due to duplicate externalId | | `LE00013` | 409 | Ledger rejected transform transaction due to duplicate externalId | | `LE00014` | 409 | Ledger rejected transfer transaction due to duplicate externalId | | `LE00015` | 409 | Ledger rejected reverse transaction due to duplicate externalId | | `LE00016` | 409 | Ledger rejected accounts receivable issue due to duplicate externalId | | `LE00017` | 409 | Ledger rejected accounts receivable settlement due to duplicate externalId | ### MC β€” Merchants | Code | Status | Message | | --- | ---: | --- | | `MC00001` | 404 | Merchant not found | | `MC00002` | 400 | Merchant has orders and cannot be deleted | | `MC00003` | 400 | The document type and number are already in use. | | `MC00004` | 400 | The slug is already in use. | | `MC00005` | 400 | The merchant pricing fee needs to be greater than 0 when selectedModel is FIXED_FEE. | | `MC00006` | 400 | The merchant pricing percentage needs to be greater than 0 when selectedModel is FIXED_PERCENTAGE. | | `MC00007` | 400 | The merchant pricing flatFeePeriod needs to be valid value | | `MC00008` | 400 | The merchant pricing minFee needs to be greater than 0 when selectedModel is VOLUME_FEE. | | `MC00009` | 400 | The merchant pricing maxFee needs to be greater than 0 when selectedModel is VOLUME_FEE. | | `MC00010` | 400 | The merchant pricing minFee needs to be less than maxFee when selectedModel is VOLUME_FEE. | | `MC00011` | 400 | The merchant pricing percentageMin needs to be greater than 0 when selectedModel is VOLUME_PERCENTAGE. | | `MC00012` | 400 | The merchant pricing percentageMax needs to be greater than 0 when selectedModel is VOLUME_PERCENTAGE. | | `MC00013` | 400 | The merchant pricing percentageMin needs to be less than percentageMax when selectedModel is VOLUME_PERCENTAGE. | | `MC00014` | 400 | Invalid phone number | | `MC00015` | 403 | Merchant does not belong to the organization | | `MC00016` | 500 | KYB submission failed to external service | | `MC00017` | 404 | Merchant not found in PSP | | `MC00018` | 409 | KYB already submitted for merchant | | `MC00019` | 422 | Invalid KYB data format | | `MC00020` | 500 | PSP connection error | | `MC00021` | 400 | Merchant city not found | | `MC00022` | 409 | Merchant already has accepted terms of service | | `MC00023` | 400 | Failed to complete KYB operation | | `MC00024` | 400 | Duplicate email | | `MC00025` | 403 | KYB not approved for payout operations. Please complete the KYB process. | | `MC00026` | 403 | KYB terms of service not accepted. Please accept the terms to enable payout operations. | | `MC00027` | 403 | KYB information not submitted. Please submit KYB information to enable payout operations. | | `MC00028` | 403 | KYB verification in progress. Payout operations will be enabled once verification is complete. | | `MC00029` | 403 | KYB information rejected. Please resubmit corrected KYB information to enable payout operations. | | `MC00030` | 403 | KYB process not initialized. Please start the KYB process to enable payout operations. | | `MC00031` | 409 | Crypto account setup conflict: unique constraint triggered but record not found. | | `MC00032` | 403 | Pre-KYB transaction limit exceeded. Complete KYB verification to remove this restriction. | | `MC00033` | 503 | Unable to validate pre-KYB transaction limit. Exchange rate unavailable. | | `MC00034` | 500 | Unable to validate pre-KYB transaction limit. Order amount is missing. | | `MC00035` | 409 | KYB status is already set to the requested value. No changes applied. | | `MC00036` | 400 | Invalid onboardingCompletedAt date format. | | `MC00037` | 403 | Lifecycle override is not allowed in production | | `MC00038` | 422 | Invalid lifecycle state transition | | `MC00042` | 422 | Merchant cannot be reset from its current state | | `MC00043` | 403 | Merchant cannot process transactions in current lifecycle state. KYB verification is required. | | `MC00044` | 422 | Merchant must be in DRAFT lifecycle state for complete-setup | | `MC00045` | 409 | Merchant already has a setup register β€” complete-setup has already been run | ### NE β€” Notifications | Code | Status | Message | | --- | ---: | --- | | `NE00001` | 404 | Notification not found | | `NE00002` | 403 | Cannot access this notification | | `NE00003` | 400 | Invalid pagination parameters | | `NE00004` | 400 | At least one recipient required when email notifications are enabled | | `NE00005` | 401 | SSE ticket missing or invalid | | `NE00006` | 401 | SSE ticket expired or already used | | `NE00007` | 403 | SSE ticket does not match this merchant | | `NE00010` | 503 | Notification service temporarily unavailable | ### OB β€” Organization balance | Code | Status | Message | | --- | ---: | --- | | `OB00001` | 404 | Organization not found | | `OB00002` | 403 | User has no access to any merchants with virtual account view permissions in this organization | | `OB00003` | 400 | Invalid or unsupported base currency. Currently only USD is supported | | `OB00004` | 400 | Cannot provide both date and period parameters | | `OB00005` | 400 | Invalid period value. Must be one of: 3d, 7d, 14d, 30d, 90d, 180d, 1y, all | | `OB00006` | 400 | Invalid interval value. Must be one of: daily, weekly, monthly | | `OB00007` | 400 | Invalid merchantIds format. Must be a comma-separated list of merchant IDs | | `OB00008` | 400 | Date parameter must be a valid ISO 8601 date string | | `OB00009` | 404 | No virtual accounts found for the specified merchants | | `OB00010` | 500 | Unable to retrieve balance data at this time | | `OB00011` | 500 | Unable to fetch currency conversion rates | | `OB00012` | 400 | Historical data not available for the requested period | | `OB00013` | 400 | Invalid period and interval combination. Daily interval only allowed for periods up to 14 days. Weekly interval only allowed for periods up to 180 days. Monthly interval required for periods longer than 180 days or for the `all` period. | | `OB00014` | 400 | Too many accessible merchants for balance query. Please contact support to increase the limit | ### OK β€” Onboarding KYB | Code | Status | Message | | --- | ---: | --- | | `OK00001` | 404 | Merchant not found | | `OK00002` | 404 | KYB process not found for merchant | | `OK00003` | 409 | KYB process already exists and is in a non-terminal state | | `OK00004` | 400 | Missing required merchant data to start KYB process | | `OK00005` | 502 | Failed to create KYB process with provider | | `OK00006` | 400 | Invalid webhook payload | | `OK00007` | 404 | KYB process not found for external ID | | `OK00008` | 500 | Failed to update KYB process status | | `OK00009` | 500 | Failed to activate merchant features after KYB approval | | `OK00010` | 400 | Merchant does not belong to an organization | | `OK00011` | 400 | Could not resolve focal point contact β€” ensure pre-onboarding data exists for this merchant, or pass contactOverride.email in the request body | | `OK00014` | 404 | No active KYB process found for merchant | | `OK00015` | 400 | KYB process is not in MANUAL_REVIEW status | | `OK00016` | 401 | Webhook signature headers are missing (X-Signature-256, X-Timestamp) | | `OK00017` | 503 | Webhook secret is not configured | | `OK00018` | 401 | Invalid webhook signature | | `OK00019` | 503 | KYB generation service is temporarily unavailable. Please try again later. | | `OK00020` | 500 | Merchant country data could not be resolved. Ensure the merchant has a valid countryId before triggering KYB. | | `OK00021` | 403 | KYB can only be triggered for merchants in ENABLED_LIMITED, KYB_REQUIRED, or KYB_RENEWAL_REQUIRED status | | `OK00022` | 401 | Webhook timestamp is expired or invalid | ### OR β€” Orders | Code | Status | Message | | --- | ---: | --- | | `OR00001` | 400 | The expiration date provided must be a future date | | `OR00002` | 409 | Order with externalId already exists. | | `OR00003` | 400 | Origin and destination currency MUST be the same for this transaction generation logic to apply. | | `OR00004` | 404 | Order not found | | `OR00005` | 400 | Unsupported order type | | `OR00006` | 400 | Error during PAYOUT execution | | `OR00007` | 400 | Error during PAYIN execution | | `OR00008` | 400 | Payouts are not implemented yet | | `OR00009` | 400 | Invalid due date format | | `OR00010` | 400 | Payment links cannot be generated for cash payments (payment condition: 0) | | `OR00011` | 404 | Order does not belong to merchant | | `OR00012` | 404 | Order does not belong to the specified organization | | `OR00013` | 400 | Order must be of type PAYMENT_LINK to send payment link email | | `OR00014` | 400 | Order must be in PENDING status to send payment link email | | `OR00015` | 400 | Order has expired and cannot be processed | | `OR00016` | 400 | Order must be of type EMAIL to send email | | `OR00017` | 400 | The expiration date exceeds the maximum allowed lifetime for orders | | `OR00018` | 404 | Merchant not found | | `OR00019` | 404 | Destination bank account not found | | `OR00020` | 400 | Payment rejected by provider: the amount may be invalid or exceed the limit for this payment method | | `OR00021` | 400 | Payment rejected by provider: daily transaction limit reached for this payment method | | `OR00022` | 400 | Order does not have an associated crypto order | | `OR00023` | 400 | Failed to set transaction hash on order | | `OR00024` | 400 | Order is not ready for transaction signing | | `OR00025` | 400 | Order is already signed or being processed | | `OR00026` | 400 | Invalid signing request for this order | | `OR00027` | 404 | Embedded wallet account not found for this order | | `OR00028` | 400 | Cannot reprocess a terminal bank income onramp order | | `OR00029` | 409 | Order in unexpected state for policy resume | | `OR00030` | 500 | Persisted order id is required before enqueuing ledger jobs | ### ORG β€” Organizations | Code | Status | Message | | --- | ---: | --- | | `ORG00001` | 404 | Organization not found | | `ORG00002` | 400 | Email is required | | `ORG00003` | 400 | Target user ID is required | | `ORG00004` | 400 | Role ID is required | | `ORG00005` | 400 | Invalid role ID | ### PER β€” Permissions | Code | Status | Message | | --- | ---: | --- | | `PER00001` | 403 | User does not have permission to perform this action | ### PIVBA β€” Pay-in virtual bank accounts | Code | Status | Message | | --- | ---: | --- | | `PIVBA00001` | 404 | Virtual bank account not found | | `PIVBA00002` | 409 | Virtual bank account already exists | | `PIVBA00003` | 400 | Invalid country-currency combination | | `PIVBA00004` | 404 | Merchant not found | | `PIVBA00005` | 400 | Virtual bank account is inactive | | `PIVBA00006` | 500 | PSP communication error | | `PIVBA00007` | 400 | Country not supported | | `PIVBA00008` | 429 | Too many requests to PSP | | `PIVBA00009` | 500 | PSP internal server error | | `PIVBA00010` | 503 | PSP service unavailable | | `PIVBA00011` | 500 | PSP unexpected error | | `PIVBA00012` | 400 | Currency not supported for country | | `PIVBA00013` | 400 | Alias not supported for this country | | `PIVBA00014` | 409 | Alias modification limit exceeded. You must wait 24 hours before modifying it again | | `PIVBA00015` | 400 | Invalid alias format | | `PIVBA00016` | 422 | Recipient KYC not approved for virtual account creation | ### PL β€” Payment links | Code | Status | Message | | --- | ---: | --- | | `PL00001` | 404 | Payment Link not found | | `PL00002` | 409 | The status change cannot be performed because the entity is already in the same status. | | `PL00003` | 400 | It is not possible to modify the status because it is in a final state | | `PL00004` | 400 | It is not possible to modify the status | | `PL00005` | 400 | The payment link need to be in PENDING status to be able to be paid | | `PL00006` | 409 | The payment link is is processing | | `PL00007` | 400 | It is not possible to modify the status | | `PL00008` | 409 | Only one paymentLink can be created for an order in pending status. | | `PL00009` | 409 | The order already has a pending paymentLink. | | `PL00010` | 400 | The contact does not have a bank account | | `PL00011` | 410 | The payment link has expired | | `PL00012` | 400 | The payment link method is not PSE | | `PL00013` | 400 | The bank is not valid to pay with PSE | | `PL00014` | 404 | The payment link method does not exist | | `PL00015` | 410 | The payment link method has expired | | `PL00016` | 400 | The successUrl is not a valid URL or is not https in production | | `PL00017` | 400 | PayerInfo is required for PAYMENT_LINK orders | | `PL00018` | 400 | Order missing origin currency information | | `PL00019` | 400 | Order missing destination currency information | | `PL00020` | 400 | Payment method is not available for the selected currency | | `PL00021` | 500 | FIAT_IN transaction not found for payment initiation | | `PL00022` | 409 | Payment for this order has already been initiated or processed | | `PL00023` | 400 | Contact is required for payment processing | | `PL00024` | 400 | Contact is missing required information (name or email) | | `PL00025` | 400 | Bank account name is required for payment processing | | `PL00026` | 400 | Phone is required for payment processing | | `PL00027` | 400 | Phone country code is required for payment processing | | `PL00028` | 400 | Phone is required for payment processing | | `PL00029` | 400 | Email is required for payment processing | | `PL00030` | 400 | First name is required for payment processing | | `PL00031` | 400 | Last name is required for payment processing | | `PL00032` | 400 | Document number is required for payment processing | | `PL00033` | 400 | Document type is required for payment processing | ### PM β€” Payment methods | Code | Status | Message | | --- | ---: | --- | | `PM00001` | 404 | Payment method not found | | `PM00002` | 409 | Payment method already deleted | ### PMP β€” Payment-method providers | Code | Status | Message | | --- | ---: | --- | | `PMP00001` | 404 | Payment method not found in transaction | | `PMP00002` | 404 | Confirmed payment method not found in transaction | | `PMP00003` | 503 | Service temporarily unavailable | | `PMP00004` | 404 | Payment method not found | | `PMP00005` | 400 | Payment method requires a dueDate | | `PMP00006` | 400 | Payment method amount is less than the minimum amount | | `PMP00007` | 400 | Payment method amount is greater than the maximum amount | | `PMP00008` | 503 | Service temporarily unavailable | | `PMP00009` | 408 | Request timeout | | `PMP00010` | 400 | Invalid payout request | | `PMP00011` | 401 | Authentication failed | | `PMP00012` | 404 | Payment option not available for country/currency combination | | `PMP00013` | 409 | Duplicate transaction | | `PMP00014` | 500 | Unexpected error occurred | | `PMP00015` | 400 | Invalid payment request | | `PMP00016` | 400 | Invalid amount range | | `PMP00017` | 400 | Payment provider not available for country/currency combination | | `PMP00018` | 409 | already processed KYB submission. Please check your KYB status before retrying. | | `PMP00019` | 400 | Invalid business industry | | `PMP00020` | 400 | Card payment method only supports CLP to CLP transactions | | `PMP00021` | 404 | Provider credentials not configured for this merchant | | `PMP00022` | 409 | Provider credentials already exist for this merchant and provider | | `PMP00023` | 404 | Provider credentials not found for this merchant and provider | ### PO β€” Pre-onboarding | Code | Status | Message | | --- | ---: | --- | | `PO00001` | 404 | Registration form not found | | `PO00002` | 400 | Invalid registration form ID format | | `PO00003` | 404 | Draft not found | | `PO00004` | 409 | Draft already exists for user | | `PO00005` | 422 | Draft already submitted | | `PO00006` | 400 | Draft validation error | | `PO00007` | 404 | Registration not found | | `PO00008` | 409 | Merchant already exists with the same tax identification | | `PO00009` | 404 | User not found | | `PO00010` | 400 | Missing required parameter: userId | | `PO00011` | 500 | Compliance check could not be enqueued | | `PO00012` | 400 | Country code not found in Compliance Tracker catalog | | `PO00013` | 500 | Unknown job name in queue consumer | | `PO00014` | 500 | Unexpected Compliance Tracker resultado | | `PO00015` | 404 | Onboarding register not found | | `PO00016` | 500 | Payload not found for onboarding register | | `PO00017` | 404 | Country not found for onboarding register | | `PO00018` | 404 | Currency not found for country | | `PO00019` | 500 | Fee definitions not found in the system | | `PO00020` | 500 | Setup register conflict but record not found | | `PO00021` | 400 | company.economicActivity must be a valid activity code (exact match). Use GET /economic-activities for valid codes (e.g. COMMERCE_RETAIL, OTHER). | | `PO00022` | 400 | When economic activity is `OTHER`, the `company.economicActivityDescription` field is required. | | `PO00023` | 400 | Economic activity description only allowed when activity is (OTHER) | | `PO00024` | 403 | Access denied: draft belongs to another user | | `PO00025` | 403 | Access denied: registration belongs to another user | | `PO00029` | 400 | Registration is not in COMPLIANCE_IN_REVIEW status | ### POL β€” Policy (MFA & approvals) | Code | Status | Message | | --- | ---: | --- | | `POL00001` | 404 | Policy not found | | `POL00002` | 403 | No policy defined for organization - zero privilege enforced | | `POL00003` | 403 | Operation denied by policy rule | | `POL00004` | 403 | Per-transaction amount limit exceeded | | `POL00005` | 403 | Daily cumulative limit exceeded | | `POL00006` | 428 | MFA verification required | | `POL00007` | 428 | Approval required for this operation | | `POL00008` | 428 | MFA verification and approval required | | `POL00009` | 400 | Invalid policy configuration | | `POL00010` | 400 | Invalid rule configuration | | `POL00011` | 400 | Approval threshold exceeds approver count | | `POL00012` | 400 | Default deny rule cannot be modified | | `POL00013` | 400 | Rule order conflict | | `POL00014` | 409 | Organization already has an active policy | | `POL00015` | 404 | Pending approval not found | | `POL00016` | 403 | Not authorized to approve this operation | | `POL00017` | 410 | Approval request expired | | `POL00018` | 409 | Approval already resolved | | `POL00019` | 403 | Requester cannot approve their own operation | | `POL00020` | 403 | MFA verification failed | | `POL00021` | 403 | MFA verification expired | | `POL00022` | 404 | Policy rule not found | | `POL00023` | 404 | Operation target not found for approved operation | | `POL00024` | 409 | Operation target is not in expected ON_HOLD state | | `POL00025` | 403 | Pending approval operation type mismatch | | `POL00026` | 400 | Missing required operationData for PASSKEY_ENROLL approval (stampedRequest with url, body, and stamp) | | `POL00027` | 400 | Invalid operationType query parameter | | `POL00028` | 400 | Invalid scope query parameter | | `POL00029` | 409 | No available policy rule order slots | | `POL00030` | 500 | Requester MFA confirmation could not be persisted | ### PSPC β€” PSP conciliation | Code | Status | Message | | --- | ---: | --- | | `PSPC00001` | 400 | Transaction status must be COMPLETED | | `PSPC00002` | 404 | Order not found for transaction | | `PSPC00003` | 400 | Order status must be COMPLETED | | `PSPC00004` | 404 | AR_ISSUE transaction not found for order | | `PSPC00005` | 404 | AR_SETTLE transaction not found for order | | `PSPC00006` | 400 | AR_SETTLE transaction must be PENDING | | `PSPC00007` | 409 | Conciliation already processed for order | | `PSPC00008` | 400 | Order is missing OriginCurrency relation | | `PSPC00009` | 400 | AR_SETTLE transaction is missing originAmount | | `PSPC00010` | 400 | AR_ISSUE transaction is missing originAmount | | `PSPC00011` | 400 | Merchant amount mismatch with AR_ISSUE transaction | | `PSPC00012` | 400 | Merchant amount mismatch with AR_SETTLE transaction | ### QE β€” Quotes | Code | Status | Message | | --- | ---: | --- | | `QE00001` | 400 | Invalid quote. You need to create a new Quote | | `QE00002` | 404 | Quote not found | | `QE00003` | 410 | Quote timeOut, You need to create a new Quote | | `QE00004` | 409 | Quote already used in another order | | `QE00005` | 400 | Failed to generate quote | | `QE00006` | 400 | orderType is required for quote requests | | `QE00007` | 400 | orderType does not match currency pair. PAYIN, PAYOUT, BALANCE_TRANSFER, and PAYMENT_LINK require fiat-to-fiat currencies | | `QE00008` | 400 | orderType ONRAMP requires fiat origin and crypto destination currencies | | `QE00009` | 400 | orderType OFFRAMP requires crypto origin and fiat destination currencies | | `QE00010` | 400 | network field is required for ONRAMP and OFFRAMP quotes | | `QE00011` | 409 | Price quote unavailable. Please try again in a few moments | | `QE00012` | 400 | Quote calculation resulted in negative amount. The transaction fees exceed the transaction amount. Please increase the amount or contact support if you think this is an error. | | `QE00013` | 400 | Asian currency quotes are not available | | `QE00014` | 400 | Currency is not currently available | | `QE00015` | 400 | Merchant business type is required | | `QE00016` | 400 | Contact is required for PSP network quotes | | `QE00017` | 404 | Contact not found for this merchant | | `QE00018` | 400 | Contact country does not match the Asian currency country | | `QE00019` | 400 | Merchant has no country | | `QE00020` | 400 | Invalid exchange rate | | `QE00021` | 400 | Failed to get valid amount from crypto API | | `QE00022` | 400 | Failed to get crypto quote | | `QE00023` | 400 | Destination country is required for this currency. Provide the destinationCountry parameter. | ### RE β€” Reports | Code | Status | Message | | --- | ---: | --- | | `RE00001` | 404 | Account not found or not accessible | | `RE00002` | 400 | Invalid date range. from date must be before to date | | `RE00003` | 400 | Invalid granularity. Must be daily or monthly | | `RE00004` | 400 | Invalid format. Must be json, csv, or pdf | | `RE00005` | 400 | Date range exceeds maximum allowed (365 days) | | `RE00006` | 404 | Ledger entry not found | | `RE00007` | 403 | Account does not belong to merchant | | `RE00008` | 400 | from date is required | | `RE00009` | 400 | to date is required | | `RE00010` | 500 | Unable to retrieve ledger data | | `RE00011` | 400 | Account must be a virtual account | | `RE00012` | 400 | Invalid cursor format | ### SBX β€” Sandbox simulator | Code | Status | Message | | --- | ---: | --- | | `SBX00001` | 404 | Sandbox simulator is not available in production | | `SBX00002` | 400 | Depositor documentType and documentNumber are required for ARS and MXN simulations | | `SBX00003` | 400 | Sandbox currency not supported | | `SBX00004` | 400 | Sandbox country not supported | | `SBX00005` | 400 | Sandbox bank income amount must be greater than zero | ### SD β€” Suite Domestic (Koywe 3) | Code | Status | Message | | --- | ---: | --- | | `SD00001` | 400 | Suite Domestic bad request | | `SD00002` | 401 | Suite Domestic unauthorized - invalid api-key | | `SD00003` | 429 | Suite Domestic rate limit exceeded | | `SD00004` | 503 | Suite Domestic service temporarily unavailable | | `SD00005` | 503 | Unable to connect to Suite Domestic service | | `SD00006` | 504 | Suite Domestic request timed out | | `SD00007` | 500 | Suite Domestic unexpected error | ### TK β€” Turnkey (embedded wallet provider) | Code | Status | Message | | --- | ---: | --- | | `TK00001` | 503 | Embedded wallet provider is not initialized | | `TK00002` | 502 | Embedded wallet provider verification failed | | `TK00003` | 500 | Embedded wallet provider did not return an OTP identifier | | `TK00004` | 500 | Embedded wallet provider returned an incomplete OTP authentication response | | `TK00005` | 502 | Embedded wallet provider request failed | | `TK00006` | 409 | User email must be unique in the embedded wallet provider | ### TX β€” Transactions | Code | Status | Message | | --- | ---: | --- | | `TX00001` | 404 | Transaction not found | | `TX00002` | 400 | Amount must be a positive number | | `TX00003` | 400 | The status of the transaction cannot be changed once it is in a final state. | | `TX00004` | 400 | The transaction status cannot go from the current state to the requested state. | | `TX00005` | 500 | Failed to process PAYIN, FIAT_IN transaction missing for order | ### UE β€” Users | Code | Status | Message | | --- | ---: | --- | | `UE00001` | 404 | User not found | ### WA β€” WebAuthn / passkeys | Code | Status | Message | | --- | ---: | --- | | `WA00001` | 400 | Organization is already initialized for passkeys β€” use invite flow | | `WA00002` | 500 | Failed to initialize the passkey signing organization | | `WA00003` | 503 | Passkey signing service is not initialized β€” check configuration | | `WA00004` | 409 | User already has an active passkey for this organization β€” use the recovery flow to replace it | | `WA00005` | 400 | Invalid attestation data | | `WA00006` | 404 | Organization not found | | `WA00007` | 404 | No passkey registered β€” complete registration first | | `WA00008` | 404 | Organization has no passkey signing organization β€” bootstrap required | | `WA00009` | 400 | Invalid or expired MFA challenge | | `WA00010` | 403 | Passkey verification failed | | `WA00011` | 500 | Failed to issue MFA token | | `WA00012` | 404 | Target user not found | | `WA00013` | 409 | Target user already has a passkey for this organization | | `WA00014` | 400 | Organization must be bootstrapped before inviting users | | `WA00015` | 404 | Pending approval not found | | `WA00016` | 403 | Only root user can approve passkey enrollment | | `WA00017` | 500 | Failed to forward stamped request to the signing provider | | `WA00018` | 403 | Only root user can initiate passkey recovery | | `WA00019` | 404 | Target user has no credential to recover for this organization | | `WA00020` | 400 | OTP verification failed β€” invalid or expired code | | `WA00021` | 500 | Failed to send recovery OTP via the signing provider | | `WA00022` | 404 | Target user credential not found β€” cannot complete recovery | | `WA00023` | 500 | Signing provider response did not return a user ID | | `WA00024` | 409 | A passkey signing user with this email already exists in this organization. If the user lost access, initiate passkey recovery. | | `WA00025` | 500 | Failed to create passkey enrollment approval | | `WA00026` | 409 | This device credential is already registered | | `WA00027` | 404 | No MFA factor registered for this organization | ### WE β€” Webhooks | Code | Status | Message | | --- | ---: | --- | | `WE000001` | 404 | Order not found for transaction status update during webhook processing | | `WE000002` | 404 | Transaction not found during webhook processing | | `WE000003` | 409 | A webhook with this URL already exists for this organization | | `WE000004` | 404 | Webhook endpoint not found | --- # Error Handling _How Koywe API errors are structured, how to recover from them, and where to find every code_ Source: https://docs.koywe.com/en/advanced/error-handling # Error Handling Every error returned by the Koywe Platform API carries a stable, unique `errorCode` like `MC00015` or `DC00010`. The uppercase prefix identifies the **domain** (merchants, documents, policy, …); the numeric suffix is the specific error within that domain. > **Looking for a specific code?** The [Error Code Catalog](/en/advanced/error-codes) lists all 685 registered codes grouped by prefix. ## Error response format All API errors follow a consistent envelope: ```json { "statusCode": 400, "timestamp": "2025-04-23T12:34:56.000Z", "path": "/api/v1/organizations/org_.../merchants/mer_.../orders", "errorCode": "BAA00008", "message": "The destination account currency does not match the order destination currency." } ``` **Fields**: - `errorCode` β€” **Always branch on this.** The prefixed code is stable across releases. - `statusCode` β€” HTTP status; useful for generic retry logic. - `message` β€” Human-readable. May be reworded without notice; don't parse it. - `timestamp`, `path` β€” Helpful for support tickets and log correlation. Branch your integration on `errorCode`, not `message`. Messages are refined over time; codes are contracts. --- ## The prefix convention Codes follow the pattern `` β€” for example `MC00015`, `POL00007`, `WE000001`. | Prefix | Domain | Prefix | Domain | | --- | --- | --- | --- | | `AUTH` | Authentication & credentials | `OR` | Orders | | `BAA` | Bank accounts | `ORG` | Organizations | | `BT` | Balance transfer | `PER` | Permissions | | `CT` | Contacts | `PIVBA` | Pay-in virtual bank accounts | | `DC` | Documents (validation) | `PL` | Payment links | | `DL` | Deals | `PMP` | Payment-method providers | | `EWW` | Embedded wallet | `POL` | Policy (MFA & approvals) | | `IMT` | Inter-merchant transfer | `QE` | Quotes | | `LE` | Ledger | `RE` | Reports | | `MC` | Merchants | `WE` | Webhooks | See the [full prefix β†’ domain map](/en/advanced/error-codes#prefix--domain) in the catalog for all 51 prefixes. --- ## HTTP status codes | Status | Meaning | Action | |--------|---------|--------| | `200` | Success | Continue | | `400` | Bad Request | Fix request parameters | | `401` | Unauthorized | Refresh token / check credentials | | `403` | Forbidden | Check permissions and organization/merchant context | | `404` | Not Found | Verify resource ID | | `408` | Timeout | Retry with backoff | | `409` | Conflict | Duplicate / idempotency issue | | `410` | Gone | Resource (e.g. quote, payment link) has expired | | `422` | Validation | Fix input validation | | `428` | Precondition Required | Satisfy a policy (MFA verification or approval) | | `429` | Rate Limit | Wait and retry | | `500` | Server Error | Retry with backoff | | `502` / `503` / `504` | Upstream issue | Retry with backoff | --- ## Common errors you'll hit first A few codes are worth knowing by heart because they trip up most integrations. The [Error Code Catalog](/en/advanced/error-codes#common-errors-youll-hit-first) has a longer list. | Code | Status | When it happens | Fix | |------|------:|------------------|-----| | `AUTH00001` | 401 | Token rejected | Re-auth with `koywe auth login` or refresh env vars | | `MC00015` | 403 | `merchantId` not in the signed-in organization | Check `organizationId` / `merchantId` in config. GETs succeed silently; only POSTs fail. | | `DC00010` | 400 | Document number doesn't match the country's document type format | Use a valid test document (see [Testing in sandbox](/en/getting-started/testing)) | | `BAA00008` | 400 | Destination account currency β‰  order destination currency | Pick a destination account whose currency matches the order | | `BAA00014` | 409 | Payout exceeds virtual-account balance | Fund the account or reduce the amount | | `QE00003` | 410 | Quote expired | Create a new quote | | `QE00004` | 409 | Quote already used by another order | Quotes are single-use β€” create a new one | | `POL00002` | 403 | No policy defined on the organization | `koywe policy create` then add an ALLOW rule | | `POL00007` | 428 | Approval required; order is `ON_HOLD` | Pass `--wait` in `flow order`, approve on the dashboard, or provide `--mfa-token` | | `PMP00009` | 408 | Upstream payment provider timed out | Retry with backoff | --- ## Recovery strategies ### 1. Retry transient failures with backoff Retry on network-level errors and the upstream-unreachable range (`408`, `429`, `5xx`). **Never** retry on `4xx` validation errors β€” they'll keep failing with the same input. ```javascript async function retryWithBackoff(fn, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { try { return await fn(); } catch (error) { const status = error.response?.status; const isRetryable = status === 408 || status === 429 || (status >= 500 && status < 600) || error.code === 'ECONNABORTED'; const isLastAttempt = i === maxRetries - 1; if (!isRetryable || isLastAttempt) throw error; // Exponential backoff with jitter: ~1s, 2s, 4s const delay = Math.pow(2, i) * 1000 + Math.random() * 500; await new Promise(r => setTimeout(r, delay)); } } } ``` ### 2. Handle policy-gated operations `POL00006`, `POL00007`, and `POL00008` (all `428 Precondition Required`) mean the operation needs MFA verification, human approval, or both before it can execute. The order is placed in `ON_HOLD` β€” the creation call succeeds and returns the order β€” and transitions once the approval/MFA is satisfied. ```javascript async function createOrderAndWait(token, orgId, merchantId, payload) { const order = await createPayinOrder(token, orgId, merchantId, payload); if (order.status !== 'ON_HOLD') return order; // Poll until approval resolves β€” the CLI's `flow order --wait` does this // for you. Production integrations should listen to webhooks instead of // polling (see "Webhooks deep dive"). for (let i = 0; i < 60; i++) { await new Promise(r => setTimeout(r, 5000)); const refreshed = await getOrder(token, orgId, merchantId, order.id); if (refreshed.status !== 'ON_HOLD') return refreshed; } throw new Error(`Order ${order.id} still ON_HOLD after 5 minutes`); } ``` ### 3. Graceful degradation for payment methods If a payment method isn't available for the country/currency combo (`PMP00012`, `PMP00017`) or the provider is momentarily down (`PMP00003`, `PMP00008`), fall back to a secondary method: ```javascript async function createPayinWithFallback(orderBase) { const attempts = [ { method: 'PSE', extra: { bankAccount: { name: 'BANCOLOMBIA' } } }, { method: 'NEQUI' }, ]; for (const pm of attempts) { try { return await createPayinOrder(token, orgId, merchantId, { ...orderBase, paymentMethods: [pm], }); } catch (error) { const code = error.response?.data?.errorCode; const retryable = ['PMP00012', 'PMP00017', 'PMP00003', 'PMP00008']; if (!retryable.includes(code)) throw error; } } throw new Error('No payment method available for this country/currency'); } ``` ### 4. Translate codes to end-user messages Map error codes to user-facing copy β€” never show raw codes to customers: ```javascript function translateErrorToUser(error) { const code = error.response?.data?.errorCode; const userMessages = { BAA00014: "We don't have enough balance to complete this payout right now.", DC00010: 'Please double-check the document number.', QE00003: 'That price quote has expired β€” please try again.', PMP00006: 'That amount is below the minimum for this payment method.', PMP00007: 'That amount exceeds the limit for this payment method.', POL00004: 'This transaction exceeds your configured limit. Please contact an admin.', POL00007: 'This operation is waiting for approval. You\'ll be notified shortly.', }; return userMessages[code] ?? 'Something went wrong. Please try again or contact support.'; } ``` ### 5. Log everything with correlation context When calling support, the single most useful thing you can send is the raw error envelope plus the `path` and `timestamp`: ```javascript function logError(error, context) { const apiError = error.response?.data ?? {}; console.error(JSON.stringify({ timestamp: new Date().toISOString(), ...context, statusCode: apiError.statusCode, errorCode: apiError.errorCode, message: apiError.message, path: apiError.path, apiTimestamp: apiError.timestamp, })); } try { await createPayoutOrder(token, orgId, merchantId, payoutData); } catch (error) { logError(error, { operation: 'create_payout', merchantId }); throw error; } ``` --- ## Production error handler Everything above, wired into a single reusable class: ```javascript class KoyweErrorHandler { constructor({ maxRetries = 3, logger = console } = {}) { this.maxRetries = maxRetries; this.logger = logger; } async execute(fn, context = {}) { for (let attempt = 0; attempt < this.maxRetries; attempt++) { try { return await fn(); } catch (error) { this.logError(error, { ...context, attempt }); if (!this.shouldRetry(error, attempt)) { throw this.formatError(error); } const delay = Math.pow(2, attempt) * 1000 + Math.random() * 500; await new Promise(r => setTimeout(r, delay)); } } } shouldRetry(error, attempt) { if (attempt >= this.maxRetries - 1) return false; const status = error.response?.status; return ( status === 408 || status === 429 || (status >= 500 && status < 600) || error.code === 'ECONNABORTED' ); } logError(error, context) { const apiError = error.response?.data ?? {}; this.logger.error('Koywe API error:', { timestamp: new Date().toISOString(), ...context, errorCode: apiError.errorCode, statusCode: apiError.statusCode, message: apiError.message, path: apiError.path, }); } formatError(error) { const apiError = error.response?.data; if (!apiError) return error; const err = new Error(apiError.message); err.errorCode = apiError.errorCode; err.statusCode = apiError.statusCode; err.path = apiError.path; return err; } } const errorHandler = new KoyweErrorHandler({ maxRetries: 3 }); const order = await errorHandler.execute( () => createPayinOrder(token, orgId, merchantId, orderData), { operation: 'create_payin', merchantId }, ); ``` --- ## Service health check Before diagnosing request-level errors, confirm the API itself is reachable. Koywe exposes a lightweight unauthenticated health endpoint: ``` GET /api/v1/healthz ``` ```json { "status": "ok" } ``` Characteristics: - Unauthenticated β€” useful for status pages and CI smoke tests. - Cheap β€” safe to hit frequently from dashboards. - Returns `503` if an upstream dependency is degraded. --- ## Next steps - [Browse the full Error Code Catalog β†’](/en/advanced/error-codes) - [Test error flows in sandbox β†’](/en/getting-started/testing) - [Webhooks deep dive β†’](/en/advanced/webhooks) --- # Notifications _In-app and email notifications for merchant users_ Source: https://docs.koywe.com/en/advanced/notifications # Notifications Koywe emits notifications to keep merchant users informed about order events β€” separately from webhooks, which are meant for server-to-server integrations. There are two independent channels, configured per merchant: - **In-app notifications**: delivered to users signed into the Koywe dashboard. Always on. - **Email notifications**: opt-in. Off by default; enable and choose recipients per merchant. Notifications are for **humans** β€” ops teams, finance, customer support. For programmatic integration, use [Webhooks](/en/advanced/webhooks). --- ## In-App Notifications In-app notifications are generated automatically for **order events only** β€” `eventType` uses the `order.*` subset of the webhook taxonomy (e.g., `order.completed`, `order.failed`) and does **not** cover `merchant.*`, `policy.*`, `webhook.ping`, or other families. Each notification is scoped to a single merchant and can be listed, counted, and marked as read through the API that powers the dashboard notification center. ### Notification Object ```json { "id": "d2dd8481-3552-4466-b99e-a7caad69a302", "merchantId": "koywe-3-sys", "orderId": "ord_abc123", "orderType": "PAYIN", "eventType": "order.completed", "title": "Your payment completed", "body": "Your payment of $500 USD was processed successfully.", "read": false, "readAt": null, "createdAt": "2026-04-13T18:00:00.000Z", "metadata": null } ``` | Field | Description | |-------|-------------| | `id` | Notification ID | | `merchantId` | Merchant the notification belongs to | | `orderId` | Related order, if any | | `orderType` | `PAYIN`, `PAYOUT`, `ONRAMP`, `OFFRAMP`, `BALANCE_TRANSFER`, `PAYMENT_LINK` | | `eventType` | Order subset of the webhook taxonomy (`order.completed`, `order.failed`, …). Other event families (`merchant.*`, `policy.*`, `webhook.ping`) do not produce in-app notifications | | `title` / `body` | Human-readable content, already localized | | `read` / `readAt` | Read state. `readAt` is `null` until marked read | | `metadata` | Optional event-specific payload | ### List Notifications ``` GET /api/v1/merchants/{merchantId}/notifications/inapp ``` Paginated, newest first. **Query parameters** | Name | Default | Description | |------|---------|-------------| | `page` | `1` | Page number | | `limit` | `50` | Items per page | ```javascript const response = await axios.get( `https://api.koywe.com/api/v1/merchants/${merchantId}/notifications/inapp`, { params: { page: 1, limit: 50 }, headers: { Authorization: `Bearer ${token}` } } ); const { data, total, page, limit } = response.data; ``` ### Unread Count ``` GET /api/v1/merchants/{merchantId}/notifications/inapp/unread-count ``` Returns `{ "count": 3 }`. Use this for the badge on your bell icon β€” it's cheaper than paginating the full list. ### Mark One as Read ``` PATCH /api/v1/merchants/{merchantId}/notifications/inapp/{notificationId}/read ``` Returns `{ "ok": true }`. Returns error `NE00001` if the notification does not exist, and `NE00002` if it belongs to a different merchant. ### Mark All as Read ``` PATCH /api/v1/merchants/{merchantId}/notifications/inapp/read-all ``` Returns `{ "updated": 5 }` β€” the number of notifications that transitioned from unread to read. --- ## Email Notification Settings Email notifications are **off by default**. Enable them per merchant and provide a list of recipient addresses. Koywe then emails the recipients on order events alongside the in-app notification and webhook. ### Get Settings ``` GET /api/v1/merchants/{merchantId}/notification-settings ``` Returns the configured settings, or the defaults if nothing has been set yet: ```json { "emailEnabled": false, "recipients": [] } ``` ### Update Settings ``` PUT /api/v1/merchants/{merchantId}/notification-settings ``` ```json { "emailEnabled": true, "recipients": ["ops@merchant.com", "finance@merchant.com"] } ``` Rules: - `emailEnabled` is required. - When `emailEnabled` is `true`, at least one valid email address must be provided in `recipients`. - When `emailEnabled` is `false`, `recipients` may be omitted and the existing recipient list is preserved until you explicitly clear it. - When `recipients` is sent, it replaces the entire existing list. To clear all recipients, send an empty array (`recipients: []`); omitting the field does **not** clear previously configured recipients. ```javascript await axios.put( `https://api.koywe.com/api/v1/merchants/${merchantId}/notification-settings`, { emailEnabled: true, recipients: ['ops@merchant.com', 'finance@merchant.com'] }, { headers: { Authorization: `Bearer ${token}` } } ); ``` --- ## When to Use What | Channel | Audience | Best for | |---------|----------|----------| | **Webhooks** | Your servers | Fulfilling orders, reconciliation, anything automated | | **In-app** | Dashboard users | Live activity feed, "what just happened" | | **Email** | Ops / finance / support | Out-of-band alerts for teams not staring at the dashboard | The three channels are independent β€” an order event fires all three where configured. --- ## Next Steps Server-to-server event delivery Handle notification errors Full notification endpoint reference Events that generate notifications --- # Passkeys & Approvals _WebAuthn MFA, embedded wallet signing, and transactional policy approvals_ Source: https://docs.koywe.com/en/advanced/passkeys-and-approvals # Passkeys & Approvals Koywe exposes a public approval layer for sensitive actions. It combines WebAuthn passkeys, short-lived MFA tokens, and transactional policy approvals. ## Public Endpoint Families ### WebAuthn / Passkeys - `POST /api/v1/organizations/{organizationId}/webauthn/register/init` - `POST /api/v1/organizations/{organizationId}/webauthn/register/complete` - `POST /api/v1/organizations/{organizationId}/webauthn/challenge` - `POST /api/v1/organizations/{organizationId}/webauthn/verify` - `GET /api/v1/organizations/{organizationId}/webauthn/credentials` - `GET /api/v1/organizations/{organizationId}/webauthn/credentials/all` ### Enrollment, Wallet Provisioning, and Recovery - `POST /api/v1/organizations/{organizationId}/webauthn/enroll/init` - `POST /api/v1/organizations/{organizationId}/webauthn/enroll/prepare` - `POST /api/v1/organizations/{organizationId}/webauthn/wallet/prepare` - `GET /api/v1/organizations/{organizationId}/webauthn/wallet/{merchantId}/status` - `POST /api/v1/organizations/{organizationId}/webauthn/wallet/complete` - recovery endpoints under `/webauthn/recovery/*` ### Transactional Policy - `POST /api/v1/organizations/{organizationId}/policy` - `GET /api/v1/organizations/{organizationId}/policy` - `POST /api/v1/organizations/{organizationId}/policy/rules` - `PUT /api/v1/organizations/{organizationId}/policy/rules/{ruleId}` - `GET /api/v1/organizations/{organizationId}/policy/audit` ### Pending Approvals - `GET /api/v1/organizations/{organizationId}/policy/approvals` - `GET /api/v1/organizations/{organizationId}/policy/approvals/{approvalId}` - `POST /api/v1/organizations/{organizationId}/policy/approvals/{approvalId}/approve` - `POST /api/v1/organizations/{organizationId}/policy/approvals/{approvalId}/reject` ## Human User Passkey Enrollment Human users should enroll passkeys in **Koywe Platform**, not by calling the WebAuthn endpoints directly. This is the recommended flow for super admins and other operators who need to approve sensitive actions: 1. Sign in to Koywe Platform with your normal user account. 2. If your organization does not have passkeys active yet, Koywe will show a prompt to enroll or activate passkeys for the organization. 3. A super admin starts the enrollment flow and approves passkey activation for the organization. 4. Each user who must operate protected flows is prompted to create their own passkey. 5. Once created, that user can use the passkey for MFA and approval flows inside the platform. ### What the User Experience Looks Like - Users will see an in-product prompt to create a passkey when their organization requires it. - The browser or device will open the native passkey dialog. - The user can store the passkey on the current device or in a password manager / cloud keychain. - If their passkey provider syncs across devices, the passkey will usually become available automatically on their other enrolled devices as well. ### Recommended Operator Guidance - Ask at least one super admin to complete organization activation first. - Ask every approver to enroll their own passkey before enabling high-friction approval policies. - Tell users to save the passkey in a synced provider when possible, so they are not tied to a single device. - Treat this as the standard path for human users; reserve the API-user signing flow below for delegated or service-driven operations. ## API User Signing Tutorial If you want an API user to participate in delegated MFA or embedded wallet signing flows, the simplest setup is: 1. generate a PEM key pair 2. submit the public key for enrollment 3. have a root user approve the enrollment 4. sign protected payloads locally with the private key ### Step 0: Create the API User Credentials Before enrolling delegated MFA, create organization- or merchant-scoped API credentials for the API user. See [Organization Setup & Invitations](/en/getting-started/organization-setup). ### Step 1: Generate a PEM Key Pair ```bash openssl genpkey -algorithm RSA -out api-user-private.pem -pkeyopt rsa_keygen_bits:2048 openssl rsa -pubout -in api-user-private.pem -out api-user-public.pem ``` Keep the private key in your secret manager. Only the public key should be submitted to Koywe. ### Step 2: Submit the Public Key for Delegated MFA Enrollment The API user submits its public key to create a pending approval: ```bash curl -X POST 'https://api-sandbox.koywe.com/api/v1/auth/organizations/YOUR_ORG_ID/api-users/mfa/prepare' \ -H 'Authorization: Bearer YOUR_API_USER_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "publicKey": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----" }' ``` This returns the enrollment payload and the `pendingApprovalId` that the root user will approve. ### Step 3: Root User Approves the Enrollment The root passkey holder loads the prepared enrollment request: ```bash curl -X POST 'https://api-sandbox.koywe.com/api/v1/auth/organizations/YOUR_ORG_ID/api-users/mfa/enroll/prepare' \ -H 'Authorization: Bearer YOUR_ROOT_USER_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "pendingApprovalId": "pap_123" }' ``` The root user signs the returned request with their passkey and finalizes approval with: ```bash curl -X POST 'https://api-sandbox.koywe.com/api/v1/auth/organizations/YOUR_ORG_ID/api-users/mfa/enroll/approve' \ -H 'Authorization: Bearer YOUR_ROOT_USER_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "pendingApprovalId": "pap_123", "stampedRequest": { "...": "signed-by-root-passkey" } }' ``` You can then confirm the delegated credential exists with: - `GET /api/v1/auth/organizations/{organizationId}/api-users/{apiKey}/mfa` ### Step 4: Sign Payloads with the Private Key Once approved, your API user keeps the PEM private key locally and uses it when a protected flow requires delegated signing. Minimal Node.js example: ```javascript const privateKey = readFileSync('./api-user-private.pem', 'utf8') const payload = JSON.stringify(requestBody) const signer = createSign('RSA-SHA256') signer.update(payload) signer.end() const signature = signer.sign(privateKey, 'base64') ``` The exact payload shape and transport field depend on the protected endpoint you are calling. Use the relevant request schema in the [API Reference](/api-reference) as the source of truth. ## Embedded Wallet Access Variant For embedded wallet access, the approval flow is similar but uses dedicated endpoints: 1. API or user context requests access with `POST /api/v1/auth/organizations/{organizationId}/wallet-access/request` 2. root user loads the approval payload with `POST /api/v1/auth/organizations/{organizationId}/wallet-access/prepare` 3. root user signs and finalizes with `POST /api/v1/auth/organizations/{organizationId}/wallet-access/approve` The operational model is the same: request, root approval, then perform the protected signing flow with the approved signer. ## How the Flow Works >API: Request WebAuthn challenge API-->>U: Challenge nonce U->>P: Sign challenge U->>API: Verify signed challenge API-->>U: MFA token U->>API: Perform protected action API-->>A: Create pending approval if policy requires it Approver->>API: Approve or reject pending operation`} /> ## Recommended Mental Model - Passkeys prove the identity of the acting user. - The MFA token authorizes a short-lived protected action. - Transactional policy decides whether the action can proceed immediately or requires approval. - Approval endpoints let a separate approver accept or reject that pending operation. For a full walkthrough of how policy rules are written, ordered, and combined with single-approver or M-of-N quorum schemes, see [Transactional Policy & Approval Schemes](/en/advanced/transactional-policy). ## When You Need This These flows matter when you use: - protected bank-account creation flows that require MFA confirmation - embedded wallet or signing flows - organization-level approval rules for treasury or operational controls ## Best Practices - Treat MFA tokens as short-lived session artifacts, not durable credentials. - Build a pending-approval state into your application if you enable transactional policy. - Surface approval IDs in operator tooling so reviewers can approve or reject quickly. - Pair this guide with [Security Best Practices](/en/advanced/security) for credential and webhook hardening. ## Next Steps - [Transactional Policy & Approval Schemes](/en/advanced/transactional-policy) - [Security Best Practices](/en/advanced/security) - [Organization Setup & Invitations](/en/getting-started/organization-setup) - [API Reference](/api-reference) --- # Security Best Practices _Secure your Koywe integration_ Source: https://docs.koywe.com/en/advanced/security # Security Best Practices Essential security practices for production Koywe integrations. ## API Credentials ### Store Securely **Do**: - Store in environment variables - Use secret management services (AWS Secrets Manager, HashiCorp Vault) - Rotate credentials periodically - Use different credentials for sandbox and production **Don't**: - Hardcode credentials in source code - Commit credentials to version control - Share credentials via email/chat - Use production credentials in development {/* Multi-language code examples */} {/* Secure Storage */} ```javascript // βœ… Good - Environment variables const API_KEY = process.env.KOYWE_API_KEY; const SECRET = process.env.KOYWE_SECRET; // ❌ Bad - Hardcoded const API_KEY = 'sk_live_abc123...'; // NEVER DO THIS ``` ### Environment Separation | Environment | Purpose | Credentials | |------------|---------|-------------| | **Development** | Local development | Sandbox | | **Staging** | Pre-production testing | Sandbox | | **Production** | Live application | Production | ### Rotating API Secrets Regular secret rotation is a critical security practice. Rotate your API secrets: - **On schedule**: Quarterly rotation recommended - **After security events**: Suspected compromise, employee offboarding - **For compliance**: Many security frameworks require periodic rotation #### Rotation Endpoint **Endpoint**: `POST /api/v1/auth/credentials/rotate-secret` **Authentication**: Bearer token required **Restriction**: Only API users can rotate secrets (not email-authenticated users) #### How to Rotate {/* Multi-language code examples */} {/* Node.js */} ```javascript async function rotateApiSecret(currentToken) { // Step 1: Call rotate endpoint with current token const response = await axios.post( 'https://api.koywe.com/api/v1/auth/credentials/rotate-secret', {}, { headers: { 'Authorization': `Bearer ${currentToken}` } } ); const newSecret = response.data.secret; console.log('New secret generated (store immediately!)'); // Step 2: Store new secret in your secret manager await secretManager.update('KOYWE_SECRET', newSecret); // Step 3: Verify new secret works const verifyResponse = await axios.post( 'https://api.koywe.com/api/v1/auth/sign-in', { apiKey: process.env.KOYWE_API_KEY, secret: newSecret } ); if (verifyResponse.data.token) { console.log('βœ“ New secret verified successfully'); // Step 4: Remove old secret from vault await secretManager.delete('KOYWE_SECRET_OLD'); } return newSecret; } ``` {/* Python */} ```python def rotate_api_secret(current_token): # Step 1: Call rotate endpoint with current token response = requests.post( 'https://api.koywe.com/api/v1/auth/credentials/rotate-secret', headers={'Authorization': f'Bearer {current_token}'} ) new_secret = response.json()['secret'] print('New secret generated (store immediately!)') # Step 2: Store new secret in your secret manager secret_manager.update('KOYWE_SECRET', new_secret) # Step 3: Verify new secret works verify_response = requests.post( 'https://api.koywe.com/api/v1/auth/sign-in', json={ 'apiKey': os.environ['KOYWE_API_KEY'], 'secret': new_secret } ) if verify_response.json().get('token'): print('βœ“ New secret verified successfully') # Step 4: Remove old secret from vault secret_manager.delete('KOYWE_SECRET_OLD') return new_secret ``` {/* cURL */} ```bash # Rotate secret curl -X POST 'https://api.koywe.com/api/v1/auth/credentials/rotate-secret' \ -H 'Authorization: Bearer YOUR_CURRENT_TOKEN' # Response: { "secret": "2af81190b3a153r48a3df3a1eefcc386ca763b99fba53d39666751ffd4e2ae81" } ``` **Important**: The new secret is only shown **once** in the response. Store it immediately in your secret manager before discarding the old secret. #### Rotation Best Practices **Rotation Checklist:** - [ ] Backup current secret before rotating - [ ] Store new secret in secret manager immediately - [ ] Verify new secret works before removing old one - [ ] Update all environments that use the secret - [ ] Log the rotation event for audit purposes - [ ] Test authentication after rotation #### Error Handling | Status Code | Error | Meaning | |-------------|-------|---------| | `401` | Unauthorized | Token invalid or expired | | `403` | Only API users can rotate | Email-authenticated users cannot rotate secrets | | `500` | Internal Server Error | Retry the request | ```javascript try { await rotateApiSecret(token); } catch (error) { if (error.response?.status === 403) { console.error('Secret rotation requires API user authentication, not email login'); } else if (error.response?.status === 401) { console.error('Token expired - re-authenticate before rotating'); } } ``` --- ## Webhook Security ### 1. Signature Verification **Always verify webhook signatures**: {/* Multi-language code examples */} {/* Signature Verification */} ```javascript function verifyWebhookSignature(payload, signature, secret) { const expectedSignature = crypto .createHmac('sha256', secret) .update(payload) .digest('hex'); return signature === expectedSignature; } // In webhook handler if (!verifyWebhookSignature(req.body, req.headers['koywe-signature'], secret)) { return res.status(401).send('Invalid signature'); } ``` ### 2. HTTPS Only - Webhook endpoints must use HTTPS - Obtain valid SSL certificate - Redirect HTTP to HTTPS ### 3. Validate Event Structure ```javascript function validateWebhookEvent(event) { if (!event.id || !event.type || !event.data) { throw new Error('Invalid event structure'); } if (!event.data.orderId) { throw new Error('Missing orderId'); } return true; } ``` --- ## Token Security ### Token Management {/* Multi-language code examples */} {/* Secure Token Handling */} ```javascript class SecureTokenManager { constructor(apiKey, secret) { this.apiKey = apiKey; this.secret = secret; this.token = null; this.tokenExpiry = null; } async getToken() { // Check if token is still valid if (this.token && this.tokenExpiry > Date.now() + (5 * 60 * 1000)) { return this.token; } // Get new token const response = await axios.post( 'https://api.koywe.com/api/v1/auth/sign-in', { apiKey: this.apiKey, secret: this.secret } ); this.token = response.data.token; this.tokenExpiry = Date.now() + (60 * 60 * 1000); // 1 hour return this.token; } clearToken() { this.token = null; this.tokenExpiry = null; } } // Usage const tokenManager = new SecureTokenManager(API_KEY, SECRET); const token = await tokenManager.getToken(); ``` ### Token Transmission - Always use HTTPS for API requests - Include token in `Authorization` header (not URL) - Never log tokens - Clear tokens on logout ```javascript // βœ… Good - Header axios.get(url, { headers: { 'Authorization': `Bearer ${token}` } }); // ❌ Bad - URL parameter axios.get(`${url}?token=${token}`); // NEVER DO THIS ``` --- ## Data Protection ### PII (Personally Identifiable Information) **Sensitive Data**: - Customer names - Email addresses - Phone numbers - Document numbers - Bank account numbers **Best Practices**: {/* Multi-language code examples */} {/* Encrypt at Rest */} ```javascript const crypto = require('crypto'); function encryptPII(data, key) { const iv = crypto.randomBytes(16); const cipher = crypto.createCipheriv('aes-256-gcm', key, iv); let encrypted = cipher.update(data, 'utf8', 'hex'); encrypted += cipher.final('hex'); const authTag = cipher.getAuthTag(); return { encrypted, iv: iv.toString('hex'), authTag: authTag.toString('hex') }; } // Store encrypted data const encryptedEmail = encryptPII(customer.email, ENCRYPTION_KEY); await db.save({ encryptedEmail }); ``` ### Logging **Do**: - Log request IDs, timestamps, statuses - Log errors and exceptions - Log business events **Don't Log**: - API credentials - Tokens - PII (emails, phone numbers, documents) - Bank account numbers - Full credit card numbers ```javascript // βœ… Good logging console.log('Order created:', { orderId: order.id, type: order.type, amount: order.amountIn, currency: order.originCurrencySymbol }); // ❌ Bad logging console.log('Order created:', { orderId: order.id, customerEmail: 'customer@example.com', // DON'T LOG PII token: 'Bearer abc123...' // DON'T LOG TOKENS }); ``` --- ## Network Security ### Firewall Configuration **Recommended**: - Whitelist Koywe API IPs - Restrict outbound traffic - Use VPC/private subnets - Enable DDoS protection ### TLS/SSL - Use TLS 1.2 or higher - Verify SSL certificates - Enable certificate pinning (optional) ```javascript const axios = require('axios'); const https = require('https'); // Enforce TLS 1.2+ const agent = new https.Agent({ minVersion: 'TLSv1.2', rejectUnauthorized: true }); const api = axios.create({ httpsAgent: agent }); ``` --- ## Input Validation ### Validate All Inputs {/* Multi-language code examples */} {/* Input Validation */} ```javascript function validateOrderInput(input) { // Amount if (typeof input.amount !== 'number' || input.amount <= 0) { throw new Error('Invalid amount'); } // Currency const validCurrencies = ['COP', 'BRL', 'MXN', 'CLP', 'USD']; if (!validCurrencies.includes(input.currency)) { throw new Error('Invalid currency'); } // External ID if (!/^[a-zA-Z0-9-_]+$/.test(input.externalId)) { throw new Error('Invalid external ID format'); } return true; } ``` ### Sanitize User Input ```javascript function sanitizeDescription(description) { // Remove potentially harmful characters return description .replace(/[<>]/g, '') // Remove < > .replace(/javascript:/gi, '') // Remove javascript: .trim() .substring(0, 255); // Limit length } ``` --- ## Rate Limiting Implement rate limiting to prevent abuse: {/* Multi-language code examples */} {/* Rate Limiting */} ```javascript const rateLimit = require('express-rate-limit'); const apiLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // Limit each IP to 100 requests per windowMs message: 'Too many requests, please try again later.' }); // Apply to API routes app.use('/api/', apiLimiter); // Stricter limit for sensitive operations const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 5, // Only 5 auth attempts skipSuccessfulRequests: true }); app.post('/api/auth', authLimiter, authHandler); ``` --- ## Error Handling ### Don't Expose Internal Details {/* Multi-language code examples */} {/* Safe Error Messages */} ```javascript function handleError(error, res) { // Log detailed error internally console.error('Internal error:', { stack: error.stack, message: error.message, timestamp: new Date() }); // Return generic message to client res.status(500).json({ error: 'An error occurred. Please try again or contact support.', requestId: generateRequestId() }); // ❌ Don't do this // res.status(500).json({ error: error.stack }); // Exposes internals } ``` --- ## Compliance ### PCI DSS If handling card data: - Never store CVV - Tokenize card numbers - Use PCI-compliant infrastructure - Conduct regular security audits ### GDPR/Data Protection - Obtain user consent for data storage - Provide data export functionality - Implement data deletion - Maintain audit logs - Encrypt PII --- ## Monitoring and Alerts ### Security Monitoring {/* Multi-language code examples */} {/* Security Alerts */} ```javascript function monitorSuspiciousActivity(request) { // Multiple failed auth attempts if (failedAttempts > 5) { alertSecurityTeam('Multiple failed auth attempts', { ip: request.ip, attempts: failedAttempts }); } // Unusual payment amounts if (amount > THRESHOLD) { alertSecurityTeam('High-value transaction', { orderId: order.id, amount: amount, currency: currency }); } // Geographic anomalies if (isUnusualLocation(request.ip)) { alertSecurityTeam('Unusual location', { ip: request.ip, location: getLocation(request.ip) }); } } ``` ### Audit Logging ```javascript function auditLog(action, details) { await db.auditLogs.insert({ timestamp: new Date(), action: action, userId: details.userId, resource: details.resource, changes: details.changes, ip: details.ip, userAgent: details.userAgent }); } // Usage await auditLog('order.created', { userId: user.id, resource: `order:${order.id}`, changes: { amount: order.amountIn, currency: order.currency }, ip: req.ip, userAgent: req.headers['user-agent'] }); ``` --- ## Security Checklist **Production Checklist**: - [ ] API credentials stored in environment variables - [ ] Different credentials for sandbox/production - [ ] HTTPS enforced on all endpoints - [ ] Webhook signature verification implemented - [ ] Token management with expiry handling - [ ] PII encrypted at rest - [ ] Sensitive data not logged - [ ] Input validation on all user inputs - [ ] Rate limiting configured - [ ] Error messages don't expose internals - [ ] Security monitoring and alerts setup - [ ] Audit logging implemented - [ ] Regular security audits scheduled - [ ] Incident response plan documented --- ## Next Steps Handle errors securely Secure webhook implementation Security testing MFA and approval workflows --- # Transactional Policy & Approval Schemes _How to model rules, conditions, and multi-party approvals for sensitive money movements_ Source: https://docs.koywe.com/en/advanced/transactional-policy # Transactional Policy & Approval Schemes Koywe's **transactional policy** is the layer that decides what happens when a user or API key tries to perform a sensitive money movement. It works the same way most enterprise wallet policies do: you write an ordered list of rules, the first matching rule wins, and that rule says either *let it through* or *require an approval* from one or more designated approvers. This page explains the building blocks at a high level. It is meant for the person who decides who can do what in your organization β€” not for someone implementing signing libraries. For signing mechanics, see [Passkeys & Approvals](/en/advanced/passkeys-and-approvals). ## Which Operations Are Subject to Policy Transactional policy applies to operations that **move funds**, **change a saved destination**, or **change policy/security configuration**. In practice that covers: - **Money movements** that have a destination β€” payouts, offramps, onramps, balance transfers. - **Destination changes** β€” adding, editing, or deleting a saved bank account or wallet address. - **Policy management** β€” changes to the policy itself (adding rules, reordering them, swapping approvers). If an operation is not in this set, the policy is never consulted and the operation runs as normal. You only need to think about transactional policy when you want to control **who** can do **what**, with **how much**, and **how many** people need to sign off. Every new organization is onboarded with a **default policy attached to the first super admin** β€” it is good enough for a solo super admin running the whole operation. As soon as you add other operators (humans or API users) or want different controls per merchant, balance, or amount band, the super admin should replace the default with a proper policy designed for your structure. ## The Three Pieces A working setup has three pieces: 1. **The policy** β€” a single container that holds the rules for your organization. 2. **The rules** β€” an ordered list. Each rule describes a situation ("any payout of $10,000 USD or more") and an outcome. 3. **The approvers** β€” the human users (with enrolled passkeys) who will be asked to approve when a rule says approval is required. You manage these through the dashboard, the [CLI](/en/cli/reference#policy-mfa--approvals), or the REST API. ## How a Rule Is Evaluated Every in-scope operation is checked against the policy **before** it is executed. The policy walks the rules in order and stops at the first one that matches. That rule's outcome decides what happens next. If no user-defined rule matches, an implicit **catch-all rule at position `99999` denies the operation**. There is no implicit "allow" β€” the default behavior of a policy is to reject anything you have not explicitly written a rule for. New organizations are not empty: they ship with a starter policy whose rules cover the first super admin; once you grow past that, your own rules need to keep at least one path open for the operations you want to support. B{Walk rules in order} B -->|Rule 1 matches| C[Apply rule 1 outcome] B -->|No match, try next| D{Rule 2} D -->|Match| E[Apply rule 2 outcome] D -->|No match| F[... continue ...] F --> G[Catch-all rule 99999: DENY] C --> H{Outcome} E --> H G --> H H -->|Allow| I[Execute immediately] H -->|Deny| J[Reject with policy error] H -->|Require approval| K[Create pending approval, wait for approvers]`} /> **Rules are first-match-wins β€” order is policy.** Put your stricter rules at the top and your broader fallbacks at the bottom, the same way you'd organize a firewall rule list. Swap the order and a permissive rule will swallow operations you meant to send to a stricter rule. The implicit `99999` deny is always last and cannot be removed or reordered. ## What a Rule Can Match On A rule is a set of filters combined with an outcome. All filters in a single rule are ANDed together: every filter must match for the rule to apply. To make a filter match anything, set it to the wildcard string `"*"` β€” that is how "any value" is expressed at the API level (the dashboard exposes this as an "Any" option in each picker). The supported filters are: - **Operation type** β€” what is being attempted. The canonical values come straight from the policy enum: `PAYOUT_FIAT`, `PAYOUT_CRYPTO`, `BALANCE_TRANSFER`, `DESTINATION_EDIT`, `POLICY_MANAGE` (plus enrollment/security operations such as `PASSKEY_ENROLL`, `API_USER_MFA_ENROLL`, `API_USER_MFA_REVOKE`, `EMBEDDED_WALLET_ACCESS_GRANT`, `USER_INVITE`). Note that at the policy layer there is no `ONRAMP` or `OFFRAMP` token β€” an offramp is gated as `PAYOUT_FIAT` (fiat leaves to the destination) and an onramp is gated as `PAYOUT_CRYPTO` (crypto leaves to the destination). This is the most common starting point: most policies are organized first by operation type. - **Initiator** β€” who is acting. Typical values are `*` (any user or API key), a specific role (for example a super admin), or a specific user or API user. Use this to write rules like "only super admins can manage the policy" or "this API user can auto-approve payouts up to $1,000". - **Amount threshold** β€” a **minimum** amount, always normalized to **USD**. The rule matches every operation whose USD-equivalent is **greater than or equal to** the threshold. A threshold of `0` matches every amount (i.e. all operations of that type); a threshold of `10000` matches every operation of $10,000 USD-equivalent or more. There is no maximum field β€” to express a band like "between $1k and $10k", you order rules from highest threshold to lowest and let first-match-wins do the rest (see [Rule Ordering & Precedence](#rule-ordering--precedence) below). For non-monetary operations (destination edits, policy management), the amount is ignored β€” set it to `0`. - **Source** β€” where the money comes from inside your organization. This has two parts: a **specific merchant** (one of the merchants under your organization) and a **specific balance** in that merchant (i.e. a balance in a given currency, fiat or crypto). Use this to write rules like "payouts from Merchant A's USDC balance need approval" while leaving Merchant B untouched. - **Destination / counterparty** β€” who is on the other side: a specific bank account, a specific contact, a saved wallet address, or any non-whitelisted destination. This is how you express things like "payouts to whitelisted bank accounts are auto-approved, anything else needs review". Every destination referenced by a transaction must be **pre-created in Koywe** before it can be used β€” you cannot send to an ad-hoc address. The policy filters on which of those pre-created destinations is being used (and on whether it has been whitelisted). **`*` on operation type does not include `POLICY_MANAGE`.** The wildcard matches every other in-scope operation, but never policy-management actions. To cover changes to the policy itself, you must write a rule whose operation type is explicitly `POLICY_MANAGE`. This is a safety design: it prevents you from accidentally granting policy edits via a broad fallback rule. A rule with every filter set to `"*"` matches every in-scope operation **except policy management**. That is occasionally useful as an explicit fallback above the implicit `99999` deny. ## The Possible Outcomes A rule resolves to one of these outcomes: - **Allow** β€” the operation goes through immediately. No approver is paged. This is the right outcome for routine, low-risk movements (small payouts to whitelisted bank accounts, internal rebalancing, etc.). - **Require approval** β€” the operation is held as a **pending approval**. It does not execute until approvers act on it. This is where the approval schemes below kick in. - **Deny** β€” the implicit `99999` catch-all rejects the operation up front. You don't write deny rules by hand; you express "this should never happen" by **not** writing an allow/approval rule for it and letting the catch-all do its job. ## Approval Schemes When a rule's outcome is *require approval*, the rule also specifies **who** has to approve and **how many** of them. Koywe supports three schemes, in increasing strictness: ### Auto-approve Effectively the *allow* outcome above β€” no human is in the loop. You list it here because, when you are designing a policy, the question "should this be auto-approved?" is the first one you ask for every operation type. ### Single approver The operation is sent to a designated group of approvers (typically by listing the users explicitly). **Any one** of them can approve it. The first approval releases the operation; any approver can also reject it, which kills the request. This is the most common scheme for day-to-day operational controls β€” for example: "payouts above $1,000 require one super admin to approve". ### M-of-N quorum The operation is sent to a group of N approvers and requires **M distinct approvals** before it can execute (for example, 2-of-3 or 3-of-5). Each approver can only contribute one vote. A single rejection from any member of the group kills the request. Use this for the most sensitive operations: large payouts, destination whitelisting changes, anything that would be catastrophic if a single approver were compromised. The point is to make collusion the only failure mode. Every approval β€” single or quorum β€” is anchored to the approver's **passkey**. The approver signs the approval payload with their passkey; the signature is what counts, not just a click. See [Passkeys & Approvals](/en/advanced/passkeys-and-approvals) for the signing mechanics. ### Who Can Be an Approver By default, approvers are **human users** with a passkey enrolled in Koywe Platform. **API users** can also be listed as approvers in a policy. By default, an API user's approval is just an authenticated API call β€” **no MFA or cryptographic signature is required**. This is convenient when you want a service to participate in approvals (for example, an internal automation that double-checks a payout against an external ledger). If you want stronger guarantees on those automated approvals, you can opt into MFA signing for the API user. That requires two things: 1. **Koywe enables the API-user signing feature on your account** β€” this is a manual flag, not self-service. Contact Koywe to turn it on. 2. **The API user onboards a public key (PEM)** with Koywe. From that moment on, every approval from that API user must be signed with the matching private key; an unsigned call is rejected. Use this when an API-user approver guards something material β€” for example, when it acts as one of the votes in an M-of-N quorum on a high-value flow. For lower-stakes automation, the default "no MFA" behavior is fine. See the [API User Signing Tutorial](/en/advanced/passkeys-and-approvals#api-user-signing-tutorial) for the key-onboarding flow. ## Putting It Together: A Worked Example Imagine a treasury team that wants the following behavior: 1. Money movements (fiat payouts, crypto payouts including offramps/onramps, balance transfers) of **$5,000 USD or more**: **two of three** treasury officers must approve. 2. Money movements under $5,000: go through automatically. 3. Any **destination edit** (changing a saved bank account or wallet address): two of three treasury officers must approve. 4. The **super admin** can manage the policy itself. 5. Anything else: implicitly denied by rule `99999`. Notice how the verbal needs collapse into a shorter rule list once you stop thinking in narrative and start thinking in filters. Here is the rule list that implements the behavior above, in order. Comma-separated values in the *Operation type* column are **OR** conditions β€” Rule 1 matches if the operation is `PAYOUT_FIAT` **or** `PAYOUT_CRYPTO` **or** `BALANCE_TRANSFER` and the amount is at least $5,000. A rule with multiple operation types is still a single rule, not three. | # | Operation type | Initiator | Source | Destination | Amount (USD min) | Outcome | | --- | --- | --- | --- | --- | --- | --- | | 1 | `PAYOUT_FIAT`, `PAYOUT_CRYPTO`, `BALANCE_TRANSFER` | `*` | `*` | `*` | `5000` | Require approval β€” 2-of-3 treasury officers | | 2 | `PAYOUT_FIAT`, `PAYOUT_CRYPTO`, `BALANCE_TRANSFER` | `*` | `*` | `*` | `0` | Allow | | 3 | `DESTINATION_EDIT` | `*` | `*` | `*` | `0` (ignored) | Require approval β€” 2-of-3 treasury officers | | 4 | `POLICY_MANAGE` | super admin user | `*` | `*` | `0` (ignored) | Allow | | 99999 | `*` | `*` | `*` | `*` | `*` | **Deny** (implicit catch-all) | A few things to notice: - **Rule 1 must come before rule 2.** Thresholds are minimums, so rule 2's `0` would match every amount if you put it first; rule 1 would never fire. - **Rule 3 has to be its own rule** because `DESTINATION_EDIT` is not a money movement and is not in Rules 1 or 2's explicit operation-type lists β€” those rules only cover `PAYOUT_FIAT`, `PAYOUT_CRYPTO`, and `BALANCE_TRANSFER`. As a side note, if Rule 2 had used the wildcard `*` instead of the explicit list, it would have caught `DESTINATION_EDIT` under $5,000 first and auto-approved it β€” which is exactly why we listed operation types explicitly. The wildcard `*` includes `DESTINATION_EDIT` but **not** `POLICY_MANAGE`, so Rule 4 also has to be explicit. - **Rule 4 is the policy-management rule.** It's typically the rule new organizations get by default, attached to the first super admin. Don't delete it without a replacement, or no one will be able to edit the policy. - **The implicit `99999` deny** sits at the very end, untouched. Anything not matched by rules 1–4 is rejected. That is how this policy expresses "anything else: denied" β€” by simply not writing a rule for it. If you flipped the order β€” for example, put rule 2 above rule 1 β€” every payout, no matter how large, would fall into the auto-approve case, because rule 2's threshold of `0` matches it first. **Order is policy.** Every destination in these rules must be **pre-created** in Koywe (as a contact's bank account, a saved wallet address, or a merchant external account) before any payout can reference it. The policy decides *whether* a transaction to that destination is allowed; it doesn't relax the requirement that the destination exists in the first place. Once this baseline works, you can layer extra rules on top β€” for example, an "auto-approve" rule above rule 1 that targets payouts to a specific whitelisted contact, or a stricter rule that targets a particular merchant's USDC balance. ## End-to-End Approval Flow When a rule requires approval, the lifecycle of the operation looks like this: >K: Trigger protected operation (e.g. payout) K->>P: Evaluate against policy rules P-->>K: Matched rule β†’ require approval (2-of-3) K->>Q: Create pending approval (status: PENDING) K-->>R: Response: pendingApprovalId Q-->>A1: Notify approver 1 Q-->>A2: Notify approver 2 A1->>K: Sign approval with passkey K->>Q: Record approval (1/2) A2->>K: Sign approval with passkey K->>Q: Record approval (2/2) β†’ quorum reached Q->>K: Release operation K-->>R: Webhook: operation executed`} /> Three details are worth calling out: - The original request gets a `pendingApprovalId` immediately. Your application should treat that as a normal, expected response β€” not an error β€” and surface it to the requester. - Approvers do not need to be online when the operation is triggered. They are notified, and they act on the approval queue (`koywe policy approvals list` or the dashboard) on their own time. - A single rejection ends the request, regardless of how many approvals were already gathered. There is no "majority wins" β€” quorum means *M positive votes with no negative votes*. ## Rule Ordering & Precedence Three practical guidelines: - **Highest amount thresholds first.** Because thresholds are minimums and a threshold of `0` matches every amount, a rule with `0` placed above a rule with `10000` will swallow every operation and the higher-threshold rule will never run. Sort rules from the highest threshold down to `0` within each operation type. - **Most specific filters first, broad fallbacks last.** A rule "payouts at or above $5,000 to non-whitelisted destinations require 2-of-3" should sit above "payouts at or above $5,000 require one approver", which should sit above any catch-all you write. The implicit `99999` deny is always at the very end. - **Reordering is a privileged operation.** Use the dashboard or `koywe policy rules reorder` to change the order. Every reorder is recorded in the policy audit log; you can review it later through `koywe policy audit`. When you are not sure how a given operation will be routed, the audit log is the best place to look: it records which rule matched and what the outcome was, for every evaluation. ## Operator Playbook A pragmatic checklist for the person rolling this out: - **Treat the default policy as a starting point, not a permanent setup.** New orgs are onboarded with a default policy that works for a solo super admin. The moment you add other operators, more merchants, or want amount-based controls, design and replace it deliberately β€” don't accumulate rules on top of the default without understanding what it already allows. - **Enroll passkeys first.** Approval rules are useless if your approvers can't sign. Make sure every intended approver has a passkey enrolled in Koywe Platform before you switch on any approval-required rules. See [Passkeys & Approvals](/en/advanced/passkeys-and-approvals). - **Decide whether API-user approvers should sign with MFA.** API users can be approvers out of the box, no extra setup β€” their approval is just an authenticated API call. If a given API-user approver guards something material (a quorum vote on a large payout, for instance), upgrade it to MFA signing: ask Koywe to enable the API-user signing feature on your account, then have the API user onboard a PEM public key. Both steps are manual, so plan ahead. - **Start permissive, tighten over time.** Begin with a small number of high-value rules (for example, only payouts above a large threshold require approval). Watch the audit log for a week. Then add more specificity. You can always add rules β€” you cannot easily un-block operations that were rejected. - **Always test in sandbox first.** Build the same policy in the sandbox environment, run real operations through it, and confirm the right rule fires. Policy bugs are far cheaper to find before they block production movements. - **Have at least three approvers for quorum rules.** A 2-of-3 with only two enrolled passkeys becomes 2-of-2 in practice, which means one absent approver blocks all critical operations. For 2-of-3, enroll at least four people if you can. - **Don't put humans in the auto-approve path of high-volume flows.** If your business does hundreds of small payouts per day, do not require approval on those β€” your approvers will start rubber-stamping, which defeats the purpose. Reserve approval for events that are rare and material. - **Review the audit log on a schedule.** `koywe policy audit` (or the dashboard equivalent) shows every rule match, every approval, every rejection. Treat it like a SIEM feed for treasury. - **Document the policy in plain language outside Koywe.** Approvers should know what they are approving and why. A one-page internal doc that explains "we have these rules because…" makes approvers thoughtful instead of mechanical. ## Common Pitfalls - **Replacing the default with a policy that has no path for your operations.** Brand-new orgs ship with a working default policy, but as soon as you rewrite it you own the outcomes. A policy that contains no rule matching a given operation β€” or where you deleted the only allow rule for it β€” falls through to the catch-all `99999` and rejects every such operation. If production movements suddenly start failing with a policy error after a policy edit, this is the first thing to check. - **Deleting the `POLICY_MANAGE` rule.** The default policy includes a rule allowing the super admin to manage the policy itself. Because operation-type `*` does **not** include `POLICY_MANAGE`, no other "broad" rule covers it. Delete that rule without a replacement and nobody β€” including the super admin β€” will be able to edit the policy. You will need to contact Koywe to recover. - **Expecting `*` on operation type to cover policy management.** It doesn't. Always write `POLICY_MANAGE` as its own explicit rule. - **Ordering bugs with the `0` threshold.** A rule with amount threshold `0` matches every amount. If you place it above a rule with a higher threshold, the higher-threshold rule will never run. Always sort by threshold descending within an operation type. - **Ordering bugs in general.** Adding a strict rule at the bottom of the list does nothing β€” an earlier permissive rule will already have matched. Always check the resulting order with `koywe policy info`. - **Missing approvers.** A quorum rule that requires 2-of-3 from a group where only one person has a passkey enrolled will deadlock every matching operation. Add approvers before activating the rule. - **Treating policy errors as bugs.** When a request is held for approval, your application gets back a `pendingApprovalId`, not the final resource. Code that expects an immediate success will look broken. Plan for this state. - **Editing the policy under load.** Reordering or deleting rules takes effect on the next evaluation. Avoid policy edits during a payout batch β€” finish the batch, then change the policy. ## Where to Go Next - [Passkeys & Approvals](/en/advanced/passkeys-and-approvals) β€” the cryptographic side: how passkeys, MFA tokens, and approval signatures fit together. - [CLI Reference β€” Policy & Approvals](/en/cli/reference#policy-mfa--approvals) β€” the exact commands for creating policies, adding rules, listing pending approvals, and viewing the audit log. - [Security Best Practices](/en/advanced/security) β€” credential hardening and webhook verification, both of which complement policy controls. --- # Webhooks Deep Dive _Advanced webhook handling and best practices_ Source: https://docs.koywe.com/en/advanced/webhooks # Webhooks Deep Dive Advanced guide to implementing production-ready webhook handling. ## Overview Webhooks allow Koywe to send real-time notifications about order status changes to your server. ### Why Use Webhooks? **Benefits**: - Real-time order status updates - No polling required - Scalable architecture - Reliable delivery with retries --- ## Webhook Signature Verification **Critical**: Always verify webhook signatures to ensure authenticity. ### Verification Process {/* Multi-language code examples */} {/* Node.js Express */} ```javascript const crypto = require('crypto'); const express = require('express'); app.post('/webhooks/koywe', express.raw({ type: 'application/json' }), // MUST use raw body (req, res) => { // 1. Get signature from header const signature = req.headers['koywe-signature']; const secret = process.env.KOYWE_WEBHOOK_SECRET; // 2. Calculate expected signature const expectedSignature = crypto .createHmac('sha256', secret) .update(req.body) // Raw body (Buffer) .digest('hex'); // 3. Compare signatures if (signature !== expectedSignature) { console.error('Invalid webhook signature'); return res.status(401).send('Invalid signature'); } // 4. Signature valid, parse event const event = JSON.parse(req.body); // 5. Process event await processWebhook(event); // 6. Respond quickly res.status(200).send('OK'); } ); ``` {/* Python Flask */} ```python from flask import Flask, request app = Flask(__name__) @app.route('/webhooks/koywe', methods=['POST']) def webhook(): # 1. Get signature signature = request.headers.get('Koywe-Signature') secret = os.environ['KOYWE_WEBHOOK_SECRET'].encode() # 2. Calculate expected signature expected_signature = hmac.new( secret, request.data, hashlib.sha256 ).hexdigest() # 3. Verify if signature != expected_signature: return 'Invalid signature', 401 # 4. Parse event event = json.loads(request.data) # 5. Process process_webhook(event) # 6. Respond return 'OK', 200 ``` **Common Mistakes**: - Using `express.json()` instead of `express.raw()` - this modifies the body - Using parsed JSON for signature calculation - must use raw body - Wrong secret - verify you're using webhook secret, not API secret --- ## Idempotency Handle duplicate webhook deliveries: {/* Multi-language code examples */} {/* Idempotent Processing */} ```javascript const processedEvents = new Set(); // In production, use Redis/database async function processWebhook(event) { const eventId = event.id; // Check if already processed if (processedEvents.has(eventId)) { console.log(`Event ${eventId} already processed, skipping`); return; } try { // Process event await handleEvent(event); // Mark as processed processedEvents.add(eventId); // In production, persist to database await db.saveProcessedEvent(eventId, new Date()); } catch (error) { console.error(`Error processing event ${eventId}:`, error); // Don't mark as processed - allow retry throw error; } } ``` --- ## Event Types Koywe emits events across several resource families. All events share the same top-level envelope (`id`, `type`, `version`, `occurred_at`, `source`, `environment`, `organization_id`, `merchant_id`, `data`) shown in the next section β€” but the contents of `data` vary by event family. Fields like `orderId` and `amountIn` in the example below are specific to the order family; they do **not** apply to `merchant.*`, `policy.*`, `invitation.*`, `bank_income.*`, or `webhook.ping` events, which carry their own resource-specific `data` shapes. The taxonomy is additive β€” new events may be introduced over time, and any breaking changes are signaled via the envelope's `version` field. ### Order Lifecycle | Event Type | When Fired | |-----------|------------| | `order.created` | Order is first created | | `order.approved` | Policy approval cleared the order; it moves into `PROCESSING` | | `order.processing` | Payment or transfer is being processed | | `order.paid` | Payment confirmed (payment provider or on-chain confirmation) | | `order.completed` | Funds settled β€” credited for PAYIN/ONRAMP, debited for PAYOUT/OFFRAMP | | `order.failed` | Order failed at any stage (payment, ledger, transfer) | | `order.expired` | Order passed its due date without being paid | | `order.canceled` | Order canceled by the user or the system | | `order.refunded` | Order was refunded after completion | | `order.updated` | Generic fallback for order state changes not covered above | **Note on spelling**: The canonical spelling is `order.canceled` (one `l`). Earlier internal drafts used `order.cancelled` β€” if you have handlers matching that spelling, switch them to `order.canceled`. ### Merchant & KYB | Event Type | When Fired | |-----------|------------| | `merchant.created` | A merchant is created under an organization | | `merchant.kyb.in_progress` | KYB review started for the merchant | | `merchant.kyb.approved` | KYB passed; merchant can operate in production | | `merchant.kyb.rejected` | KYB was rejected; remediation required | ### Invitations Fired for both user-level invitations and organization-level invitations (distinguishable via the `resourceType` in the payload: `invitation` vs `organization_invitation`). | Event Type | When Fired | |-----------|------------| | `invitation.created` | An invitation is issued | | `invitation.accepted` | The recipient accepted the invitation | | `invitation.expired` | The invitation expired before acceptance | | `invitation.failed` | Invitation delivery or processing failed | | `invitation.assigned` | Invitation target was resolved/assigned to a user | ### Policy Approvals Fired when a protected operation goes through the policy-approval flow. | Event Type | When Fired | |-----------|------------| | `policy.approval.requested` | A policy triggered; one or more approvers notified | | `policy.approval.received` | An individual approver responded (approve/reject) | | `policy.approval.approved` | Threshold met; the request is approved | | `policy.approval.rejected` | The request was rejected by policy | ### Policy Execution A separate dynamic event fires when a policy finally executes against its target resource. The event type embeds the resource type: ``` policy.{resourceType}.executed ``` Currently emitted resource types: | Event Type | Resource | |-----------|----------| | `policy.order.executed` | Order | | `policy.account.executed` | Bank account | | `policy.deal.executed` | Deal | | `policy.user_invite.executed` | User invitation | | `policy.passkey_enrollment.executed` | Passkey enrollment | | `policy.wallet_access_policy.executed` | Wallet access policy | | `policy.policy.executed` | Policy definition | | `policy.policy_rule.executed` | Policy rule | If you want to handle all policy executions generically, match on the `policy.*.executed` pattern rather than enumerating each resource type β€” new ones are added as new resource kinds become policy-protected. ### Bank Income | Event Type | When Fired | |-----------|------------| | `bank_income.received` | A deposit was credited to a virtual bank account β€” fired for both real bank transfers and [sandbox simulations](/en/getting-started/testing#simulating-bank-income-sandbox-only) | ### System | Event Type | When Fired | |-----------|------------| | `webhook.ping` | Test event emitted by the webhook `ping` endpoint; use to verify connectivity and signature verification | ### Event Payload Structure ```json { "id": "evt_abc123xyz", "type": "order.completed", "version": "v1", "occurred_at": "2025-11-13T15:30:00Z", "source": "koywe.api", "environment": "production", "organization_id": "org_xyz789", "merchant_id": "mrc_abc123", "data": { "orderId": "ord_123456", "type": "PAYIN", "status": "COMPLETED", "amountIn": 50000, "originCurrencySymbol": "COP", "destinationCurrencySymbol": "COP", "externalId": "order-12345", "contactId": "cnt_customer1", "dates": { "confirmationDate": "2025-11-13T15:29:00Z", "paymentDate": "2025-11-13T15:29:30Z", "deliveryDate": "2025-11-13T15:30:00Z" } } } ``` --- ## Event Handling ### Complete Event Handler {/* Multi-language code examples */} {/* Production Handler */} ```javascript async function handleEvent(event) { const { type, data } = event; switch (type) { case 'order.created': await onOrderCreated(data); break; case 'order.approved': await onOrderApproved(data); break; case 'order.processing': await onOrderProcessing(data); break; case 'order.paid': await onOrderPaid(data); break; case 'order.completed': await onOrderCompleted(data); break; case 'order.failed': await onOrderFailed(data); break; case 'order.expired': await onOrderExpired(data); break; case 'order.canceled': await onOrderCanceled(data); break; case 'order.refunded': await onOrderRefunded(data); break; case 'order.updated': await onOrderUpdated(data); break; default: // Other families (merchant.*, merchant.kyb.*, invitation.*, // policy.approval.*, policy.*.executed, bank_income.*, webhook.ping) // are handled in their own routers. console.warn(`Unhandled event type: ${type}`); } } async function onOrderCompleted(data) { // CRITICAL: This is where you fulfill orders if (data.type === 'PAYIN') { // Customer paid, fulfill order await fulfillOrder(data.externalId); await sendConfirmationEmail(data.externalId); await updateInventory(data.externalId); } else if (data.type === 'PAYOUT') { // Payout completed, mark as paid await markInvoiceAsPaid(data.externalId); await notifyProvider(data.externalId); } } async function onOrderFailed(data) { // Handle failure await markOrderFailed(data.externalId); await sendFailureNotification(data.externalId); // Log for investigation console.error('Order failed:', { orderId: data.orderId, externalId: data.externalId, type: data.type }); } ``` --- ## Response Times Respond to webhooks quickly (< 5 seconds): {/* Multi-language code examples */} {/* Async Processing */} ```javascript app.post('/webhooks/koywe', express.raw({ type: 'application/json' }), async (req, res) => { // Convert raw body to string once const rawBody = req.body.toString(); // 1. Verify signature (fast) if (!verifySignature(rawBody, req.headers['koywe-signature'])) { return res.status(401).send('Invalid signature'); } // Parse JSON from string const event = JSON.parse(rawBody); // 2. Check idempotency (fast) if (await isEventProcessed(event.id)) { return res.status(200).send('Already processed'); } // 3. Respond immediately res.status(200).send('OK'); // 4. Process asynchronously (AFTER responding) setImmediate(async () => { try { await processWebhook(event); await markEventProcessed(event.id); } catch (error) { console.error('Webhook processing error:', error); await logWebhookError(event, error); } }); }); ``` **Best Practice**: Respond immediately (200 OK) and process the webhook asynchronously. This prevents timeouts and ensures reliable delivery. --- ## Retry Policy Koywe sends each webhook once, then retries only if the failure looks transient. The rules are: - **Request timeout**: 30 seconds. Slower responses are treated as a network failure. - **Retryable failures** β€” delivery will be retried with backoff: - `5xx` responses from your endpoint - `429 Too Many Requests` - Network errors and timeouts (no HTTP response at all) - **Non-retryable failures** β€” delivery is marked `FAILED` after a single attempt, with **no** retry: - Any other `4xx` response (e.g. `400`, `401`, `403`, `404`, `422`) Retryable failures are re-enqueued with backoff. Each attempt increments `retryCount` and appends to the delivery's `attempts[]`; once the maximum retry budget is exhausted, `deliveryStatus` flips to `FAILED` β€” this applies to `429` too, so returning `429` signals throttling but does **not** exempt the delivery from being marked failed if retries are exhausted. You can inspect delivery state via `GET /api/v1/organizations/:organizationId/webhook-events/:eventId/deliveries`, and re-send a failed event manually with the replay endpoint. **Retries only apply before you ACK.** Koywe's automatic retry logic (`5xx` / `429` / network timeout) only triggers when it receives an error response β€” or no response at all β€” from your endpoint. Once you return `200 OK`, the delivery is considered successful and **will not** be retried, even if your background processing later fails. If you use the [ACK-first pattern shown above](#response-times), you must **durably persist** the event (e.g., enqueue to a reliable job queue or commit to a database) **before** returning `200 OK`; otherwise a crash between ACK and processing will lose the event and Koywe will not redeliver it automatically β€” you'll need to call the replay endpoint manually. **`4xx` is terminal.** If your endpoint returns `401` because a secret rotation hasn't landed yet, or `404` because of a path typo, Koywe will not retry β€” it assumes the request is fundamentally wrong. Fix the endpoint, then use `POST /api/v1/organizations/:organizationId/webhook-events/:eventId/replay` to resend. Return `5xx` if you want Koywe to retry automatically (e.g. during transient downstream outages). Return `429` to signal throttling β€” retries still apply with backoff, and the delivery can still be marked `FAILED` once the retry budget is exhausted. ### Handling Retries {/* Multi-language code examples */} {/* Idempotent Handler */} ```javascript async function processWebhook(event) { const eventId = event.id; // Use database for persistence (not in-memory) const alreadyProcessed = await db.isEventProcessed(eventId); if (alreadyProcessed) { console.log(`Event ${eventId} already processed`); return; // Return success without re-processing } // Process event await handleEvent(event); // Mark as processed AFTER successful handling await db.markEventProcessed(eventId, { processedAt: new Date(), eventType: event.type, orderId: event.data.orderId }); } ``` --- ## Testing Webhooks ### Local Testing with ngrok ### Install ngrok ```bash npm install -g ngrok ``` ### Start your local server ```bash node server.js # Server running on http://localhost:3000 ``` ### Create ngrok tunnel ```bash ngrok http 3000 ``` ### Use ngrok URL ``` Forwarding: https://abc123.ngrok.io -> http://localhost:3000 ``` ### Configure webhook Use `https://abc123.ngrok.io/webhooks/koywe` as your webhook URL ### Test Create orders and watch webhooks arrive in real-time ### Using webhook.site For quick testing without code: 1. Visit https://webhook.site 2. Copy your unique URL 3. Use as webhook URL in API 4. View incoming webhooks in browser --- ## Production Checklist **Before going live**: - [ ] Signature verification implemented - [ ] Idempotency handling in place - [ ] Quick response times (< 5 seconds) - [ ] Async processing implemented - [ ] Error logging configured - [ ] Event persistence to database - [ ] Monitoring and alerts setup - [ ] Tested with ngrok/webhook.site - [ ] Webhook endpoint is HTTPS - [ ] Firewall allows Koywe IPs --- ## Monitoring and Debugging ### Webhook Logs Query webhook delivery attempts: {/* Multi-language code examples */} {/* Node.js */} ```javascript async function getWebhookDeliveries(token, orgId, orderId) { const response = await axios.get( `https://api.koywe.com/api/v1/organizations/${orgId}/webhook-events`, { params: { orderId }, headers: { 'Authorization': `Bearer ${token}` } } ); return response.data; } // Usage const deliveries = await getWebhookDeliveries(token, orgId, 'ord_123'); deliveries.forEach(d => { console.log(`Attempt ${d.attempt}: ${d.status} (${d.responseCode})`); console.log(` Delivered at: ${d.deliveredAt}`); console.log(` Response time: ${d.responseTime}ms`); }); ``` ### Replay Webhooks Replay a webhook manually: {/* Multi-language code examples */} {/* Replay Event */} ```javascript async function replayWebhook(token, orgId, eventId) { const response = await axios.post( `https://api.koywe.com/api/v1/organizations/${orgId}/webhook-events/{eventId}/replay`, {}, { headers: { 'Authorization': `Bearer ${token}` } } ); console.log('Webhook replayed'); } ``` --- ## Security Best Practices **Critical Security Measures**: 1. **Always verify signatures** - Never skip this step 2. **Use HTTPS** - Webhooks over HTTP are insecure 3. **Validate event structure** - Check required fields exist 4. **Use webhook secret** - Don't use API secret 5. **Log all webhooks** - For audit trail 6. **Rate limit your endpoint** - Protect against attacks --- ## Next Steps Handle errors gracefully Security best practices Test webhook scenarios Complete webhook API docs