# Generate API Key
Source: https://documentation.kotanipay.com/v3/api-reference/authentication/generate-key
GET /api/v3/auth/api-key
This endpoint is used to generate an the API Key which can be used to authorize transactions and more
Please pass the token from the magic link email in the `Authorization` header as a Bearer token to generate your API key.
# Human Verification
Source: https://documentation.kotanipay.com/v3/api-reference/authentication/human-verify
POST /api/v3/auth/human-verify
Complete human verification for potentially scanner-detected magic link requests. This step ensures the request is from a real user.
Complete human verification for magic link requests that were flagged as potentially suspicious to ensure account security.
# Authentication Login
Source: https://documentation.kotanipay.com/v3/api-reference/authentication/login
POST /api/v3/auth/login
Login to your integrator account. This endpoint allows you to login to your account
After submitting your login request, a verification email with a **magic link** will be sent to your registered email address. Please check your spam or junk folder if you don't see it in your inbox.
# Refresh Access Token
Source: https://documentation.kotanipay.com/v3/api-reference/authentication/refresh-token
GET /api/v3/auth/refresh-token
This endpoint is used to refresh the access token using a valid refresh token
Refresh an expired access token using a valid refresh token to maintain authenticated access to the API.
# Get Available Banks
Source: https://documentation.kotanipay.com/v3/api-reference/country-support/banks
GET /api/v3/customer/support/banks
Returns a list of all supported banks with optional filtering by currency or search term. Banks are sourced from the centralized bank data registry.
Get a list of all supported banks with optional filtering by currency or search term.
# Get Supported Countries
Source: https://documentation.kotanipay.com/v3/api-reference/country-support/countries
GET /api/v3/customer/support/countries
Returns a list of all supported countries with their configurations. Supports filtering by service type, currency, country code, and active status. Data is aggregated from the Provider Registry for real-time accuracy. The isEnabled flag indicates whether the authenticated integrator has this service enabled and providers attached.
Get a list of all supported countries with their configurations, filterable by service type, currency, and status.
# Get Country Details
Source: https://documentation.kotanipay.com/v3/api-reference/country-support/country-details
GET /api/v3/customer/support/countries/{countryCode}
Returns comprehensive details about a specific country including supported currencies, networks, and service availability. The isEnabled flag indicates whether the authenticated integrator has each service enabled and providers attached.
Get comprehensive details about a specific country including supported currencies, networks, banks, and service availability.
# Get Deposit Service Configuration
Source: https://documentation.kotanipay.com/v3/api-reference/country-support/deposit-services
GET /api/v3/customer/support/services/deposit
Returns all countries that support deposit (onramp) services with their configurations.
Get all countries that support deposit services with their configurations, payment methods, fees, and limits.
# Get Available Networks
Source: https://documentation.kotanipay.com/v3/api-reference/country-support/networks
GET /api/v3/customer/support/networks/{countryCode}
Returns all active mobile money networks available in a specific country, aggregated from all providers. Indicates if network selection is required.
Get all active mobile money networks available in a specific country, including network codes and phone number prefixes.
# Get Withdraw Service Configuration
Source: https://documentation.kotanipay.com/v3/api-reference/country-support/withdraw-services
GET /api/v3/customer/support/services/withdraw
Returns all countries that support withdraw (offramp) services with their configurations.
Get all countries that support withdrawal services with their configurations, payment methods, fees, and limits.
# Create Mobile Money Customer
Source: https://documentation.kotanipay.com/v3/api-reference/customers/mobile-money/create
POST /api/v3/customer/mobile-money
An integrator can use this endpoint to create the customers who will be either receiving or sending money using mobile money.
An integrator can use this endpoint to create customers who will be either receiving or sending money using mobile money.
# Get Mobile Money Customer
Source: https://documentation.kotanipay.com/v3/api-reference/customers/mobile-money/get
GET /api/v3/customer/mobile-money/{customer_key}
An integrator can use this endpoint to get the customer who will be either receiving or sending money using mobile money by passing customer key.
An integrator can use this endpoint to get the customer who will be either receiving or sending money using mobile money by passing customer key.
# Get Customer by Phone
Source: https://documentation.kotanipay.com/v3/api-reference/customers/mobile-money/get-by-phone
GET /api/v3/customer/mobile-money/phone/{phone_number}
An integrator can use this endpoint to get the customer who will be either receiving or sending money using mobile money by passing phone number.
An integrator can use this endpoint to get the customer who will be either receiving or sending money using mobile money by passing phone number.
# List Mobile Money Customers
Source: https://documentation.kotanipay.com/v3/api-reference/customers/mobile-money/list
GET /api/v3/customer/mobile-money
An integrator can use this endpoint to get all the customers who will be either receiving or sending money using mobile money.
An integrator can use this endpoint to get all the customers who will be either receiving or sending money using mobile money.
# Update Mobile Money Customer
Source: https://documentation.kotanipay.com/v3/api-reference/customers/mobile-money/update
PATCH /api/v3/customer/mobile-money/{customer_key}
An integrator can use this endpoint to update the customers who will be either receiving or sending money using mobile money.
An integrator can use this endpoint to update the customers who will be either receiving or sending money using mobile money.
# Name Lookup
Source: https://documentation.kotanipay.com/v3/api-reference/customers/name-lookup
POST /api/v3/name-lookup
Checks the phone number and network already on file for the given customer_key against whichever provider (pawaPay or Onafriq) would actually handle a payout to them, reducing the risk of sending funds to the wrong recipient.
Before disbursing a payout, confirm the recipient's name matches what the receiving mobile money provider has on file for that phone number — reduces the risk of sending funds to the wrong person.
The phone number and network checked are always the ones already on file for the `customer_key` you pass — you can't look up an arbitrary phone number, only an existing mobile money customer.
This feature is gated per integrator. If your account doesn't have it enabled, contact [sales@kotanipay.com](mailto:sales@kotanipay.com) to request access.
Match scoring (`matchScore`) is only available for some payout corridors — it may be absent even on a `MATCHED` result.
# Validate Bank Account
Source: https://documentation.kotanipay.com/v3/api-reference/customers/validate-bank
POST /api/v3/customer/validate/bank
Validates a bank account number format for a given country and bank code. Returns validation status, bank information, and whether the country is supported.
Validates a bank account by checking the account number format, bank code, and country support. Returns validation status and bank information.
# Validate Mobile Money Account
Source: https://documentation.kotanipay.com/v3/api-reference/customers/validate-mobile-money
POST /api/v3/customer/validate/mobile-money
Validates a phone number in any format (with or without country code). Returns validation status, country information, formatted number, and whether the country is supported for mobile money.
Validates a mobile money account by checking the phone number format and country support. Optionally verifies if the account exists and returns account holder information.
# Bank Checkout Deposit
Source: https://documentation.kotanipay.com/v3/api-reference/deposits/bank-checkout
POST /api/v3/deposit/bank/checkout
Customer Completed transaction using Checkout Url
Customer completed transaction using checkout URL. This endpoint allows customers to deposit funds via bank checkout.
# Bank Checkout Deposit Status
Source: https://documentation.kotanipay.com/v3/api-reference/deposits/bank-checkout-status
GET /api/v3/deposit/bank/checkout/status/{reference_id}
An integrator can use this endpoint to check the status of a deposit
An integrator can use this endpoint to check the status of a bank checkout deposit transaction.
# Card Deposit
Source: https://documentation.kotanipay.com/v3/api-reference/deposits/card
POST /api/v3/deposit/card
Customer Completed transaction using Checkout Url
Customer completed transaction using checkout URL. This endpoint allows customers to deposit funds via card payment.
# Card Deposit Status
Source: https://documentation.kotanipay.com/v3/api-reference/deposits/card-status
GET /api/v3/deposit/card/{reference_id}
An integrator can use this endpoint to check the status of a deposit
An integrator can use this endpoint to check the status of a card deposit transaction.
# Deposit On-Chain (Receive Crypto)
Source: https://documentation.kotanipay.com/v3/api-reference/deposits/deposit-on-chain
POST /api/v3/deposit/on-chain
This endpoint is marked as **deprecated** but is fully functional. It will be replaced with a clearer naming convention in the future.
## Use Case
This endpoint is for **e-commerce platforms and businesses that want to accept mobile money payments and receive the equivalent in cryptocurrency**.
**Perfect for:**
* E-commerce platforms accepting crypto payments
* Merchants wanting to receive payments in stablecoins
* Platforms converting fiat payments to crypto automatically
**NOT for:**
* Selling crypto to customers (use `/onramp` instead)
* Running a crypto exchange
## How It Works
1. Your customer pays via mobile money (STK push)
2. We collect the mobile money payment
3. We convert the fiat to crypto at current rates
4. **Your crypto wallet receives the USDT/crypto**
5. You fulfill the customer's order
## Flow
```mermaid theme={null}
sequenceDiagram
participant Customer
participant KotaniPay
participant MobileMoney
participant Blockchain
participant Merchant
Customer->>KotaniPay: Initiate deposit
KotaniPay->>MobileMoney: Send STK push
MobileMoney->>Customer: Request PIN
Customer->>MobileMoney: Enter PIN
MobileMoney->>KotaniPay: Payment successful
KotaniPay->>Blockchain: Convert & send crypto
Blockchain->>Merchant: Receive USDT/crypto
KotaniPay->>Merchant: Webhook notification
```
## Key Differences
| Aspect | This Endpoint (Deposit On-Chain) | Onramp Endpoint |
| ------------------------ | -------------------------------- | -------------------------------- |
| Who receives crypto? | ✅ **You** (the merchant) | Customer |
| Who receives fiat? | Payment processor | ✅ **You** (the merchant) |
| Crypto inventory needed? | ❌ No | ✅ Yes (must have crypto to sell) |
| Use case | Accept payments in crypto | Sell crypto to customers |
## Examples
### Receive crypto to your wallet
When you DON'T provide `public_address`, crypto goes to your wallet:
```json theme={null}
{
"customer_key": "cus_abc123",
"wallet_id": "wallet_polygon_usdt_xyz",
"amount": 1450,
"chain": "POLYGON",
"token": "USDT",
"callback_url": "https://yourdomain.com/webhook",
"reference_id": "order_12345"
}
```
**Result:** You receive \~0.65 USDT in your POLYGON wallet
### Send crypto to customer's address
When you provide `public_address`, crypto goes directly to that address:
```json theme={null}
{
"customer_key": "cus_abc123",
"wallet_id": "wallet_polygon_usdt_xyz",
"amount": 1450,
"chain": "POLYGON",
"token": "USDT",
"public_address": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
"callback_url": "https://yourdomain.com/webhook",
"reference_id": "order_12345"
}
```
**Result:** Customer receives \~0.65 USDT at their address
## Supported Chains & Tokens
Get supported chains and tokens:
```bash theme={null}
GET /api/v3/deposit/on-chain/supported-chains
```
Response:
```json theme={null}
{
"data": {
"POLYGON": ["USDT", "USDC"],
"ETHEREUM": ["USDT", "USDC"],
"CELO": ["CUSD"],
"STELLAR": ["USDC"],
"SOLANA": ["USDT", "USDC"]
}
}
```
## Status Tracking
Track your deposit status:
```bash theme={null}
GET /api/v3/deposit/on-chain/status/{reference_id}
```
See [Get Deposit On-Chain Status](/v3/api-reference/deposits/deposit-on-chain-status) for details.
# Get Deposit On-Chain Status
Source: https://documentation.kotanipay.com/v3/api-reference/deposits/deposit-on-chain-status
GET /api/v3/deposit/on-chain/status/{reference_id}
Get the status of a deposit on-chain transaction, including both mobile money collection and blockchain transfer details.
## Response Structure
The response contains two parts:
1. **mobile\_money** - Status of the mobile money collection
2. **onchain** - Status of the blockchain transfer
## Status Values
### Mobile Money Status
| Status | Description |
| ------------ | ----------------------------------- |
| `PENDING` | STK push sent, waiting for customer |
| `SUCCESSFUL` | Payment collected successfully |
| `FAILED` | Payment failed or cancelled |
### Crypto Received Status
| Status | Description |
| ------------ | ---------------------------------- |
| `PENDING` | Blockchain transaction in progress |
| `SUCCESSFUL` | Crypto received at destination |
| `FAILED` | Blockchain transaction failed |
## Example Response
```json theme={null}
{
"data": {
"mobile_money": {
"id": "dep_abc123",
"status": "SUCCESSFUL",
"amount": 1450,
"currency": "CDF",
"transaction_amount": 1450,
"transaction_cost": 50,
"reference_id": "order_12345",
"provider": "VODACOM",
"created_at": "2025-01-07T12:00:00Z"
},
"onchain": {
"status": "SUCCESSFUL",
"crypto_received_status": "SUCCESSFUL",
"chain": "POLYGON",
"token": "USDT",
"public_address": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
"crypto_amount": 0.65,
"transaction_hash": "0xabc...def"
}
}
}
```
## Webhook Notification
You'll receive a webhook when the transaction completes:
```json theme={null}
{
"event": "deposit.onchain.successful",
"data": {
"reference_id": "order_12345",
"status": "SUCCESSFUL",
"mobile_money_status": "SUCCESSFUL",
"crypto_received_status": "SUCCESSFUL",
"amount": 1450,
"currency": "CDF",
"crypto_amount": 0.65,
"chain": "POLYGON",
"token": "USDT",
"transaction_hash": "0xabc...def",
"created_at": "2025-01-07T12:00:00Z",
"completed_at": "2025-01-07T12:02:30Z"
}
}
```
# Get Supported Chains & Tokens
Source: https://documentation.kotanipay.com/v3/api-reference/deposits/deposit-on-chain-supported-chains
GET /api/v3/deposit/on-chain/supported-chains
Get the list of supported blockchain networks and tokens for deposit on-chain transactions.
## Use Case
Use this endpoint to:
* Display available crypto options to your users
* Validate chain/token combinations before initiating deposits
* Build dynamic UI for chain/token selection
## Response Format
```json theme={null}
{
"data": {
"POLYGON": ["USDT", "USDC"],
"ETHEREUM": ["USDT", "USDC"],
"CELO": ["CUSD"],
"STELLAR": ["USDC"],
"SOLANA": ["USDT", "USDC"],
"TRON": ["USDT"],
"BASE": ["USDC"]
}
}
```
## Integration Example
```javascript theme={null}
// Fetch supported chains
const response = await fetch('https://api.kotanipay.io/api/v3/deposit/on-chain/supported-chains', {
headers: {
'Authorization': 'Bearer YOUR_API_KEY'
}
});
const { data } = await response.json();
// Display to user
Object.entries(data).forEach(([chain, tokens]) => {
console.log(`${chain}: ${tokens.join(', ')}`);
});
// Validate user selection
function isValidSelection(chain, token) {
return data[chain]?.includes(token) ?? false;
}
// Example
console.log(isValidSelection('POLYGON', 'USDT')); // true
console.log(isValidSelection('POLYGON', 'DAI')); // false
```
## Notes
* Supported chains/tokens may vary by country
* Check this endpoint periodically for updates
* Some chains may have minimum amounts (check pricing)
# List Deposit Transactions
Source: https://documentation.kotanipay.com/v3/api-reference/deposits/list-transactions
GET /api/v3/deposit/mobile-money/transactions
Returns a paginated list of the integrator's deposit transactions. Supports filtering by status, date range, telco ID, and currency.
Returns a paginated list of the integrator's mobile money deposit transactions. Supports filtering by status, date range, telco/MNO transaction ID, and currency.
# Deposit via Mobile Money
Source: https://documentation.kotanipay.com/v3/api-reference/deposits/mobile-money
POST /api/v3/deposit/mobile-money
An integrator’s customers can initiate a deposit from their respective mobile money wallets. An STK push will be sent to the customer and the respective amount will be deducted from their mobile money wallets and deposited into the integrator’s fiat wallet
An integrator's customers can initiate a deposit from their respective mobile money wallets. An STK push will be sent to the customer and the respective amount will be deducted from their mobile money wallets and deposited into the integrator's fiat wallet.
# Get Deposit Mobile Money Status
Source: https://documentation.kotanipay.com/v3/api-reference/deposits/mobile-money-status
GET /api/v3/deposit/mobile-money/status/{reference_id}
An integrator can use this endpoint to check the status of a deposit
An integrator can use this endpoint to check the status of a mobile money deposit transaction.
The `reference_id` path parameter accepts either the Kotani platform reference ID or the telco/MNO transaction ID — whichever is available.
# Get Integrator
Source: https://documentation.kotanipay.com/v3/api-reference/integrator/get
GET /api/v3/integrator
This endpoint is used to retrieve an integrator details
Retrieve the details of the authenticated integrator account, including business information and account status.
# Update Webhook Configuration
Source: https://documentation.kotanipay.com/v3/api-reference/integrator/update-webhook
PATCH /api/v3/integrator/webhook
Allows an integrator to update their default webhook URL, secret, and event subscriptions.
Update your integrator's webhook URL, secret, and event subscriptions for receiving real-time notifications.
# API Reference
Source: https://documentation.kotanipay.com/v3/api-reference/introduction
Kotani Pay API endpoints for digital asset transactions using local payment channels
Welcome to the Kotani Pay API reference. Our API enables businesses to
purchase digital assets using local payment channels such as mobile money and
bank transfers.
## Welcome
Kotani Pay provides a comprehensive API platform that supports various use cases including remittance, forex arbitrage, lending, insurance, savings, social protection, business financing, and gig work.
Our API documentation is built using OpenAPI specifications to provide you with:
* Interactive API playground
* Comprehensive endpoint documentation
* Code examples in multiple languages
* Real-time testing capabilities
View the complete OpenAPI specification
Fork the complete API collection with environments
## Test with Postman
Our comprehensive Postman collection includes all 57 API endpoints organized by functionality, plus pre-configured environments for sandbox and production testing.
**What's included:**
* Complete API collection with all endpoints
* Sandbox and production environment configurations
* Pre-configured authentication workflows
* Sample request bodies and response examples
* Environment variables for easy testing
**To get started:**
1. Click the "Run in Postman" button above to fork the collection
2. Import both the collection and environment to your Postman workspace
3. Set up your API credentials in the environment variables
4. Start testing endpoints immediately
The Postman collection automatically handles authentication token management
and includes examples for common workflows like deposits, payouts, and wallet
management.
## Base URLs
**Sandbox Environment:** `https://sandbox-api.kotanipay.io`\
**Production Environment:** `https://api.kotanipay.io`
## Authentication
All API endpoints require authentication using JWT Bearer tokens. You can obtain an access token by:
1. Creating an integrator account
2. Logging in to get a JWT token
3. Generating an API key for ongoing operations
```json theme={null}
"security": [
{
"JWT": []
}
]
```
## Getting Started
1. **Create an Integrator Account** - Register at [integrator.kotanipay.com/register](https://integrator.kotanipay.com/register)
2. **Login** - Authenticate to receive your JWT token
3. **Generate API Key** - Create an API key for secure transaction processing
4. **Start Integrating** - Begin making API calls to process transactions
## Support
Need help getting started? Contact our support team at [sales@kotanipay.com](mailto:sales@kotanipay.com) or visit our website at [https://kotanipay.com](https://kotanipay.com)
# Cancel Offramp Transaction
Source: https://documentation.kotanipay.com/v3/api-reference/offramp/cancel
GET /api/v3/offramp/cancel/{referenceId}
This endpoint will cancel the offramp transaction.
This endpoint will cancel the offramp transaction.
# Create Offramp Request
Source: https://documentation.kotanipay.com/v3/api-reference/offramp/create
POST /api/v3/offramp
This endpoint will create a offramp request for a customer. If the fiat transfer fails after successful crypto receipt, an automatic refund will be initiated after 5 minutes. Use the refund-status endpoint to check refund status.
This endpoint will create an offramp request for a customer to convert crypto to fiat.
# Get Offramp Status (by Reference ID)
Source: https://documentation.kotanipay.com/v3/api-reference/offramp/get-status
GET /api/v3/offramp/{referenceId}
This endpoint will return the status of the withdrawal request.
Get the status of an offramp transaction using the reference ID. Alternative endpoint to the main status endpoint.
# Get Offramp Transactions
Source: https://documentation.kotanipay.com/v3/api-reference/offramp/list-transactions
GET /api/v3/offramp/transactions
This endpoint will return all offramp transactions for the integrator.
Get all offramp transactions for your integrator account with pagination and filtering support.
# Get Refund Status
Source: https://documentation.kotanipay.com/v3/api-reference/offramp/refund-status
GET /api/v3/offramp/refund-status/{referenceId}
Get the status of an onchain refund. Refunds are automatically initiated 5 minutes after a failed transaction if the crypto was successfully received but fiat transfer failed.
Get the status of an automatic crypto refund for a failed offramp transaction.
# Retry Failed Refund
Source: https://documentation.kotanipay.com/v3/api-reference/offramp/retry-refund
POST /api/v3/offramp/retry-refund/{referenceId}
Retry a failed crypto refund for an offramp transaction. Gas/fund errors can be retried up to 10 times, other errors up to 4 times. Refunds marked for manual intervention cannot be retried through this endpoint.
Retry a failed crypto refund for an offramp transaction. Can be retried up to 3 times.
# Get Offramp Status
Source: https://documentation.kotanipay.com/v3/api-reference/offramp/status
GET /api/v3/offramp/{referenceId}
This endpoint will return the status of the withdrawal request.
This endpoint will return the status of the offramp withdrawal request.
# Create Onramp
Source: https://documentation.kotanipay.com/v3/api-reference/onramp/create
POST /api/v3/onramp
You can create an onramp request with either mobile money or bank checkout
You can create an onramp request with either mobile money or bank checkout to convert fiat to crypto.
# Send Token to Crypto Wallet
Source: https://documentation.kotanipay.com/v3/api-reference/onramp/crypto
POST /api/v3/onramp/crypto
Retry multiple failed crypto refunds by providing an array of reference IDs. Gas/fund errors can be retried up to 10 times, other errors up to 4 times. Refunds marked for manual intervention will be skipped.
This endpoint will send token to any crypto wallet address specified.
# Get Crypto Onramp Status
Source: https://documentation.kotanipay.com/v3/api-reference/onramp/crypto-status
GET /api/v3/onramp/crypto/{referenceId}
This endpoint will be used to get the status of the transaction
This endpoint will be used to get the status of the crypto onramp transaction.
# Get Crypto Onramp Status (by Reference ID)
Source: https://documentation.kotanipay.com/v3/api-reference/onramp/get-crypto-status
GET /api/v3/onramp/crypto/{referenceId}
This endpoint will be used to get the status of the transaction
Get the status of a crypto onramp transaction using the reference ID.
# Get Onramp Status (by Reference ID)
Source: https://documentation.kotanipay.com/v3/api-reference/onramp/get-status
GET /api/v3/onramp/{referenceId}
This endpoint will be used to get the status of the transaction
Get the status of an onramp transaction using the reference ID.
# Get Onramp Transactions
Source: https://documentation.kotanipay.com/v3/api-reference/onramp/list-transactions
GET /api/v3/onramp/transactions
This endpoint will return all onramp transactions for the integrator.
Get all onramp transactions for your integrator account with pagination and filtering support.
# Get Onramp Status
Source: https://documentation.kotanipay.com/v3/api-reference/onramp/status
GET /api/v3/onramp/{referenceId}
This endpoint will be used to get the status of the transaction
This endpoint will be used to get the status of the onramp transaction.
# Get Crypto Bridge Rate
Source: https://documentation.kotanipay.com/v3/api-reference/rates/crypto-bridge-rate
POST /api/v3/rate/crypto-bridge
Get a rate quote for crypto bridge transactions (fiat to crypto conversion). This endpoint generates a rate_id that must be used in the crypto bridge deposit request. Rate is valid for 60 seconds and can only be used once. Currently supports: KES -> Lightning (MSAT)
Get an exchange rate quote for a crypto bridge transaction before initiating the deposit. Use this to display the estimated source fiat amount, crypto amount, and destination fiat amount to the customer.
# Get Fiat to Fiat Exchange Rate
Source: https://documentation.kotanipay.com/v3/api-reference/rates/fiat-to-fiat
POST /api/v3/rate/fiat
An integrator can use this endpoint to get the exchange rate between two fiat currencies
An integrator can use this endpoint to get the exchange rate between two fiat currencies.
# Get All Public Exchange Rates
Source: https://documentation.kotanipay.com/v3/api-reference/rates/get-all-public-rates
GET /api/v3/public/rate
Public endpoint for getting all available exchange rates. Useful for dropdown displays or rate comparison.
Get all available exchange rates without authentication. Useful for dropdown displays or rate comparison.
# Get All Exchange Rates
Source: https://documentation.kotanipay.com/v3/api-reference/rates/get-all-rates
GET /api/v3/rate
An integrator can use this endpoint to get all the exchange rates available
An integrator can use this endpoint to get all the exchange rates available.
# Get Public Exchange Rate
Source: https://documentation.kotanipay.com/v3/api-reference/rates/get-public-rate
GET /api/v3/public/rate/{from}/{to}
Public endpoint for getting exchange rates between two currencies. Used for payment link checkout currency conversion display.
Get the exchange rate between two currencies without authentication. Used for payment link checkout currency conversion display.
# Get Exchange Rate
Source: https://documentation.kotanipay.com/v3/api-reference/rates/get-rate
GET /api/v3/rate/{from}/{to}
An integrator can use this endpoint to get the exchange rate between two currencies
An integrator can use this endpoint to get the exchange rate between two currencies.
# Get Offramp Exchange Rate
Source: https://documentation.kotanipay.com/v3/api-reference/rates/offramp-rate
POST /api/v3/rate/offramp
An integrator can use this endpoint to get the exchange rate between two currencies
An integrator can use this endpoint to get the exchange rate between two currencies for offramp transactions.
# Get Onramp Exchange Rate
Source: https://documentation.kotanipay.com/v3/api-reference/rates/onramp-rate
POST /api/v3/rate/onramp
An integrator can use this endpoint to get the exchange rate between two currencies
An integrator can use this endpoint to get the exchange rate between two currencies for onramp transactions.
# Cancel Settlement
Source: https://documentation.kotanipay.com/v3/api-reference/settlements/cancel
DELETE /api/v3/integrator/settlements/{settlementId}
Cancel a pending settlement request. Only settlements in `PENDING` status can be cancelled.
# Dashboard Settlements
Source: https://documentation.kotanipay.com/v3/api-reference/settlements/dashboard-settlements
API reference for settlement requests, batch settlements, merging, beneficiaries, and notification preferences
The settlements API lets you request, track, and manage the movement of your Kotani Pay balance to an external account. You can submit individual requests, group them into batches, merge existing requests, and configure saved beneficiaries for faster reuse.
All endpoints require a valid API key — see [API Keys](/v3/getting-started/api-keys).
***
## Schedule & Limits
Before submitting a settlement, check the active schedule to understand allowed days, cutoff times, and amount limits.
### GET /api/v3/integrator/settlements/schedule
See [Settlement Schedule](/v3/api-reference/settlements/schedule).
| Field | Type | Description |
| --------------- | --------- | --------------------------------------------------------------------------------- |
| `enabled` | boolean | Whether settlements are currently open for requests |
| `allowedDays` | number\[] | Days of the week accepted (0 = Sunday … 6 = Saturday) |
| `cutoffEnabled` | boolean | Whether a daily cutoff time is enforced |
| `cutoffTime` | string | Cutoff in `HH:MM` format — requests after this are queued to the next allowed day |
| `timezone` | string | IANA timezone for cutoff evaluation (e.g. `"Africa/Nairobi"`) |
| `minAmount` | number | Minimum settlement amount in USD |
| `maxAmount` | number | Maximum settlement amount in USD |
***
## Fee Preview
Preview the fee before submitting.
### GET /api/v3/integrator/settlements/fee-preview
See [Settlement Fee Preview](/v3/api-reference/settlements/fee-preview).
Query parameters: `amount` (required), `walletId` (required).
| Field | Type | Description |
| --------------- | ------ | ------------------------------------------ |
| `amount` | number | The queried amount in local currency |
| `fee` | number | Fee amount in local currency |
| `feePercentage` | number | Fee as a percentage of `amount` |
| `netAmount` | number | Amount received after fee (`amount - fee`) |
| `currency` | string | Local currency code |
| `usdRate` | number | Exchange rate used to convert to USD |
| `usdAmount` | number | `amount` in USD |
| `usdNetAmount` | number | `netAmount` in USD |
***
## Single Settlement Requests
### POST /api/v3/integrator/settlements
See [Request Settlement](/v3/api-reference/settlements/request-settlement).
| Field | Type | Required | Description |
| -------------------- | ------ | ----------- | ------------------------------------------------------------------ |
| `walletId` | string | Yes | Fiat wallet to settle from |
| `amount` | number | Yes | Amount in the wallet's local currency |
| `balanceSource` | string | Yes | `PAYOUT` or `DEPOSIT` |
| `beneficiaryDetails` | object | Conditional | Inline beneficiary — required if `savedBeneficiaryId` is omitted |
| `savedBeneficiaryId` | string | Conditional | Saved beneficiary ID — required if `beneficiaryDetails` is omitted |
| `integratorNote` | string | No | Internal memo |
| `callbackUrl` | string | No | URL to receive webhook on status change |
### GET /api/v3/integrator/settlements
See [List Settlements](/v3/api-reference/settlements/list-settlements). Query: `limit`, `offset`, `status`, `from`, `to`.
### GET /api/v3/integrator/settlements/:id
See [Get Settlement](/v3/api-reference/settlements/get-settlement).
### DELETE /api/v3/integrator/settlements/:id
See [Cancel Settlement](/v3/api-reference/settlements/cancel). Only `PENDING` settlements can be cancelled.
***
## Batch Settlements
Group multiple wallet settlements into one operation under a single admin approval.
### POST /api/v3/integrator/settlements/batch
See [Create Batch Settlement](/v3/api-reference/settlements/batch/create).
| Field | Type | Required | Description |
| ---------------- | --------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `requests` | object\[] | Yes | Array of settlement sub-requests. Each accepts `walletId`, `amount`, `balanceSource`, and optionally `beneficiaryDetails` or `savedBeneficiaryId` |
| `batchReference` | string | No | Your own idempotency reference for the batch |
| `integratorNote` | string | No | Internal memo for the batch |
### GET /api/v3/integrator/settlements/batch
See [List Settlement Batches](/v3/api-reference/settlements/batch/list).
### GET /api/v3/integrator/settlements/batch/:batchId
See [Get Settlement Batch](/v3/api-reference/settlements/batch/get).
### DELETE /api/v3/integrator/settlements/batch/:batchId
See [Cancel Settlement Batch](/v3/api-reference/settlements/batch/cancel). Cancels all children still in `PENDING`.
***
## Merge Into Batch
Convert two or more existing settlements into a batch after the fact.
### POST /api/v3/integrator/settlements/merge
See [Merge Settlements into Batch](/v3/api-reference/settlements/merge).
| Field | Type | Required | Description |
| ---------------- | --------- | -------- | ------------------------------------------------------------------------------------------- |
| `settlementIds` | string\[] | Yes | IDs to merge. Minimum 2. All must be `PENDING` or `UNDER_REVIEW` and not already in a batch |
| `integratorNote` | string | No | Internal memo for the resulting batch |
***
## Notification Preferences
### PATCH /api/v3/integrator/settlements/notification-preferences
See [Update Notification Preferences](/v3/api-reference/settlements/notification-preferences).
| Field | Type | Description |
| -------------------------------- | ------- | ------------------------------------------ |
| `settlementNotificationEmail` | string | Email address for settlement notifications |
| `settlementNotificationsEnabled` | boolean | Toggle email notifications |
| `settlementSlackWebhookUrl` | string | Slack incoming webhook URL |
| `settlementSlackEnabled` | boolean | Toggle Slack notifications |
***
## Saved Beneficiaries
Store destination account details once and reuse with `savedBeneficiaryId`.
### GET /api/v3/integrator/settlement-config/beneficiaries
See [List Beneficiaries](/v3/api-reference/settlements/settlement-config/beneficiaries).
### POST /api/v3/integrator/settlement-config/beneficiaries
See [Create Beneficiary](/v3/api-reference/settlements/settlement-config/create-beneficiary).
### PATCH /api/v3/integrator/settlement-config/beneficiaries/:id
See [Update Beneficiary](/v3/api-reference/settlements/settlement-config/get-beneficiary).
### DELETE /api/v3/integrator/settlement-config/beneficiaries/:id
See [Delete Beneficiary](/v3/api-reference/settlements/settlement-config/delete-beneficiary).
***
## Related
Lifecycle states, batch flow, webhooks, and schedule rules
How deposit and payout balances work
How to verify and handle signed webhook events
All status codes used across the API
# Get Settlement
Source: https://documentation.kotanipay.com/v3/api-reference/settlements/get-settlement
GET /api/v3/integrator/settlements/{settlementId}
Get a settlement request by ID.
# List Settlements
Source: https://documentation.kotanipay.com/v3/api-reference/settlements/list-settlements
GET /api/v3/integrator/settlements
Retrieve the authenticated integrator's settlement requests.
# Request Settlement
Source: https://documentation.kotanipay.com/v3/api-reference/settlements/request-settlement
POST /api/v3/integrator/settlements
Submit a settlement request for processing.
# Incoming SMS Webhook
Source: https://documentation.kotanipay.com/v3/api-reference/sms/incoming
POST /api/v3/sms/incoming
Webhook endpoint for AfricasTalking incoming SMS messages.
This endpoint is called directly by AfricasTalking when an inbound SMS is received on your registered shortcode. No authentication is required. KotaniPay saves the message and forwards it to all registered webhook clients for your account.
Configure this URL (`https://sandbox-api.kotanipay.io/api/v3/sms/incoming`) as the callback URL in your AfricasTalking dashboard under **SMS → Callback URL**.
# Register SMS Webhook
Source: https://documentation.kotanipay.com/v3/api-reference/sms/register-webhook
POST /api/v3/sms/clients
Register a webhook client to receive SMS notifications
Register a webhook URL to receive real-time notifications for incoming SMS messages and delivery reports. Only one webhook can be registered per integrator account. Returns a `409` if a webhook is already registered — use the update endpoint to change it.
# Send SMS
Source: https://documentation.kotanipay.com/v3/api-reference/sms/send
POST /api/v3/sms/send
Send a standard SMS message
Send a standard SMS message to a recipient phone number via AfricasTalking. The message is queued and dispatched immediately. A record of the sent message is stored against your integrator account.
# Health Check
Source: https://documentation.kotanipay.com/v3/api-reference/system/health
GET /health
This endpoint is used to check for the health of the application
Use this endpoint to check the health status of the Kotani Pay API platform.
# Payment Providers
Source: https://documentation.kotanipay.com/v3/api-reference/system/providers
POST /api/v3/providers
This Api Handles fetching of payment providers
Get the list of available payment providers and their supported currencies and countries.
# Create Crypto Wallet
Source: https://documentation.kotanipay.com/v3/api-reference/wallets/crypto/create
POST /api/v3/wallet/crypto
The crypto wallet will contain the specified chain and coins an integrator desires to hold and one can create as many wallets as needed.
Create a crypto wallet to hold specified blockchain assets. The crypto wallet will contain the specified chain and coins that an integrator desires to hold, and you can create as many wallets as needed.
# Get Crypto Wallet
Source: https://documentation.kotanipay.com/v3/api-reference/wallets/crypto/get
GET /api/v3/wallet/crypto/{id}
This endpoint will return the crypto wallet created by the integrator by passing the wallet id.
Retrieve details of a specific crypto wallet by its ID, including supported tokens and current balances.
# List Crypto Wallets
Source: https://documentation.kotanipay.com/v3/api-reference/wallets/crypto/list
GET /api/v3/wallet/crypto
This endpoint will return all the crypto wallets created by the integrator.
Get a list of all crypto wallets belonging to the authenticated integrator, including supported chains and balances.
# Create Fiat Wallet
Source: https://documentation.kotanipay.com/v3/api-reference/wallets/fiat/create
POST /api/v3/wallet/fiat
This endpoint will create a fiat wallet for the integrator.
Create a new fiat wallet for a specific currency. Each integrator can have multiple fiat wallets for different currencies.
# Get Fiat Wallet
Source: https://documentation.kotanipay.com/v3/api-reference/wallets/fiat/get
GET /api/v3/wallet/fiat/{id}
This endpoint will return the fiat wallet created by the integrator.
Retrieve details of a specific fiat wallet by its ID, including balance and transaction history.
# Get Wallet by Currency
Source: https://documentation.kotanipay.com/v3/api-reference/wallets/fiat/get-by-currency
GET /api/v3/wallet/fiat/currency/{currency}
This endpoint will return the fiat wallet created by the integrator. Optionally filter by country using the "country" query parameter.
Retrieve a fiat wallet by its currency code (e.g., KES, UGX, USD).
# List Fiat Wallets
Source: https://documentation.kotanipay.com/v3/api-reference/wallets/fiat/list
GET /api/v3/wallet/fiat
This endpoint will return all the fiat wallets created by the integrator.
Get a list of all fiat wallets belonging to the authenticated integrator, with their current balances and status.
# Transfer Deposit Balance
Source: https://documentation.kotanipay.com/v3/api-reference/wallets/fiat/transfer-deposit-balance
POST /api/v3/wallet/transfer/deposit-balance
This endpoint will transfer the deposit balance of the fiat wallet to the main balance.
Transfer the deposit balance of the fiat wallet to the main balance. This endpoint will transfer the deposit balance of the fiat wallet to the main balance.
# Update Fiat Wallet
Source: https://documentation.kotanipay.com/v3/api-reference/wallets/fiat/update
PATCH /api/v3/wallet/fiat/{id}
This endpoint will update the fiat wallet created by the integrator.
Update the configuration and settings of an existing fiat wallet.
# Bank Withdrawal Status
Source: https://documentation.kotanipay.com/v3/api-reference/withdrawals/bank-status
GET /api/v3/withdraw/v2/bank/status/{referenceId}
This Api Handles payout to bank, currently only supports SA Banks
This API handles checking the status of bank payouts, currently only supports SA Banks.
# Bank Withdrawal
Source: https://documentation.kotanipay.com/v3/api-reference/withdrawals/bank-v2
POST /api/v3/withdraw/v2/bank
This Api Handles payout to bank, currently only supports SA Banks
This API handles payout to bank, currently only supports SA Banks.
# Lipa na M-Pesa Withdrawal
Source: https://documentation.kotanipay.com/v3/api-reference/withdrawals/lipa-na-mpesa
POST /api/v3/withdraw/lipa-na-mpesa
Send money from integrator wallet to customer via Lipa na M-Pesa STK Push or to a business shortcode.
Send money from integrator wallet to customer via Lipa na M-Pesa STK Push or to a business shortcode.
# List Withdrawal Transactions
Source: https://documentation.kotanipay.com/v3/api-reference/withdrawals/list-transactions
GET /api/v3/withdraw/transactions
Returns a paginated list of the integrator's withdrawal transactions. Supports filtering by status, date range, telco ID, and currency.
Returns a paginated list of the integrator's withdrawal transactions. Supports filtering by status, date range, telco/MNO transaction ID, and currency.
# Mobile Money Withdrawal
Source: https://documentation.kotanipay.com/v3/api-reference/withdrawals/mobile-money
POST /api/v3/withdraw/mobile-money
This api will withdraw fiat from the integrator’s fiat wallet to the customer’s mobile money wallet.
This API will withdraw fiat from the integrator's fiat wallet to the customer's mobile money wallet.
# Mobile Money Withdrawal Status
Source: https://documentation.kotanipay.com/v3/api-reference/withdrawals/mobile-money-status
GET /api/v3/withdraw/status/{reference_id}
This endpoint will return the status of the withdrawal request.
This endpoint will return the status of the mobile money withdrawal request.
The `reference_id` path parameter accepts either the Kotani platform reference ID or the telco/MNO transaction ID — whichever is available.
# Paybill Withdrawal
Source: https://documentation.kotanipay.com/v3/api-reference/withdrawals/paybill
POST /api/v3/withdraw/paybill
Send money from integrator wallet to a Paybill number with account reference.
Send money from integrator wallet to a Paybill number with account reference.
# Supporting Banks
Source: https://documentation.kotanipay.com/v3/api-reference/withdrawals/supporting-banks
GET /api/v3/withdraw/v2/bank/supporting-banks/{currency}
This Api Handles fetching of supporting banks
This API handles fetching of supporting banks for bank withdrawals by currency.
# Development Guide
Source: https://documentation.kotanipay.com/v3/development
Environments, authentication, request structure, and error handling for the Kotani Pay API v3
## Environments
Always develop against sandbox first. Production and sandbox share the same API structure — only the base URL and credentials differ.
| Environment | Base URL |
| -------------- | ---------------------------------- |
| **Sandbox** | `https://sandbox-api.kotanipay.io` |
| **Production** | `https://api.kotanipay.io` |
Sandbox API keys do not work in production and vice versa. Generate environment-specific credentials for each.
***
## Authentication
Every request requires an `Authorization: Bearer` header. Both API keys and JWT tokens are accepted in the same header format.
**API keys** are the right choice for server-to-server integrations — they do not expire unless revoked. **JWT tokens** are short-lived (1 hour) and mainly used during the API key generation flow.
See [API Keys](/v3/getting-started/api-keys) for how to generate a key and the difference between integrator-level and user-bound keys.
***
## Request & Response Format
All endpoints accept and return JSON. Set `Content-Type: application/json` on requests with a body.
All responses follow a consistent envelope:
**Success:**
```json theme={null}
{
"success": true,
"message": "...",
"data": {}
}
```
**Error:**
```json theme={null}
{
"success": false,
"message": "Descriptive error message",
"error_code": 400,
"data": {}
}
```
See [Error Handling](/v3/essentials/error-handling) for the full status code reference and error payload details.
***
## Error Codes
| Status | Meaning |
| ------ | -------------------------------------------------------------- |
| `200` | Success |
| `400` | Bad request — check your request body and parameters |
| `401` | Unauthorized — API key or JWT is missing or invalid |
| `403` | Forbidden — key exists but lacks permission for this operation |
| `404` | Resource not found |
| `429` | Rate limit exceeded |
| `500` | Internal server error — contact support if persistent |
***
## Rate Limits
The API applies per-endpoint, per-key rate limits with three tiers:
| Tier | Applied to |
| ------------- | ---------------------------------------------- |
| **General** | Standard create/update operations |
| **Sensitive** | Authentication and security-critical endpoints |
| **Read-only** | GET/list endpoints |
When rate limited, the response includes a `retryAfter` value in seconds. Specific limits are configurable per integrator — contact your account manager if you need a higher limit for a high-volume integration.
***
## Idempotency
For deposits and withdrawals, always send a reference ID. The field name differs by flow:
* Deposits use `reference_id` (snake\_case)
* Withdrawals use `referenceId` (camelCase)
Reference IDs must be unique per integrator. If a request times out or fails at the network level, retrying with the same reference ID will not create a duplicate transaction.
***
## Webhooks
Use webhooks instead of polling. Configure your endpoint in the dashboard under **Settings**.
There are two delivery modes — which one you receive depends on whether a webhook secret is configured on your account. When a secret is configured, each request includes an `X-Kotani-Signature` header you must verify before processing. Without a secret, Kotani Pay posts directly to the `callbackUrl` on each transaction with no signature headers.
See [Webhooks](/v3/essentials/webhooks) for both delivery modes, all event types, payload schemas, and signature verification.
***
## Going to Production
1. Switch your base URL to `https://api.kotanipay.io`
2. Run the magic link login flow against production to get a production JWT
3. Generate a production API key using that JWT
4. Update your webhook endpoint to point to your production server
Sandbox credentials do not work against the production URL. Always generate environment-specific keys.
***
## Transaction Flows
Each flow walks through the full lifecycle — what to call, what the customer experiences, how the callback arrives, and how to poll for status.
Mobile money, bank checkout, and card collections
Mobile money and bank disbursements
Fiat → crypto via mobile money or bank
Crypto → fiat disbursement and refund handling
Hosted checkout pages with multiple payment methods
CSV-based mass disbursements with batch tracking
# Balances & Settlement
Source: https://documentation.kotanipay.com/v3/essentials/balances-and-settlement
How deposit and payout balances work, how to move funds between them, and how to settle your earnings
Kotani Pay tracks two separate fiat balances for each wallet currency. Understanding the difference between them — and how to move money between them — is essential before you start processing real transactions.
***
## Two Separate Balances
### Deposit Balance
Your **deposit balance** is credited every time a deposit transaction is successfully collected from a user. When a customer pays via mobile money, bank checkout, or card, those funds land in your deposit balance after reconciliation.
Think of it as: *money you've collected from your users.*
### Payout Balance
Your **payout balance** is what funds withdrawals (disbursements to users). When you initiate a withdrawal via the API, the amount is immediately debited from your payout balance. If your payout balance is zero, withdrawal requests will fail with insufficient funds.
Think of it as: *money available to send out.*
***
## Viewing Your Balances
In the dashboard, go to **Wallets** in the left sidebar. Each currency has its own wallet card showing both balances separately.
Use the **country/currency selector in the top right** to switch between currencies if you operate in multiple countries.
***
## Transferring from Deposit to Payout Balance
Before you can send money to users, you need funds in your **payout balance**. You can move funds from your deposit balance to your payout balance at any time.
### From the Dashboard
1. Go to **Wallets** in the sidebar
2. Find the wallet for the currency you want to transfer
3. Click the **Transfer Deposit Balance** button on that wallet card
4. Enter the amount to transfer and confirm
***
### Via the API
See the [Transfer Deposit Balance](/v3/api-reference/wallets/fiat/transfer-deposit-balance) endpoint in the API reference for the full request and response details.
The transfer is immediate — your payout balance increases and your deposit balance decreases by the same amount.
***
## Settlement
Settlement is the process of moving your collected funds out of Kotani Pay to your external account. Currently, **settlement is manual only** — there is no automated settlement schedule.
### How Settlement Works
Reach out through your designated communication channel (e.g., Slack) to request a settlement. The Kotani Pay team will send you a settlement form to fill out.
The form captures the details needed to process your settlement:
* The amount you want to settle
* The currency you want to receive
* Your destination account or wallet details
The Kotani Pay team reviews the submitted form. Once approved, the requested amount is deducted from your balance.
Before releasing the full amount, a small test transaction is sent to your destination. This confirms the destination details are correct.
After the test transaction is confirmed, the remaining funds are released and the settlement is marked complete.
***
## Common Questions
**Why did my withdrawal fail with insufficient funds?**
Withdrawals pull from your **payout balance**, not your deposit balance. If you have collected deposits but haven't transferred them to your payout balance, you'll see an insufficient funds error even though you have a deposit balance. Use the **Transfer Deposit Balance** flow to move funds across.
**Do I need to move funds to the payout balance before every withdrawal?**
You only need to ensure your payout balance covers the withdrawals you're processing. Many integrators fund the payout balance in bulk (e.g., top it up at the start of the day) rather than per transaction.
**What currency do I receive settlement in?**
Settlement currency and destination are agreed upon when you submit the settlement form. Speak to your account manager for the specific options available to you.
**How long does settlement take?**
After the test transaction is confirmed, the remaining funds are released promptly. Total time depends on review time and how quickly the test transaction confirms. Expect 1–3 business days end to end.
**Is there an API endpoint to request settlement?**
Settlement requests are currently handled manually — reach out through your communication channel to begin the process.
***
## Related
Transfer deposit balance via the API
How withdrawals debit from your payout balance
# Error Handling
Source: https://documentation.kotanipay.com/v3/essentials/error-handling
HTTP status codes and error response format for the Kotani Pay API
The Kotani Pay API uses standard HTTP status codes. `2xx` means success, `4xx` means the request was rejected, `5xx` means something failed on the server.
***
## Response Envelope
All responses — success and error — follow a consistent envelope.
**Success:**
```json theme={null}
{
"success": true,
"message": "Operation completed successfully",
"data": {}
}
```
**Error:**
```json theme={null}
{
"success": false,
"message": "Descriptive error message",
"error_code": 400,
"data": {}
}
```
The `error_code` field mirrors the HTTP status code. For validation failures, `data.errors` contains the individual field messages:
```json theme={null}
{
"success": false,
"message": "Validation failed: amount must be a positive number, customerKey should not be empty",
"error_code": 400,
"data": {
"errors": [
"amount must be a positive number",
"customerKey should not be empty"
]
}
}
```
***
## Status Codes
| Status | Meaning |
| ------ | -------------------------------------------------------------------------------- |
| `200` | Success |
| `400` | Bad request — invalid parameters or missing required fields |
| `401` | Unauthorized — API key or JWT is missing or invalid |
| `403` | Forbidden — key is valid but lacks permission, or the resource is not accessible |
| `404` | Resource not found |
| `429` | Rate limit exceeded |
| `500` | Internal server error — contact support if it persists |
***
## Rate Limit Errors
When rate limited, the `data` field includes how long to wait:
```json theme={null}
{
"success": false,
"message": "Too many requests. You have exceeded the limit of 60 requests per 60 seconds. Please wait before trying again.",
"error_code": 429,
"data": {
"retryAfter": 60
}
}
```
***
## Customer Suspension
If a `customer_key` belongs to a suspended customer, the request returns `403`:
```json theme={null}
{
"success": false,
"message": "Customer cust_abc123 is temporarily suspended until 2025-01-01 12:00:00 UTC and cannot initiate transactions. Please contact support to resolve the customer's status.",
"error_code": 403,
"data": {
"bannedUntil": "2025-01-01T12:00:00Z"
}
}
```
# Offramp Refunds
Source: https://documentation.kotanipay.com/v3/essentials/offramp-refunds
How Kotani Pay returns crypto to integrators when a fiat disbursement fails after on-chain receipt.
When an offramp transaction's fiat disbursement fails after Kotani has already received the on-chain crypto, Kotani initiates an automatic refund back to the sender's address (or to a Lightning invoice for Lightning offramps).
## How Refunds Work
1. **On-chain settlement confirmed** — Kotani receives the crypto from the sender.
2. **Fiat disbursement fails** — Mobile money, bank, or paybill transfer fails or is cancelled.
3. **Refund triggered automatically** — Kotani attempts to return the crypto to the integrator.
4. **Webhook fired** — `refund.completed` or `refund.failed` is sent to your webhook endpoint.
For non-Lightning chains (EVM, Solana, Stellar, Cardano, Tron), Kotani sends the crypto directly back to `senderAddress`. No action is needed from you.
For **Lightning**, Kotani cannot push funds unprompted — it needs a bolt11 invoice to pay. See below.
***
## Lightning Refunds
### Option A — Provide a refund invoice at create time (recommended)
Include `refund_config` when creating the offramp. If fiat fails, Kotani pays the invoice immediately — no manual intervention needed.
```json theme={null}
{
"cryptoAmount": 0.015,
"currency": "KES",
"chain": "LIGHTNING",
"token": "MSAT",
"referenceId": "TXN-001",
"mobileMoneyReceiver": { ... },
"refund_config": {
"bolt11": "lnbc1500n1p0xyz...",
"payment_hash": "a1b2c3d4e5f6...",
"amount_msat": 1500000,
"expires_at": "2024-11-22T13:00:00Z",
"generate_invoice_url": "https://api.yourapp.com/lightning/generate-refund-invoice"
}
}
```
| Field | Required | Notes |
| ---------------------- | ----------- | ---------------------------------------------------------------------------- |
| `bolt11` | Yes | Valid, unpaid Lightning invoice |
| `payment_hash` | Recommended | Used to detect if invoice was already paid |
| `amount_msat` | Yes | Must match the expected refund amount exactly |
| `expires_at` | Yes | ISO 8601 — Kotani uses this to decide whether to call `generate_invoice_url` |
| `generate_invoice_url` | Recommended | Called when `bolt11` is expired; see contract below |
> **Important:** Lightning invoices expire. Always provide `generate_invoice_url` so Kotani can fetch a fresh one if the refund is triggered after the invoice expires. Without it, and with an expired invoice, Kotani falls back to Option B.
### Option B — Submit invoice manually when notified
If no `refund_config` was provided (or `bolt11` is expired and no `generate_invoice_url` exists), Kotani sends a `refund.lightning.invoice_needed` webhook and an email asking you to submit an invoice via:
```
POST /api/v3/offramp/submit-refund-invoice/{referenceId}
Body: { "invoice": "lnbc..." }
```
The invoice must be for exactly the refund amount in sats, must not be expired, and must not already be paid.
***
## `generate_invoice_url` Contract
Kotani calls your URL with a `POST` request when it needs a fresh Lightning invoice:
### Request from Kotani
```json theme={null}
{
"amount_msat": 1500000,
"reference_id": "TXN-001"
}
```
### Your expected response
```json theme={null}
{
"bolt11": "lnbc1500n1p0abc...",
"payment_hash": "f6e5d4c3b2a1...",
"amount_msat": 1500000,
"expires_at": "2024-11-22T14:30:00Z"
}
```
| Field | Required | Notes |
| -------------- | ----------- | ------------------------------------------------ |
| `bolt11` | Yes | Fresh, unpaid bolt11 invoice |
| `payment_hash` | Recommended | For Kotani to track payment |
| `amount_msat` | Yes | Must match the requested `amount_msat` |
| `expires_at` | Recommended | ISO 8601 — so Kotani knows when this one expires |
> **Your endpoint must respond within 10 seconds.** If it times out or returns an error, Kotani falls back to the manual notification flow.
***
## Refund Webhooks
### `refund.completed`
Fired when Kotani successfully refunds crypto to the integrator.
```json theme={null}
{
"event": "refund.completed",
"data": {
"referenceId": "TXN-001",
"status": "REVERSED",
"refundStatus": "SUCCESSFUL",
"refundTransactionHash": "0xabc123...",
"refundAmount": 0.015,
"chain": "LIGHTNING",
"token": "MSAT",
"currency": "KES",
"timestamp": "2024-11-22T13:05:00Z"
},
"signature": "sha256=..."
}
```
The parent transaction's `status` is set to `REVERSED` once refunded.
### `refund.failed`
Fired when refund attempts are exhausted (after up to 5 retries for general errors, 10 for gas/fund errors).
```json theme={null}
{
"event": "refund.failed",
"data": {
"referenceId": "TXN-001",
"refundStatus": "FAILED",
"refundAmount": 0.015,
"chain": "LIGHTNING",
"token": "MSAT",
"currency": "KES",
"error": "Invoice has expired. Please provide a new valid invoice.",
"totalRetries": 5,
"timestamp": "2024-11-22T13:10:00Z"
},
"signature": "sha256=..."
}
```
Contact [support@kotanipay.com](mailto:support@kotanipay.com) with the `referenceId` if you receive this — manual intervention will be required.
### `refund.lightning.invoice_needed`
Fired when Kotani needs you to submit a Lightning invoice manually (Option B). Includes the exact amount and the submit URL.
```json theme={null}
{
"event": "refund.lightning.invoice_needed",
"data": {
"referenceId": "TXN-001",
"refundAmount": 1500000,
"refundAmountSats": 1500,
"chain": "LIGHTNING",
"requiresAction": true,
"action": {
"type": "SUBMIT_LIGHTNING_INVOICE",
"submitUrl": "https://api.kotanipay.io/api/v3/offramp/submit-refund-invoice/TXN-001",
"method": "POST",
"body": { "invoice": "lnbc..." },
"invoiceRequirements": {
"amount": 1500,
"currency": "SATS",
"format": "bolt11",
"mustNotExpire": true,
"mustNotBePaid": true,
"mustMatchExactAmount": true
}
},
"timestamp": "2024-11-22T13:00:00Z"
},
"signature": "sha256=..."
}
```
***
## Retry Logic
| Scenario | Max Retries | Behaviour |
| ------------------------ | ----------- | -------------------------------------------------------------------------- |
| Gas / insufficient funds | 10 | Auto-retried on next cron cycle |
| Timeout | 5 | Auto-retried on next cron cycle |
| All other errors | 5 | Auto-retried; `refund.failed` fired when exhausted |
| Lightning — no invoice | ∞ | Waits until invoice submitted; `refund.lightning.invoice_needed` sent once |
Use `POST /api/v3/offramp/retry-refund/{referenceId}` to manually trigger a retry on a `FAILED` refund.
# Rate Limits
Source: https://documentation.kotanipay.com/v3/essentials/rate-limits
How the Kotani Pay API rate limits requests and what to expect when you exceed them
The Kotani Pay API uses a Redis-backed sliding window rate limiter applied per endpoint, per API key. Limits are not fixed globally — they vary by endpoint sensitivity and are configurable per integrator.
***
## Limit Tiers
Endpoints are grouped into three tiers:
| Tier | Applied to |
| ------------- | ---------------------------------------------- |
| **General** | Standard create and update operations |
| **Sensitive** | Authentication and security-critical endpoints |
| **Read-only** | GET and list endpoints |
Each tier has its own request limit and window duration. A separate burst limit applies across all tiers, catching rapid-fire requests within any 5-second window regardless of the per-minute count.
***
## When You're Rate Limited
When you exceed a limit, the API returns `429 Too Many Requests`:
```json theme={null}
{
"statusCode": 429,
"message": "Too many requests. You have exceeded the limit of N requests per X seconds. Please wait before trying again.",
"error": "Too Many Requests",
"data": {
"retryAfter": 60
}
}
```
The `data.retryAfter` value is in seconds. Wait at least that long before retrying.
***
## Integrator Exemptions
High-volume integrators can be placed on an exempt list that bypasses rate limiting entirely. Contact your account manager if your use case requires this.
***
## Best Practices
* Use webhooks instead of polling for transaction status — polling is the most common cause of hitting rate limits
* When checking many records, use list endpoints with pagination rather than looping individual lookups
* When you receive a `429`, back off and retry using the `retryAfter` value from the response
# Request Signing
Source: https://documentation.kotanipay.com/v3/essentials/request-signing
Protect outbound API requests with HMAC-SHA256 signatures when secure mode is enabled on your account.
When request signing (`isSecure` mode) is enabled on your integrator account, every request you send to the Kotani Pay API must include three additional headers carrying an HMAC-SHA256 signature. The server rejects any request that is missing these headers, uses a stale timestamp, or replays a nonce it has already seen.
Request signing governs requests **from your server to Kotani Pay**. It is separate from webhook signature verification, which covers the opposite direction — notifications Kotani Pay sends to your server. See [Webhook Notifications](/v3/essentials/webhooks) for that.
## Required Headers
Include all three headers on every request.
| Header | Description |
| ------------- | ---------------------------------------------------------------------------------------- |
| `x-timestamp` | Unix timestamp in seconds (e.g. `1715123456`). Must be within ±5 minutes of server time. |
| `x-nonce` | UUID v4. Must be unique per request — each value is accepted exactly once. |
| `x-signature` | HMAC-SHA256 of the signing payload using your API secret, hex-encoded. |
## Signing Payload
Construct the payload string before computing the signature.
**POST / PUT / PATCH**
```
${timestamp}.${nonce}.${JSON.stringify(requestBody)}
```
**GET**
```
${timestamp}.${nonce}.${lastPathSegment}
```
where `lastPathSegment` is the final segment of the request URL path. For example, a request to `/api/v3/wallets/fiat/64a1b2c3d4e5f6a7b8c9d0e2` uses `64a1b2c3d4e5f6a7b8c9d0e2`.
Serialize the request body with **no extra spaces** — compact JSON only. Any whitespace difference between your serialisation and the server's will cause a signature mismatch and a `401 Unauthorized` response.
### Example signature value
```
8b5a21c2e33f3cf33a2e9c92d585cb5a1c304b6f63f91d2f6b0a428cbe9854d1
```
## Postman Pre-request Script
Paste this into the **Pre-request Script** tab of your Postman collection or individual request. It reads your secret from a Postman environment variable and automatically injects the three signing headers before each request.
```javascript theme={null}
// Kotani Pay — Request Signing Pre-request Script
// Add KOTANI_PAY_SIGNATURE to your Postman environment before running.
const secret = pm.environment.get("KOTANI_PAY_SIGNATURE");
if (!secret) {
console.warn("[KotaniPay] KOTANI_PAY_SIGNATURE is not set in the active environment.");
}
const timestamp = Math.floor(Date.now() / 1000).toString();
// UUID v4 — Postman sandbox does not expose a uuid library
const nonce = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
return (c === "x" ? r : (r & 0x3) | 0x8).toString(16);
});
const method = pm.request.method;
let payload;
if (method === "GET") {
const parts = pm.request.url.getPath().split("/");
const lastSegment = parts[parts.length - 1] || "";
payload = `${timestamp}.${nonce}.${lastSegment}`;
} else {
const body = pm.request.body?.raw || "{}";
payload = `${timestamp}.${nonce}.${body}`;
}
// CryptoJS is available globally in the Postman sandbox
const signature = CryptoJS.HmacSHA256(payload, secret).toString();
pm.request.headers.add({ key: "x-timestamp", value: timestamp });
pm.request.headers.add({ key: "x-nonce", value: nonce });
pm.request.headers.add({ key: "x-signature", value: signature });
console.log("[KotaniPay] Signed:", { timestamp, nonce, payload });
```
**Setup steps:**
1. Open your collection → **Variables** tab (or **Environments**) and add `KOTANI_PAY_SIGNATURE` with your secret value.
2. Paste the script into the collection's **Pre-request Script** tab so it runs for every request automatically.
3. Set your request body as **raw → JSON**. The script reads `pm.request.body.raw` directly, so the body must already be the final JSON string before the script runs.
Postman uses **CryptoJS** (not Node's `crypto` module) — `CryptoJS` is available globally with no `require()` needed. Do not use `crypto.createHmac` here; it will throw a reference error.
## Code Snippets
### Node.js
```typescript theme={null}
import crypto from "crypto";
import { v4 as uuidv4 } from "uuid";
function signRequest({
secret,
body,
method,
}: {
secret: string;
body: Record | string;
method: string;
}) {
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonce = uuidv4();
const payload =
method === "GET"
? `${timestamp}.${nonce}.${body}` // body = last path segment for GETs
: `${timestamp}.${nonce}.${JSON.stringify(body)}`;
const signature = crypto
.createHmac("sha256", secret)
.update(payload, "utf8")
.digest("hex");
return {
"x-timestamp": timestamp,
"x-nonce": nonce,
"x-signature": signature,
};
}
```
Usage:
```typescript theme={null}
const headers = signRequest({
secret: process.env.KOTANI_PAY_SIGNATURE!,
body: { wallet_id: "64a1b2c3d4e5f6a7b8c9d0e2", amount: 1000 },
method: "POST",
});
await fetch("https://api.kotanipay.io/api/v3/deposits/mobile-money", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.KOTANI_API_KEY}`,
"Content-Type": "application/json",
...headers,
},
body: JSON.stringify({ wallet_id: "64a1b2c3d4e5f6a7b8c9d0e2", amount: 1000 }),
});
```
### Python
```python theme={null}
import hmac, hashlib, time, uuid, json
def sign_request(secret: str, body: dict | str, method: str) -> dict:
timestamp = str(int(time.time()))
nonce = str(uuid.uuid4())
if method.upper() == "GET":
payload = f"{timestamp}.{nonce}.{body}"
else:
payload = f"{timestamp}.{nonce}.{json.dumps(body, separators=(',', ':'))}"
signature = hmac.new(
secret.encode(), payload.encode(), hashlib.sha256
).hexdigest()
return {"x-timestamp": timestamp, "x-nonce": nonce, "x-signature": signature}
```
## Security Notes
* **The nonce is single-use.** Reusing a nonce that the server has already accepted returns `401 Unauthorized`. Generate a fresh UUID v4 for every request.
* **The timestamp window is ±5 minutes.** A request whose `x-timestamp` falls outside that window returns `401 Unauthorized`. Ensure your server clock is NTP-synchronised.
* **Compact JSON serialisation is required.** Use `JSON.stringify` without formatting options in Node.js, and `json.dumps(..., separators=(',', ':'))` in Python. Extra whitespace breaks the signature.
* **Your API secret is shown once.** The secret is displayed at key generation time and cannot be retrieved afterwards. Store it in a secrets manager or environment variable — never in source code.
* **Secure mode is permanent.** Once request signing is activated for your account it cannot be turned off. Test your signing implementation in the sandbox environment before going live.
## Getting Your API Secret
Your API secret is generated alongside your API key in the dashboard.
1. Log in → **Settings** → **API Keys**
2. Click **Generate Key** and choose **Secure** mode
3. Copy both the API key and the API secret immediately — the secret is shown only once
If you have an existing API key without a secure secret, generate a new key with secure mode enabled. See [API Keys](/v3/getting-started/api-keys) for full key management instructions.
# Transaction Statuses
Source: https://documentation.kotanipay.com/v3/essentials/transaction-statuses
Understand all transaction status codes used across Kotani Pay API endpoints
## Overview
Kotani Pay uses a comprehensive set of status codes to track the lifecycle of different transaction types. Understanding these statuses is crucial for building robust integrations and handling transaction states correctly.
## Status Categories
### Terminal Statuses
Terminal statuses indicate that a transaction has reached its final state and will not change further. **You should stop polling for updates once a transaction reaches a terminal status.**
Terminal statuses include:
* `SUCCESSFUL` / `SUCCESS`
* `FAILED`
* `CANCELLED`
* `EXPIRED`
* `DECLINED`
* `PERMANENTLY_FAILED`
* `REVERSED`
### Intermediate Statuses
Intermediate statuses indicate that a transaction is still being processed. **You should continue polling or wait for webhooks when a transaction is in an intermediate status.**
Intermediate statuses include:
* `PENDING`
* `INITIATED`
* `IN_PROGRESS`
* `PROCESSING`
* `RETRY`
### Special Statuses
Special statuses are used for specific scenarios:
* `DUPLICATE` - Transaction is a duplicate of an existing one
* `ERROR_OCCURRED` - An error occurred during processing
* `REQUIRE_REVIEW` - Transaction requires manual review
* `NOT_INITIATED` - Transaction has not been initiated yet
***
## Deposit Transaction Statuses
Used for mobile money deposits, bank deposits, and card deposits.
Initial status. Payment request has been created but not yet sent to the provider.
Payment request has been sent to the provider and is awaiting customer action.
Customer has initiated payment and it's being processed by the provider.
**Terminal Status** - Payment completed successfully. Funds have been credited to your fiat wallet.
**Terminal Status** - Alternative success status used by some providers.
**Terminal Status** - Payment failed. Common reasons include insufficient funds, network errors, or provider issues.
**Terminal Status** - Payment request expired before customer completed payment (typically 30 minutes timeout).
**Terminal Status** - Payment was cancelled by customer or system.
**Terminal Status** - Payment was declined by the provider or payment network.
**Terminal Status** - Payment was reversed/refunded.
Duplicate transaction detected (same reference ID already exists).
An unexpected error occurred during processing.
Transaction flagged for manual review.
Transaction is being retried after a temporary failure.
**Terminal Status** - Transaction failed permanently after all retry attempts exhausted.
***
## Withdrawal Transaction Statuses
Used for mobile money withdrawals and bank withdrawals.
Withdrawal request created but not yet sent to provider.
Withdrawal request queued for processing.
Withdrawal request sent to provider.
Provider has initiated the withdrawal transaction.
Withdrawal is being processed by the provider.
**Terminal Status** - Withdrawal completed successfully. Funds have been sent to the recipient.
**Terminal Status** - Withdrawal failed.
**Terminal Status** - Withdrawal was cancelled.
**Terminal Status** - Withdrawal was declined by provider.
**Terminal Status** - Withdrawal request expired.
**Terminal Status** - Withdrawal was reversed.
An error occurred during withdrawal processing.
Withdrawal requires manual review.
Withdrawal is being retried.
Provider is retrying the withdrawal transaction.
**Terminal Status** - Retry attempt was successful.
**Terminal Status** - Retry attempt failed.
***
## Onramp Transaction Statuses
Onramp transactions use two separate status fields:
### `depositStatus`
Tracks the fiat payment status (mobile money/bank deposit). Uses the same statuses as [Deposit Transaction Statuses](#deposit-transaction-statuses).
### `onchainStatus`
Tracks the crypto transfer status (sending crypto to external wallet).
Crypto transfer is pending. Waiting for deposit to complete first.
Crypto transfer is being processed on the blockchain.
**Terminal Status** - Crypto successfully sent to recipient address. Transaction hash available.
**Terminal Status** - Crypto transfer failed.
**Terminal Status** - Crypto transfer cancelled (usually because deposit failed).
***
## Payment Link Transaction Statuses
Used for payment link transactions.
Payment link created but no payment attempted yet.
Customer is completing the payment.
**Terminal Status** - Payment completed successfully.
**Terminal Status** - Payment failed.
**Terminal Status** - Payment cancelled by customer or expired.
**Terminal Status** - Payment link expired before payment was completed.
***
## Callback Statuses
Internal statuses tracking callback delivery to your webhook URL.
Callback delivered successfully to your webhook.
Callback waiting to be sent.
Callback is being sent.
Callback delivery failed (will retry).
Callback delivery permanently failed after all retries.
***
## Best Practices
Stop polling when a transaction reaches any **terminal status**:
* `SUCCESSFUL` / `SUCCESS`
* `FAILED`
* `CANCELLED`
* `EXPIRED`
* `DECLINED`
* `PERMANENTLY_FAILED`
* `REVERSED`
Different failure statuses indicate different scenarios:
* `FAILED` - Temporary failure, user can retry creating a new transaction
* `DECLINED` - Provider declined the transaction (insufficient funds, limits, etc.)
* `EXPIRED` - Timeout, user can create a new transaction
* `CANCELLED` - User or system cancelled, no action needed
* `PERMANENTLY_FAILED` - Unrecoverable failure, investigate the error
**Recommended**: Use webhooks for real-time status updates instead of polling.
Configure webhook URLs in your integration to receive automatic notifications when transaction statuses change.
See [Webhooks documentation](/v3/essentials/webhooks) for setup instructions.
For onramp transactions, monitor BOTH statuses:
1. **`depositStatus`** - Tracks if customer paid (mobile money/bank)
2. **`onchainStatus`** - Tracks if crypto was sent
Success criteria: **BOTH** must be `SUCCESSFUL`
Common flow:
```
depositStatus: PENDING → INITIATED → SUCCESSFUL
onchainStatus: PENDING → IN_PROGRESS → SUCCESSFUL
```
When you receive `ERROR_OCCURRED` or `REQUIRE_REVIEW`:
1. Check the `error` or `errorMessage` field in the response for details
2. Log the full transaction details for support
3. Contact support if the issue persists
4. Do not retry automatically - these statuses often require manual intervention
***
## Common Status Flows
### Successful Deposit Flow
```
PENDING → INITIATED → IN_PROGRESS → SUCCESSFUL
```
### Failed Deposit Flow
```
PENDING → INITIATED → FAILED
```
### Expired Deposit Flow
```
PENDING → INITIATED → EXPIRED
```
### Successful Withdrawal Flow
```
NOT_INITIATED → PENDING → INITIATED → IN_PROGRESS → SUCCESSFUL
```
### Successful Onramp Flow
```
Deposit: PENDING → INITIATED → SUCCESSFUL
Onchain: PENDING → IN_PROGRESS → SUCCESSFUL
```
### Failed Onramp Flow (Deposit Fails)
```
Deposit: PENDING → INITIATED → FAILED
Onchain: PENDING → CANCELLED
```
### Failed Onramp Flow (Crypto Transfer Fails)
```
Deposit: PENDING → INITIATED → SUCCESSFUL
Onchain: PENDING → IN_PROGRESS → FAILED
```
**Note**: If deposit succeeds but crypto transfer fails, your fiat wallet is still credited. The crypto transfer will be automatically retried or can be manually retried via support.
# Webhook Notifications
Source: https://documentation.kotanipay.com/v3/essentials/webhooks
Receive real-time notifications about transaction status updates and other events.
Kotani Pay pushes notifications to your server when key events happen — transaction status changes, refund outcomes, KYC updates, and system notices.
***
## Two Delivery Modes
How notifications are delivered depends on whether a webhook secret is configured on your account.
### Signed webhooks
When a webhook secret is configured, all events are delivered through the signed system. Each `POST` request includes:
| Header | Value |
| --------------------- | ---------------------------------------------------------- |
| `X-Kotani-Signature` | `sha256=` — use this to verify authenticity |
| `X-Kotani-Event` | The event name (e.g. `transaction.deposit.status.updated`) |
| `X-Kotani-Integrator` | Your integrator ID |
| `Content-Type` | `application/json` |
The body is always wrapped in this envelope:
```json theme={null}
{
"event": "transaction.deposit.status.updated",
"data": { ... },
"signature": "sha256=a1b2c3d4e5f6..."
}
```
The `signature` field in the body mirrors `X-Kotani-Signature` — the **header is the source of truth** for verification.
### Direct callbacks
If no webhook secret is configured, Kotani Pay posts directly to the `callbackUrl` set on each transaction at the time it was created. These requests:
* Are sent as `POST` with a JSON body
* Do **not** include `X-Kotani-Signature`, `X-Kotani-Event`, or `X-Kotani-Integrator` headers
* Contain the transaction fields **directly** in the body — no `event` or `signature` wrapper
Configure a webhook secret in **Settings** to switch to signed webhooks.
***
## Supported Events
| Event | When it fires |
| --------------------------------------- | -------------------------------------------------------------- |
| `transaction.deposit.status.updated` | A deposit changes status |
| `transaction.withdrawal.status.updated` | A withdrawal changes status |
| `transaction.onramp.status.updated` | An onramp (fiat→crypto) changes status |
| `transaction.offramp.status.updated` | An offramp (crypto→fiat) changes status |
| `kyc.status.changed` | A customer's KYC verification outcome changes |
| `refund.completed` | Crypto refund successfully sent back to the sender |
| `refund.failed` | Refund exhausted all retry attempts |
| `refund.lightning.invoice_needed` | Lightning offramp needs a bolt11 invoice to process the refund |
| `settlement.approved` | A settlement request was approved |
| `settlement.processed` | A settlement was completed and funds disbursed |
| `settlement.rejected` | A settlement request was rejected |
| `settlement.paused` | A settlement was paused pending review |
| `settlement.batch.approved` | A settlement batch was approved |
| `settlement.batch.processed` | A settlement batch completed (fully or partially) |
| `settlement.batch.rejected` | A settlement batch was rejected |
| `settlement.batch.cancelled` | A settlement batch was cancelled |
| `system.event` | Operational notices and maintenance alerts |
| `transaction.status.updated` | *(Deprecated)* Use the specific events above |
Settlement events are opt-in. Subscribe to them in **Settings → Webhooks**.
***
## Verifying Signatures
Always verify the `X-Kotani-Signature` header before processing any event.
1. Parse the JSON body.
2. **Remove the `signature` field** from the parsed object.
3. Compute `sha256=HMAC-SHA256(secret, JSON.stringify({event, data}))`.
4. Compare with `X-Kotani-Signature` using a timing-safe comparison.
```typescript theme={null}
import crypto from 'crypto';
function verifyWebhook({
secret,
payload,
headerSignature,
}: {
secret: string;
payload: { event: string; data: Record; signature?: string };
headerSignature: string;
}): boolean {
const { signature, ...payloadWithoutSignature } = payload;
const computed =
'sha256=' +
crypto
.createHmac('sha256', secret)
.update(JSON.stringify(payloadWithoutSignature))
.digest('hex');
try {
return crypto.timingSafeEqual(
Buffer.from(computed),
Buffer.from(headerSignature.trim()),
);
} catch {
return false;
}
}
```
```typescript theme={null}
import express from 'express';
const app = express();
app.post('/webhook', express.json(), (req, res) => {
const isValid = verifyWebhook({
secret: process.env.KOTANI_WEBHOOK_SECRET!,
payload: req.body,
headerSignature: req.headers['x-kotani-signature'] as string,
});
if (!isValid) return res.status(401).send('Invalid signature');
const { event, data } = req.body;
// handle event...
res.status(200).send('OK');
});
```
***
## Event Payloads
**Casing conventions:** Deposit fields use `snake_case` (`reference_id`, `wallet_id`, `customer_key`). Withdrawal, onramp, and offramp fields use `camelCase` (`referenceId`, `walletId`, `customerKey`). Handle both in your webhook handler.
***
### `transaction.deposit.status.updated`
Fired whenever a deposit changes status — including intermediate states like `INITIATED` and `IN_PROGRESS` as well as terminal states (`SUCCESSFUL`, `FAILED`, `CANCELLED`).
#### Payload fields
Current deposit status. See [Transaction Statuses](/v3/essentials/transaction-statuses).
Your reference ID (or system-generated if not provided).
Auto-generated sequential reference number.
Kotani internal record ID.
Amount the customer was asked to pay.
ID of the integrator fiat wallet credited.
Amount actually credited to your wallet after fees.
Processing fee charged.
The customer identifier supplied at deposit creation.
Callback URL set on the transaction.
ISO 8601 creation timestamp.
Mobile money receipt code from the network (e.g. Mpesa confirmation code like `OEI2AK4D9X`). Present on successful mobile money deposits.
Provider-level confirmation reference. May differ from `telco_id` for some providers.
Bank name for bank-based deposits (e.g. `Capitec`, `FNB`). Only present for bank deposits.
Bank code for bank-based deposits. Only present for bank deposits.
Card payment brand for card deposits (e.g. `VISA`, `MASTERCARD`). Only present for card deposits.
Human-readable failure reason. Always present (may be empty string) for non-successful statuses.
Detailed provider error description. Always present (may be empty string) for non-successful statuses.
Provider error code. Always present (may be empty string) for non-successful statuses.
Raw internal error from the processing pipeline. Always present (may be empty string) for non-successful statuses.
#### Example — successful mobile money deposit
```json theme={null}
{
"event": "transaction.deposit.status.updated",
"data": {
"status": "SUCCESSFUL",
"reference_id": "order-abc-001",
"reference_number": 1001,
"id": "64a1b2c3d4e5f6a7b8c9d0e1",
"amount": 1000,
"wallet_id": "64a1b2c3d4e5f6a7b8c9d0e2",
"callback_url": "https://your-server.com/webhook",
"created_at": "2025-01-01T00:00:00.000Z",
"transaction_amount": 975,
"transaction_cost": 25,
"customer_key": "cust_abc123",
"telco_id": "OEI2AK4D9X"
},
"signature": "sha256=a1b2c3d4e5f6..."
}
```
#### Example — failed deposit
```json theme={null}
{
"event": "transaction.deposit.status.updated",
"data": {
"status": "FAILED",
"reference_id": "order-abc-002",
"reference_number": 1002,
"id": "64a1b2c3d4e5f6a7b8c9d0e3",
"amount": 500,
"wallet_id": "64a1b2c3d4e5f6a7b8c9d0e2",
"created_at": "2025-01-01T00:00:00.000Z",
"transaction_amount": 0,
"transaction_cost": 0,
"customer_key": "cust_abc123",
"error_message": "Insufficient funds",
"error_description": "The customer does not have enough funds",
"error_code": "INSUFFICIENT_FUNDS",
"transactionError": ""
},
"signature": "sha256=a1b2c3d4e5f6..."
}
```
#### Example — bank deposit (additional fields)
```json theme={null}
{
"event": "transaction.deposit.status.updated",
"data": {
"status": "SUCCESSFUL",
"reference_id": "order-bank-001",
"amount": 2000,
"transaction_amount": 1960,
"transaction_cost": 40,
"customer_key": "cust_za_001",
"bank_name": "Capitec",
"bank_code": "470010",
"payment_brand": "VISA"
},
"signature": "sha256=a1b2c3d4e5f6..."
}
```
***
### `transaction.withdrawal.status.updated`
Fired whenever a withdrawal changes status.
#### Payload fields
Current withdrawal status.
Your reference ID.
Auto-generated sequential reference number.
Kotani internal record ID.
Amount requested for withdrawal.
ID of the integrator fiat wallet debited.
Amount debited from your wallet including fees.
Processing fee charged.
The customer identifier supplied at withdrawal creation.
Callback URL set on the transaction.
ISO 8601 creation timestamp.
Mobile money receipt code from the network. Present on successful mobile money payouts.
Provider-level confirmation reference.
Additional integrator fee charged on the transaction, if configured.
Human-readable failure reason. Always present (may be empty string) for non-successful statuses.
Raw error from the processing pipeline. Always present (may be empty string) for non-successful statuses.
#### Example — successful withdrawal
```json theme={null}
{
"event": "transaction.withdrawal.status.updated",
"data": {
"status": "SUCCESSFUL",
"referenceId": "payout-xyz-001",
"referenceNumber": 2001,
"id": "64a1b2c3d4e5f6a7b8c9d0e3",
"amount": 500,
"walletId": "64a1b2c3d4e5f6a7b8c9d0e2",
"callbackUrl": "https://your-server.com/webhook",
"created_at": "2025-01-01T00:00:00.000Z",
"transactionAmount": 520,
"transactionCost": 20,
"customerKey": "cust_abc123",
"telcoId": "OEI2AK4D9Y"
},
"signature": "sha256=a1b2c3d4e5f6..."
}
```
#### Example — failed withdrawal
```json theme={null}
{
"event": "transaction.withdrawal.status.updated",
"data": {
"status": "FAILED",
"referenceId": "payout-xyz-002",
"referenceNumber": 2002,
"id": "64a1b2c3d4e5f6a7b8c9d0e4",
"amount": 300,
"walletId": "64a1b2c3d4e5f6a7b8c9d0e2",
"transactionAmount": 300,
"transactionCost": 0,
"customerKey": "cust_abc123",
"errorMessage": "Recipient number not found",
"transactionError": "INVALID_PHONE_NUMBER"
},
"signature": "sha256=a1b2c3d4e5f6..."
}
```
***
### `transaction.onramp.status.updated`
Fired when a fiat→crypto onramp transaction changes status. Onramp has **two independent status fields** — fiat collection (`depositStatus`) and on-chain delivery (`onchainStatus`).
#### Payload fields
Your reference ID for the onramp transaction.
Combined overall status of the onramp.
Status of the fiat payment collection leg.
Status of the on-chain crypto delivery leg.
Blockchain the crypto was sent on (e.g. `POLYGON`, `STELLAR`).
Token delivered (e.g. `USDT`, `USDC`).
Expected crypto amount to deliver.
Actual crypto amount delivered on-chain (may differ from `cryptoAmount` due to gas).
Base fiat amount collected (before fee).
Platform fee on the fiat side.
Total fiat the customer paid (`fiatAmount + fiatFee`).
On-chain address the crypto was delivered to.
Blockchain transaction hash once on-chain delivery completes.
Rate used for the conversion (`from`, `to`, `cryptoAmount`).
Error details if the onramp failed. Contains `message`, `code`, and `details`.
#### Example — successful onramp
```json theme={null}
{
"event": "transaction.onramp.status.updated",
"data": {
"referenceId": "onramp-001",
"status": "SUCCESSFUL",
"depositStatus": "SUCCESSFUL",
"onchainStatus": "SUCCESSFUL",
"chain": "POLYGON",
"token": "USDT",
"cryptoAmount": 38.5,
"cryptoAmountReceived": 38.5,
"fiatAmount": 5000,
"fiatFee": 100,
"fiatAmountToSend": 5100,
"receiverAddress": "0xrecipient123...",
"transactionHash": "0xabc123def456...",
"rate": { "from": "KES", "to": "USDT", "cryptoAmount": 38.5 }
},
"signature": "sha256=a1b2c3d4e5f6..."
}
```
#### Example — fiat collected, crypto transfer failed
```json theme={null}
{
"event": "transaction.onramp.status.updated",
"data": {
"referenceId": "onramp-002",
"status": "FAILED",
"depositStatus": "SUCCESSFUL",
"onchainStatus": "FAILED",
"chain": "POLYGON",
"token": "USDT",
"cryptoAmount": 38.5,
"fiatAmount": 5000,
"fiatFee": 100,
"fiatAmountToSend": 5100,
"transactionHash": null,
"error": {
"message": "Crypto transfer failed after retries",
"code": "CRYPTO_TRANSFER_FAILED",
"details": {}
}
},
"signature": "sha256=a1b2c3d4e5f6..."
}
```
***
### `transaction.offramp.status.updated`
Fired when a crypto→fiat offramp changes status.
#### Payload fields
Your reference ID for the offramp transaction.
Overall offramp status (`SUCCESSFUL`, `FAILED`, `PENDING`, etc.).
Status of the on-chain crypto receipt leg.
Full fiat amount before fees.
Amount actually disbursed to the recipient after fees.
Crypto amount received from the sender.
Fiat currency code (e.g. `KES`, `GHS`).
Customer identifier.
On-chain address that sent the crypto.
Kotani escrow address the crypto was sent to.
ID of the integrator fiat wallet used, if applicable.
On-chain transaction hash of the crypto receipt.
Exact on-chain amount confirmed (may differ from `cryptoAmount` due to network fees).
Rate used for the conversion (`from`, `to`, `fiatAmount`).
Whether the platform's own integrated crypto wallet was used.
ISO 8601 creation timestamp.
ISO 8601 last-updated timestamp.
On-chain error details if the crypto receipt failed. Present (may be `{}`) for non-successful transactions.
Fiat disbursement error details. Present (may be `{}`) for non-successful transactions.
#### Example — successful offramp
```json theme={null}
{
"event": "transaction.offramp.status.updated",
"data": {
"referenceId": "offramp-001",
"status": "SUCCESSFUL",
"onchainStatus": "SUCCESSFUL",
"fiatAmount": 5000,
"fiatTransactionAmount": 4850,
"cryptoAmount": 38.5,
"fiatCurrency": "KES",
"customerKey": "cust_abc123",
"fiatWalletId": "64a1b2c3d4e5f6a7b8c9d0e2",
"senderAddress": "0xabc123...",
"transactionHash": "0xdef456...",
"transactionHashAmount": 38.5,
"rate": { "from": "USDT", "to": "KES", "fiatAmount": 5000 },
"escrowAddress": "0xescrow123...",
"usingIntegratedWallet": false,
"created_at": "2025-01-01T00:00:00.000Z",
"updated_at": "2025-01-01T00:05:00.000Z"
},
"signature": "sha256=a1b2c3d4e5f6..."
}
```
#### Example — fiat disbursement failed
```json theme={null}
{
"event": "transaction.offramp.status.updated",
"data": {
"referenceId": "offramp-002",
"status": "FAILED",
"onchainStatus": "SUCCESSFUL",
"fiatAmount": 5000,
"fiatTransactionAmount": 0,
"cryptoAmount": 38.5,
"fiatCurrency": "KES",
"customerKey": "cust_abc123",
"senderAddress": "0xabc123...",
"escrowAddress": "0xescrow123...",
"transactionHash": "0xdef456...",
"onchainError": {},
"transactionError": "Recipient mobile number is not registered"
},
"signature": "sha256=a1b2c3d4e5f6..."
}
```
***
### `refund.completed`
Fired when a crypto refund has been successfully sent back to the sender.
#### Payload fields
Reference ID of the original offramp transaction.
Always `REVERSED`.
Always `SUCCESSFUL`.
On-chain transaction hash of the refund.
Amount refunded (in token native units — sats for Lightning, token units for EVM/Solana).
Chain the refund was sent on.
Token refunded.
Fiat currency of the original transaction.
ISO 8601 timestamp of the refund.
#### Example
```json theme={null}
{
"event": "refund.completed",
"data": {
"referenceId": "offramp-001",
"status": "REVERSED",
"refundStatus": "SUCCESSFUL",
"refundTransactionHash": "0xrefund123...",
"refundAmount": 38.5,
"chain": "POLYGON",
"token": "USDT",
"currency": "KES",
"timestamp": "2025-01-01T00:10:00.000Z"
},
"signature": "sha256=a1b2c3d4e5f6..."
}
```
***
### `refund.failed`
Fired when a refund has exhausted all retry attempts. Manual intervention is required — contact support with the `referenceId`.
#### Payload fields
Reference ID of the original offramp transaction.
Always `FAILED`.
Amount that was attempted for refund.
Chain the refund was attempted on.
Token that was being refunded.
Fiat currency of the original transaction.
Error message from the last refund attempt.
Number of refund attempts made before giving up.
ISO 8601 timestamp of the final failure.
#### Example
```json theme={null}
{
"event": "refund.failed",
"data": {
"referenceId": "offramp-001",
"refundStatus": "FAILED",
"refundAmount": 38.5,
"chain": "POLYGON",
"token": "USDT",
"currency": "KES",
"error": "Refund failed after max retries",
"totalRetries": 5,
"timestamp": "2025-01-01T00:20:00.000Z"
},
"signature": "sha256=a1b2c3d4e5f6..."
}
```
***
### `refund.lightning.invoice_needed`
Fired when a Lightning offramp's fiat disbursement fails and Kotani needs a bolt11 invoice to return the funds. You must submit a valid invoice before the refund can proceed. See [Offramp Refunds](/v3/essentials/offramp-refunds) for the full Lightning refund lifecycle.
#### Payload fields
Reference ID of the original offramp transaction.
Status of the offramp (typically `FAILED`).
On-chain crypto receipt status (typically `SUCCESSFUL` — crypto was received).
Always `INVOICE_NEEDED` when this event fires.
Amount to be refunded in millisatoshis.
Amount to be refunded in satoshis.
Always `LIGHTNING`.
Fiat currency of the original transaction.
Always `true` — you must submit an invoice.
Instructions for submitting the invoice. Contains `type`, `description`, `submitUrl`, `method`, `body`, and `invoiceRequirements`.
#### Example
```json theme={null}
{
"event": "refund.lightning.invoice_needed",
"data": {
"referenceId": "offramp-lightning-001",
"status": "FAILED",
"onchainStatus": "SUCCESSFUL",
"refundStatus": "INVOICE_NEEDED",
"refundAmount": 1500000,
"refundAmountSats": 1500,
"chain": "LIGHTNING",
"currency": "KES",
"requiresAction": true,
"action": {
"type": "SUBMIT_LIGHTNING_INVOICE",
"description": "Submit Lightning invoice for 1500 sats to receive refund",
"submitUrl": "https://api.kotanipay.io/api/v3/offramp/submit-refund-invoice/offramp-lightning-001",
"method": "POST",
"body": { "invoice": "lnbc..." }
}
},
"signature": "sha256=a1b2c3d4e5f6..."
}
```
Submit the invoice to the `action.submitUrl`:
```bash theme={null}
curl -X POST https://api.kotanipay.io/api/v3/offramp/submit-refund-invoice/offramp-lightning-001 \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{"invoice": "lnbc1500n1p0..."}'
```
***
### Settlement events
Settlement events are **opt-in** — subscribe to them in **Settings → Webhooks**.
All single-settlement events (`settlement.approved`, `settlement.processed`, `settlement.rejected`, `settlement.paused`) share the same payload shape.
#### Single settlement payload fields
Kotani internal settlement ID.
Settlement reference ID.
New settlement status (`APPROVED`, `PROCESSED`, `REJECTED`, `PAUSED`).
Gross settlement amount.
Fee charged on this settlement.
Fee as a percentage of the gross amount.
Amount disbursed after fee (`amount - fee`).
Settlement currency (e.g. `KES`, `GHS`).
Approximate USD value of the gross amount at time of settlement.
Approximate USD value of the fee.
Approximate USD value of the net disbursement.
Which wallet balance was settled (e.g. `DEPOSIT`, `WITHDRAWAL`).
Batch ID if this settlement is part of a batch.
Sub-reference within a batch, if applicable.
Destination bank/wallet details for the disbursement.
ISO 8601 event timestamp.
#### Example — `settlement.processed`
```json theme={null}
{
"event": "settlement.processed",
"data": {
"settlementId": "64a1b2c3d4e5f6a7b8c9d0f1",
"referenceId": "SET-2025-001",
"status": "PROCESSED",
"amount": 50000,
"fee": 750,
"feePercentage": 1.5,
"netAmount": 49250,
"currency": "KES",
"tentativeUsdAmount": 387.50,
"tentativeUsdFee": 5.81,
"tentativeUsdNetAmount": 381.69,
"balanceSource": "DEPOSIT",
"beneficiaryDetails": {
"bankName": "Equity Bank",
"accountNumber": "0123456789"
},
"timestamp": "2025-01-01T12:00:00.000Z"
},
"signature": "sha256=a1b2c3d4e5f6..."
}
```
#### Batch settlement payload fields
Batch events (`settlement.batch.approved`, `settlement.batch.processed`, `settlement.batch.rejected`, `settlement.batch.cancelled`) carry the same fields plus a `settlements` array.
Kotani internal batch ID.
Human-readable batch reference.
New batch status.
Total approximate USD value of all settlements in the batch.
Array of settlement summaries in the batch. Each entry contains `_id`, `subReference`, `status`, `currency`, `netAmount`, `tentativeUsdNetAmount`, `referenceId`, and `channels`.
***
## Configuring Webhooks
1. Log in to the dashboard → **Settings**
2. Enter a publicly reachable **HTTPS** endpoint URL
3. Select the events you want to subscribe to
4. Copy the generated signing secret and store it securely
5. Save
You can rotate the signing secret at any time. Update your verification logic with the new secret before applying it in production to avoid a verification gap.
***
## Retries
If your endpoint returns a non-`2xx` response or times out, Kotani Pay retries delivery with exponential backoff — up to every 2 hours for a maximum of 24 hours. Return `200 OK` as quickly as possible and handle processing asynchronously.
# Bulk Payments Flow
Source: https://documentation.kotanipay.com/v3/flows/bulk-payments-flow
How to disburse funds to many recipients at once — validate a CSV, assign wallets, and track batch processing
Bulk payments let you disburse to hundreds or thousands of recipients in a single operation. Upload a CSV of recipients, assign a payout wallet per currency, and Kotani Pay processes the payments asynchronously — one batch per currency.
***
## Before You Start
* An API key — see [API Keys](/v3/getting-started/api-keys)
* Funded payout wallets for each currency in your CSV
* A `callbackUrl` — each individual payment fires a `transaction.withdrawal.status.updated` webhook as it settles
***
## 1. Validate Your CSV
Call [POST /api/v3/bulk-payments/validate-csv](/v3/api-reference/bulk-payments/validate-csv) with your CSV file. Kotani Pay validates every row and returns:
* A summary grouped by currency with totals
* Row-level errors with line numbers if any data is invalid
Fix any errors and re-upload. A file hash is computed to prevent duplicate submissions.
***
## 2. Preview Batches
Call [POST /api/v3/bulk-payments/preview-batches](/v3/api-reference/bulk-payments/preview-batches) with the file hash. This shows you exactly how many batches will be created, the total per currency, and which wallet you'll need to assign to each.
***
## 3. Create Batches
Call [POST /api/v3/bulk-payments/create-batches](/v3/api-reference/bulk-payments/create-batches), assigning a payout wallet to each currency. Kotani Pay verifies your balance, creates the transaction record, and queues all batches for processing.
Save the `transactionId` and `batchId`s from the response — you'll need them to track progress.
***
## 4. Processing Happens Asynchronously
Batches process in the background. For each recipient, Kotani Pay initiates a disbursement to their mobile money wallet or bank account. Your payout balance is debited as each payment is sent.
* If a disbursement succeeds, the recipient gets an SMS from the mobile money network
* If a disbursement fails, the debit is automatically reversed
***
## 5. Kotani Pay Notifies You
Each individual payment fires a `transaction.withdrawal.status.updated` webhook as it settles — the same payload as a standard withdrawal. See [Webhooks](/v3/essentials/webhooks#transactionwithdrawalstatusupdated).
For overall batch progress, poll the batch status endpoints rather than relying solely on per-payment webhooks.
***
## 6. Track Progress
* [GET /api/v3/bulk-payments/batches/:batchId](/v3/api-reference/bulk-payments/get-batch) — current batch status and success/fail counts
* [GET /api/v3/bulk-payments/batches/:batchId/payments](/v3/api-reference/bulk-payments/get-batch-payments) — status of each individual payment
* [GET /api/v3/bulk-payments/transactions/:transactionId](/v3/api-reference/bulk-payments/list-transactions) — overview of all batches under a transaction
Batch statuses move from `pending` → `processing` → `completed`, `completed_with_errors`, or `failed`.
***
## Monitoring from the Dashboard
1. Log in to [integrator.kotanipay.com](https://integrator.kotanipay.com)
2. Navigate to **Bulk Payments** in the left sidebar
3. Select a transaction to see all batches and individual payment statuses
# Deposit Flow
Source: https://documentation.kotanipay.com/v3/flows/deposit-flow
How to collect payments from customers via mobile money, bank checkout, or card
A deposit is a collection — money moves from your customer into your Kotani Pay deposit balance. There are three collection methods: mobile money, bank checkout, and card.
***
## Before You Start
* An API key — see [API Keys](/v3/getting-started/api-keys)
* A fiat wallet for the currency you're collecting in
* A customer record for the person paying — see [Create Customer](/v3/api-reference/customers/mobile-money/create)
* A publicly reachable `callbackUrl` on your server
***
## Mobile Money Deposit
The most common collection method across Kenya, Uganda, Tanzania, Ghana, and other markets.
**1. Create the customer** if you haven't already. The customer record holds the phone number and network details needed to trigger the collection.
**2. Initiate the deposit** — call [POST /api/v3/deposit/mobile-money](/v3/api-reference/deposits/mobile-money) with your `callbackUrl`. The API responds immediately with a `PENDING` transaction. The customer has not paid yet.
**3. The customer confirms.** Depending on the network:
* M-PESA / Airtel — STK push, customer enters PIN to approve
* MTN / others — USSD prompt or SMS confirmation
**4. Kotani Pay notifies you** via your `callbackUrl` when the transaction settles:
* `SUCCESSFUL` — funds collected, your deposit balance will be credited
* `FAILED` — collection failed (declined, insufficient funds, timeout)
If you have a webhook secret configured, the callback arrives in a signed envelope with `X-Kotani-Signature`, `X-Kotani-Event`, and `X-Kotani-Integrator` headers. Without a secret, the transaction fields are posted directly to your `callbackUrl`. See [Webhooks](/v3/essentials/webhooks).
**5. Poll for status** if needed — [GET /api/v3/deposit/mobile-money/status/:reference\_id](/v3/api-reference/deposits/mobile-money-status).
***
## Bank Checkout Deposit
Kotani Pay generates a checkout URL and the customer completes payment on the bank's hosted page.
**1. Initiate the checkout** — call [POST /api/v3/deposit/bank-checkout](/v3/api-reference/deposits/bank-checkout) with your `callbackUrl` and a `redirectUrl` (where to send the customer after they finish on the bank page).
**2. Redirect the customer** to the `checkoutUrl` in the response. They complete payment there.
**3. Kotani Pay notifies you** via your `callbackUrl` when the bank confirms — same event structure as mobile money.
**4. Poll for status** if needed — [GET /api/v3/deposit/bank-checkout/status/:reference\_id](/v3/api-reference/deposits/bank-checkout-status).
***
## Card Deposit
**1. Initiate the charge** — call [POST /api/v3/deposit/card](/v3/api-reference/deposits/card) with your `callbackUrl`.
**2. Handle 3DS if required.** If the response includes a redirect URL, send the customer there to complete 3D Secure authentication before the charge can settle.
**3. Kotani Pay notifies you** via your `callbackUrl` when the charge settles.
**4. Poll for status** if needed — [GET /api/v3/deposit/card/status/:reference\_id](/v3/api-reference/deposits/card-status).
***
## Monitoring from the Dashboard
1. Log in to [integrator.kotanipay.com](https://integrator.kotanipay.com/)
2. Use the **country and currency selector** to filter by market
3. Navigate to **Collections** in the left sidebar
Each row shows the reference, amount, status, customer, and timestamp. Click any row for the full detail including provider reference and callback history.
***
Successful deposits land in your **deposit balance**, not your payout balance. To use collected funds for withdrawals, transfer them to your payout balance first — see [Balances & Settlement](/v3/essentials/balances-and-settlement).
# Offramp Flow (Crypto → Fiat)
Source: https://documentation.kotanipay.com/v3/flows/offramp-flow
How to convert crypto to fiat — customer sends crypto, Kotani Pay disburses fiat via mobile money or bank
An offramp converts cryptocurrency into local fiat currency. The customer sends crypto to a Kotani Pay escrow address; once confirmed on-chain, Kotani Pay disburses the equivalent fiat to the customer's mobile money wallet or bank account.
***
## Before You Start
* An API key — see [API Keys](/v3/getting-started/api-keys)
* A customer record for the recipient — see [Create Customer](/v3/api-reference/customers/mobile-money/create)
* A funded payout balance in the disbursement currency
* A `callbackUrl` on your server
***
## 1. Get a Rate Quote
Fetch the current offramp rate to show the customer how much fiat they'll receive. Call [GET /api/v3/rates/offramp-rate](/v3/api-reference/rates/offramp-rate) with the crypto token and target fiat currency.
***
## 2. Create the Offramp
Call [POST /api/v3/offramp](/v3/api-reference/offramp/create) with your `callbackUrl`. The API responds with a `PENDING` transaction and a `depositAddress` — this is the on-chain address the customer must send crypto to. Display this address (and optionally a QR code) to them.
***
## 3. Customer Sends Crypto
The customer sends the exact amount to the `depositAddress` from their own wallet. Kotani Pay monitors the address and advances the transaction to `CRYPTO_RECEIVED` once the required confirmations are reached. No further action is needed from your server at this step.
***
## 4. Fiat is Disbursed
Once crypto is confirmed, Kotani Pay initiates the fiat disbursement. The recipient gets an SMS confirmation from the mobile money network when funds arrive.
If fiat disbursement fails after crypto is received, Kotani Pay automatically initiates a crypto refund 5 minutes after the failure. The transaction moves to `REFUND_PENDING`. Track this via the refund endpoints below.
***
## 5. Kotani Pay Notifies You
Kotani Pay POSTs to your `callbackUrl` at each terminal state:
* `SUCCESS` — fiat delivered to the recipient
* `FAILED` — disbursement failed
* `REFUNDED` — crypto returned to sender after a failed fiat disbursement
* `REFUND_FAILED` — crypto refund failed; contact support
If you have a webhook secret configured, the callback arrives in a signed envelope with `X-Kotani-Signature`, `X-Kotani-Event`, and `X-Kotani-Integrator` headers. Without a secret, the transaction fields are posted directly to your `callbackUrl`. See [Webhooks](/v3/essentials/webhooks).
***
## 6. Poll for Status
* [GET /api/v3/offramp/:referenceId](/v3/api-reference/offramp/get-status) — full transaction status
* [GET /api/v3/offramp/refund-status/:referenceId](/v3/api-reference/offramp/refund-status) — refund status if fiat disbursement failed
***
## Refund Handling
If the automatic refund fails, you can retry:
* **Single retry** — [POST /api/v3/offramp/retry-refund/:referenceId](/v3/api-reference/offramp/retry-refund)
* **Lightning offramps** — if the refund requires a bolt11 invoice, the customer provides one and you submit it via `POST /api/v3/offramp/submit-refund-invoice/:referenceId`
See [Offramp Refunds](/v3/essentials/offramp-refunds) for the full refund states and handling.
***
## Cancel a Transaction
A transaction can only be cancelled while still in `PENDING` — before any crypto is received on-chain.
[GET /api/v3/offramp/cancel/:referenceId](/v3/api-reference/offramp/cancel)
***
## Monitoring from the Dashboard
1. Log in to [integrator.kotanipay.com](https://integrator.kotanipay.com/)
2. Use the **country and currency selector** to filter by market
3. Navigate to **Offramp** in the left sidebar
Failed transactions show the reason and refund status.
# Onramp Flow (Fiat → Crypto)
Source: https://documentation.kotanipay.com/v3/flows/onramp-flow
How to convert fiat to crypto — customer pays via mobile money or bank, Kotani Pay sends crypto to their wallet
An onramp converts local fiat currency into cryptocurrency. The customer pays via mobile money or bank checkout; once payment confirms, Kotani Pay sends the crypto to the wallet address you provided.
***
## Before You Start
* An API key — see [API Keys](/v3/getting-started/api-keys)
* A customer record for the person paying — see [Create Customer](/v3/api-reference/customers/mobile-money/create)
* A crypto wallet address to receive the funds
* A `callbackUrl` on your server
***
## 1. Get a Rate Quote
Fetch the current exchange rate before initiating so you can show the customer what crypto amount they'll receive. Call [GET /api/v3/rates/onramp-rate](/v3/api-reference/rates/onramp-rate) with the fiat currency and crypto token.
***
## 2. Initiate the Onramp
Call [POST /api/v3/onramp](/v3/api-reference/onramp/create) with your `callbackUrl` and the destination crypto wallet address. The API responds immediately with a `PENDING` transaction. The exchange rate is locked at this point.
***
## 3. Customer Pays
* **Mobile money** — the customer receives an STK push (M-PESA, Airtel) or USSD prompt (MTN, others) and confirms by entering their PIN
* **Bank checkout** — the response includes a `checkoutUrl`; redirect the customer there to complete payment on the bank's page
The customer has not paid yet when you receive the initial response — they must complete their confirmation step.
***
## 4. Crypto is Delivered
Once fiat payment confirms, Kotani Pay converts at the locked rate and sends crypto to the wallet:
* `CRYPTO_PENDING` — fiat confirmed, crypto transfer in progress
* `PROCESSING` — blockchain transaction submitted, awaiting on-chain confirmations
* `SUCCESS` — crypto delivered
If the crypto transfer fails after fiat is collected, the fiat amount is automatically refunded to your fiat wallet.
***
## 5. Kotani Pay Notifies You
Kotani Pay POSTs to your `callbackUrl` when the transaction reaches a terminal state:
* `SUCCESS` — crypto delivered to the wallet
* `FAILED` — transaction failed; fiat refunded automatically
If you have a webhook secret configured, the callback arrives in a signed envelope with `X-Kotani-Signature`, `X-Kotani-Event`, and `X-Kotani-Integrator` headers. Without a secret, the transaction fields are posted directly to your `callbackUrl`. See [Webhooks](/v3/essentials/webhooks).
***
## 6. Poll for Status
* [GET /api/v3/onramp/:referenceId](/v3/api-reference/onramp/get-status) — standard onramp status
* [GET /api/v3/onramp/crypto/:referenceId](/v3/api-reference/onramp/get-crypto-status) — crypto delivery status
***
## Monitoring from the Dashboard
1. Log in to [integrator.kotanipay.com](https://integrator.kotanipay.com/)
2. Use the **country and currency selector** to filter by market
3. Navigate to **Onramp** in the left sidebar
# Payment Links Flow
Source: https://documentation.kotanipay.com/v3/flows/payment-links-flow
How to create a hosted payment page and collect payments via mobile money, bank, card, or crypto
A payment link is a hosted checkout page that Kotani Pay generates for you. Create the link once, share the URL anywhere, and Kotani Pay handles the checkout experience. No need to build your own payment form.
***
## Before You Start
* An API key — see [API Keys](/v3/getting-started/api-keys)
* A fiat wallet for the currency you're collecting in
* A `callbackUrl` on your server for payment notifications
***
## 1. Create the Payment Link
Call `POST /api/v3/payment-links`. At a minimum you'll configure:
* **Pricing type** — fixed amount, variable (customer chooses within a range), package tiers, or open customer choice
* **Payment methods** — which of mobile money, bank checkout, card, and crypto to enable
* **Usage limit** — single-use, limited to a max count, or unlimited
* **callbackUrl** — where to receive payment notifications
The response includes the link URL (e.g. `https://pay.kotanipay.com/l/{shortCode}`). Share it via SMS, email, social, or embed it as a button.
***
## 2. Customer Pays
The customer opens the link in their browser. What happens next depends on the method they select:
* **Mobile money** — the customer enters their phone number; Kotani Pay sends an STK push or USSD prompt; the customer confirms with their PIN
* **Bank checkout** — the customer is redirected to the bank's hosted page to authenticate and authorise the payment
* **Card** — the customer enters card details on a secure page; 3DS challenge is handled inline if required
* **Crypto** — Kotani Pay shows a deposit address and QR code; the customer sends from their own wallet; the page updates as confirmations arrive
***
## 3. Kotani Pay Notifies You
Payment link transactions are backed by standard deposit transactions. When a payment settles, Kotani Pay fires a `transaction.deposit.status.updated` event to your `callbackUrl`. The payload includes the standard deposit fields plus identifiers for the originating payment link.
* `SUCCESSFUL` — payment received, your deposit balance will be credited
* `FAILED` — payment failed or timed out
If you have a webhook secret configured, the callback arrives in a signed envelope with `X-Kotani-Signature`, `X-Kotani-Event`, and `X-Kotani-Integrator` headers. Without a secret, the transaction fields are posted directly to your `callbackUrl`. See [Webhooks](/v3/essentials/webhooks).
***
## 4. Poll for Status
* `GET /api/v3/payment-links/transactions/:referenceId` — status of a specific payment
* `GET /api/v3/payment-links/:shortCode/transactions` — all payments for a given link
***
## Monitoring from the Dashboard
1. Log in to [integrator.kotanipay.com](https://integrator.kotanipay.com)
2. Navigate to **Payment Links** in the left sidebar
3. Click any link to see all transactions associated with it
# Settlement Flow
Source: https://documentation.kotanipay.com/v3/flows/settlement-flow
Settlement lifecycle, batch and merge flows, webhook events, and schedule rules
A settlement moves your Kotani Pay balance to an external account — a bank account, mobile money wallet, or crypto address. This guide covers the full lifecycle from submission to completion, including batch and merge patterns, the PAUSED state, and the webhook events fired at each transition.
***
## Before You Start
* An API key — see [API Keys](/v3/getting-started/api-keys)
* A fiat wallet with a deposit or payout balance to settle from
* A beneficiary destination — either inline `beneficiaryDetails` or a [saved beneficiary](/v3/api-reference/settlements/settlement-config/beneficiaries)
* A `callbackUrl` on your server if you want programmatic status notifications
***
## Settlement Lifecycle
```
PENDING → UNDER_REVIEW → APPROVED → PROCESSED
```
### Active States
| State | Description |
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `PENDING` | Request submitted and awaiting review. The only state in which you can cancel. |
| `UNDER_REVIEW` | A Kotani Pay agent has picked up the request for manual review. |
| `APPROVED` | Review passed. Funds are being disbursed to the destination account. |
| `PAUSED` | An admin has paused the settlement pending additional information or due to a compliance hold — see [PAUSED State](#paused-state). |
### Terminal States
| State | Description |
| ----------- | ---------------------------------------------------------------------------------------------- |
| `PROCESSED` | Funds have been delivered. The settlement is complete. |
| `REJECTED` | Review failed. Not approved. The reason is recorded on the settlement record. |
| `CANCELLED` | Cancelled by the integrator before review began. Only possible from `PENDING`. |
| `FAILED` | An error occurred during disbursement after approval. Contact support with the settlement ID. |
| `REVERSED` | Disbursement was sent but subsequently reversed. Contact support to confirm balance treatment. |
Stop polling once a settlement reaches a terminal state. Use the `callbackUrl` or configure [notification preferences](/v3/api-reference/settlements/notification-preferences) to receive status-change events instead.
***
## PAUSED State
An admin can pause a settlement at any point before `PROCESSED`. When paused:
* `pauseReason` on the settlement record explains why it was paused
* `pausedUntil` indicates when the system will automatically resume the settlement (if set)
* A `settlement.paused` webhook fires immediately — see [Webhook Events](#webhook-events)
The settlement resumes automatically on `pausedUntil` if set. Otherwise it resumes when the admin lifts the hold. Check your registered notification email or Slack channel for context from the Kotani Pay team.
***
## Checking the Schedule
Call [GET /api/v3/integrator/settlements/schedule](/v3/api-reference/settlements/schedule) before submitting to confirm settlements are open and your amount is within limits.
Key fields:
* `enabled` — if `false`, requests will be rejected
* `allowedDays` — requests on unlisted days are queued to the next allowed day
* `cutoffEnabled` / `cutoffTime` / `timezone` — requests after the cutoff are queued to the next allowed day
* `minAmount` / `maxAmount` — both in USD; your local-currency amount is converted before this check
***
## Previewing Fees
Call [GET /api/v3/integrator/settlements/fee-preview](/v3/api-reference/settlements/fee-preview) with `?amount=&walletId=` before submitting. The response shows the fee, net amount, and USD-equivalent values used for limit checking.
***
## Single Settlement Flow
**1. Check the schedule** — confirm settlements are enabled and your amount is within `minAmount`/`maxAmount`.
**2. Preview the fee** — confirm the net amount is acceptable.
**3. Submit** — call [POST /api/v3/integrator/settlements](/v3/api-reference/settlements/request-settlement). The response includes the settlement ID and initial status `PENDING`.
**4. Wait for review** — status moves to `UNDER_REVIEW` then `APPROVED`.
**5. Funds disbursed** — status moves to `PROCESSED` once funds reach your destination.
**6. You are notified** — via `callbackUrl` or [notification preferences](/v3/api-reference/settlements/notification-preferences) at each status transition.
***
## Batch Settlement Flow
Batch settlements submit multiple wallet requests in one call. Each child settlement is processed independently. The batch status reflects the aggregate of its children.
**1. Prepare your requests array** — each item follows the same schema as a single settlement request: `walletId`, `amount`, `balanceSource`, plus `beneficiaryDetails` or `savedBeneficiaryId`.
**2. Submit the batch** — call [POST /api/v3/integrator/settlements/batch](/v3/api-reference/settlements/batch/create) with an optional `batchReference` for your own tracking.
**3. Track the batch** — call [GET /api/v3/integrator/settlements/batch/:batchId](/v3/api-reference/settlements/batch/get) for the overall status. Each child can also be fetched individually via [GET /api/v3/integrator/settlements/:id](/v3/api-reference/settlements/get-settlement).
**4. Cancelling** — call [DELETE /api/v3/integrator/settlements/batch/:batchId](/v3/api-reference/settlements/batch/cancel). Children still in `PENDING` are cancelled; children in `UNDER_REVIEW` or later are not affected.
### Batch Status at Completion
| Condition | Webhook fired |
| ------------------------------------------ | ---------------------------- |
| All children `PROCESSED` | `settlement.batch.processed` |
| All children `REJECTED` | `settlement.batch.rejected` |
| Mix of `PROCESSED` and `REJECTED`/`FAILED` | `settlement.batch.partial` |
| Batch cancelled | `settlement.batch.cancelled` |
| Batch approved by admin | `settlement.batch.approved` |
***
## Merge Flow
The merge endpoint converts two or more existing individual settlements into a batch after the fact.
**1. Identify settlements to merge** — all must be `PENDING` or `UNDER_REVIEW` and not already in a batch.
**2. Submit the merge** — call [POST /api/v3/integrator/settlements/merge](/v3/api-reference/settlements/merge) with the `settlementIds` array (minimum 2).
**3. A new batch is created** — the response contains the new batch ID. The source settlements become children of that batch.
You cannot un-merge settlements once grouped into a batch. To cancel, use [DELETE /api/v3/integrator/settlements/batch/:batchId](/v3/api-reference/settlements/batch/cancel).
***
## Webhook Events
Settlement events are delivered via the signed webhook system — see [Webhook Notifications](/v3/essentials/webhooks) for verification and retry details.
| Event | When it fires |
| ---------------------------- | -------------------------------------------------------- |
| `settlement.approved` | Settlement moves to `APPROVED` |
| `settlement.rejected` | Settlement is `REJECTED` after review |
| `settlement.processed` | Settlement reaches `PROCESSED` — funds delivered |
| `settlement.cancelled` | Settlement cancelled by integrator |
| `settlement.paused` | Admin pauses the settlement |
| `settlement.batch.approved` | Batch approved by admin |
| `settlement.batch.rejected` | All children in a batch are `REJECTED` |
| `settlement.batch.processed` | All children in a batch reach `PROCESSED` |
| `settlement.batch.partial` | Some children `PROCESSED`, others `REJECTED` or `FAILED` |
| `settlement.batch.cancelled` | Batch cancelled by integrator |
### Example: `settlement.paused`
```json theme={null}
{
"event": "settlement.paused",
"data": {
"settlementId": "stl_abc123",
"status": "PAUSED",
"pauseReason": "Additional KYC documentation required for this destination",
"pausedUntil": "2025-05-10T00:00:00.000Z",
"currency": "KES",
"amount": 50000
},
"signature": "sha256=a1b2c3..."
}
```
### Example: `settlement.batch.partial`
```json theme={null}
{
"event": "settlement.batch.partial",
"data": {
"batchId": "bat_def789",
"processedCount": 3,
"rejectedCount": 1,
"failedCount": 0,
"totalCount": 4
},
"signature": "sha256=b2c3d4..."
}
```
***
## Saved Beneficiaries
Store destination details once and reference by ID on any settlement or batch request.
1. Create — [POST /api/v3/integrator/settlement-config/beneficiaries](/v3/api-reference/settlements/settlement-config/create-beneficiary)
2. Use `savedBeneficiaryId` in your settlement or batch request
3. Update — [PATCH /api/v3/integrator/settlement-config/beneficiaries/:id](/v3/api-reference/settlements/settlement-config/get-beneficiary)
4. Delete — [DELETE /api/v3/integrator/settlement-config/beneficiaries/:id](/v3/api-reference/settlements/settlement-config/delete-beneficiary)
***
## Notification Preferences
Configure email and Slack notifications via [PATCH /api/v3/integrator/settlements/notification-preferences](/v3/api-reference/settlements/notification-preferences). These supplement signed webhook delivery and are intended for human awareness rather than programmatic integration.
***
## Related
Full parameter reference for all settlement endpoints
Signed webhooks, verification, and retry behaviour
Deposit vs payout balances
All status codes across the API
# Withdrawal Flow
Source: https://documentation.kotanipay.com/v3/flows/withdrawal-flow
How to disburse funds to customers via mobile money or bank transfer
A withdrawal is a disbursement — money moves from your Kotani Pay payout balance to a customer's mobile money wallet or bank account.
Your **payout balance** must have funds before you can disburse. If it doesn't, transfer from your deposit balance first — see [Balances & Settlement](/v3/essentials/balances-and-settlement).
***
## Before You Start
* An API key — see [API Keys](/v3/getting-started/api-keys)
* A funded payout balance in the disbursement currency
* A customer record for the recipient — see [Create Customer](/v3/api-reference/customers/mobile-money/create)
* A `callbackUrl` on your server
***
## Mobile Money Withdrawal
**1. Create the customer** if you haven't already. The customer record holds the recipient's phone number and network details.
**2. Initiate the withdrawal** — call [POST /api/v3/withdraw/mobile-money](/v3/api-reference/withdrawals/mobile-money) with your `callbackUrl`. The API responds immediately with a `PENDING` transaction. Your payout balance is debited at this point.
**3. The recipient receives funds.** If the disbursement succeeds, the customer receives an SMS confirmation from the mobile money network.
**4. Kotani Pay notifies you** via your `callbackUrl` when the transaction settles:
* `SUCCESSFUL` — funds delivered to the recipient
* `FAILED` — disbursement failed (invalid number, network error, etc.); your payout balance is **automatically reversed**
If you have a webhook secret configured, the callback arrives in a signed envelope with `X-Kotani-Signature`, `X-Kotani-Event`, and `X-Kotani-Integrator` headers. Without a secret, the transaction fields are posted directly to your `callbackUrl`. See [Webhooks](/v3/essentials/webhooks).
**5. Poll for status** if needed — [GET /api/v3/withdraw/status/:reference\_id](/v3/api-reference/withdrawals/mobile-money-status).
***
## Kenya-Specific Withdrawals
Two additional disbursement methods are available for Kenya:
* **Lipa na M-Pesa** — sends to a till number or triggers an STK push — [POST /api/v3/withdraw/lipa-na-mpesa](/v3/api-reference/withdrawals/lipa-na-mpesa)
* **Paybill** — sends to a paybill number with an account reference — [POST /api/v3/withdraw/paybill](/v3/api-reference/withdrawals/paybill)
The flow is the same — initiate, wait for callback, poll if needed.
***
## Bank Withdrawal
Currently supported in Kenya and South Africa.
**1. Get supported banks** for the target currency — [GET /api/v3/withdraw/v2/bank/supporting-banks/:currency](/v3/api-reference/withdrawals/supporting-banks). This gives you the bank codes to use in the request.
**2. Initiate the withdrawal** — call [POST /api/v3/withdraw/v2/bank](/v3/api-reference/withdrawals/bank-v2) with your `callbackUrl` and the recipient's bank account details.
**3. Kotani Pay notifies you** via your `callbackUrl` when the transfer completes or fails — same event structure as mobile money withdrawals.
**4. Poll for status** if needed — [GET /api/v3/withdraw/v2/bank/status/:referenceId](/v3/api-reference/withdrawals/bank-status).
***
## Monitoring from the Dashboard
1. Log in to [integrator.kotanipay.com](https://integrator.kotanipay.com/)
2. Use the **country and currency selector** to filter by market
3. Navigate to **Payouts** in the left sidebar
Each row shows the reference, recipient, amount, status, and timestamp. Failed transactions show the error reason.
# API Keys
Source: https://documentation.kotanipay.com/v3/getting-started/api-keys
How to generate API keys from the dashboard or via the API, and the two key types
API keys are used for server-to-server authentication with the Kotani Pay API. There are two ways to generate them: from the dashboard (no code required) or via the API using the magic link authentication flow.
***
## Method 1 — Generate from the Dashboard
The easiest way to get an API key is directly from the backoffice dashboard.
1. Log in to [integrator.kotanipay.com](https://integrator.kotanipay.com/)
2. Click **API Keys** in the left sidebar
3. Click **Generate New Key** and choose the key type (integrator-level or user-bound)
4. Copy the key immediately — it will not be shown again after you leave the page
5. To revoke a key, find it in the list and click **Revoke**
***
## Method 2 — Generate via the API
If you need to generate keys programmatically, use the magic link authentication flow to get a JWT, then call the key generation endpoint.
### Step 1 — Get a JWT via Magic Link
Call `POST /auth/login` with your registered integrator email:
```bash theme={null}
curl -X POST https://sandbox-api.kotanipay.io/api/v3/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "you@yourcompany.com"}'
```
Response:
```json theme={null}
{
"success": true,
"message": "login magic link sent to your email",
"data": {}
}
```
Check your inbox for an email from Kotani Pay. Click the **Login to Kotani Pay** button (or copy the URL into your browser).
The magic link expires in **1 hour**. If it has expired, repeat the `POST /auth/login` call to get a fresh one.
Clicking the link hits `GET /api/v3/auth/verify?hash=...` and returns your tokens:
```json theme={null}
{
"success": true,
"message": "ok",
"data": {
"user_id": "abc123",
"session_id": "sess_xyz",
"token_id": "tok_123",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}
```
Save the `token` — you'll use it in the next step. The JWT expires after **1 hour**; use `GET /api/v3/auth/refresh-token` with your `refresh_token` to get a new one without re-doing the magic link flow.
**Human verification**: If the link is opened by an email security scanner before you click it, you may be redirected to a verification page. Complete the prompt — the system re-validates the request and issues your tokens normally. See [Human Verification](/v3/api-reference/authentication/human-verify) for details.
### Step 2 — Generate the Key
Two endpoints are available depending on whether you need a signing secret:
**Key only:**
```bash theme={null}
curl -X GET https://sandbox-api.kotanipay.io/api/v3/auth/api-key \
-H "Authorization: Bearer "
```
Response:
```json theme={null}
{
"success": true,
"message": "Api-key generated successfully",
"data": {
"key": "eyJ1c2VyX2lkIjoiYWJjMTIzIiwiY3JlYXRlZF9hdCI6Ii4uLiJ9.a1b2c3d4e5f6..."
}
}
```
**Key + secret** (for end-to-end payload verification):
```bash theme={null}
curl -X GET https://sandbox-api.kotanipay.io/api/v3/auth/api-key/secure \
-H "Authorization: Bearer "
```
Response:
```json theme={null}
{
"success": true,
"message": "Api-key generated successfully",
"data": {
"key": "eyJ1c2VyX2lkIjoiYWJjMTIzIiwiY3JlYXRlZF9hdCI6Ii4uLiJ9.a1b2c3d4e5f6...",
"secret": "7f3a9b2c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0"
}
}
```
Store both values securely. The `secret` will not be shown again.
***
## Using the API Key
Include the key in the `Authorization` header of every API request:
```bash theme={null}
curl -X GET https://sandbox-api.kotanipay.io/health \
-H "Authorization: Bearer eyJ1c2VyX2lkIjoiYWJjMTIzIi4uLn0=.a1b2c3d4..."
```
The API also accepts a JWT token in the same header — both formats are valid.
***
## API Key Types
There are two ownership modes for an API key.
### Integrator Level
Generated without a user assignment — gets **wildcard (`*`) permissions**, meaning full access to all operations. Use this for your main server-to-server integration.
### User Bound
Generated tied to a specific dashboard user account. Starts with zero permissions — you assign only what that key needs via **API Keys** in the dashboard. Useful when you need scoped access (e.g., a key that can only initiate deposits but not withdrawals).
***
## Next Steps
Receive real-time callbacks when transactions update — two delivery modes
Understand how your fiat balances work and how to settle
# Accessing the Dashboard
Source: https://documentation.kotanipay.com/v3/getting-started/dashboard-login
How to log in to the Kotani Pay dashboard, reset your password via OTP, and navigate the interface
The Kotani Pay dashboard is your control center for monitoring transactions, managing wallets, viewing balances, and configuring your integration. It lives at [integrator.kotanipay.com](https://integrator.kotanipay.com/).
***
## Logging In
The dashboard uses **email and password** authentication. Go to [integrator.kotanipay.com](https://integrator.kotanipay.com/), enter your registered email and password, and click **Sign In**.
Your dashboard login is separate from the API authentication flow. The dashboard uses email + password. The API uses a magic link sent to your email — see [API Keys](/v3/getting-started/api-keys) for that flow.
***
## Choosing Your Environment
On your **first login**, before the main dashboard loads, you'll see an environment gate asking which environment to work in.
Select **Sandbox** to work with test data, or **Production** if your account has been provisioned for live transactions. You can switch at any time using the **environment selector in the top right** of the dashboard.
Sandbox and production are completely isolated. Transactions, API keys, wallets, and customers do not cross between environments.
***
## First-Time Login / Setting Your Password
If your account was just created and you haven't set a password yet, use the **Forgot Password** flow below to set one. Your account email is your registered integrator email.
***
## Forgot Password
If you don't have a password or can't remember it:
On the login page, click the **Forgot Password** link.
Enter your registered integrator email and submit. A **6-digit OTP** is sent to that email address. The code expires in **10 minutes**.
Look for an email from Kotani Pay with your one-time password. Check spam if you don't see it within a minute.
The OTP is valid for **10 minutes** only. If it expires, repeat the process to get a new one.
Back on the dashboard, enter the 6-digit code along with your new password (and confirmation). Submit to save.
Once saved, go back to the login screen and sign in with your email and the new password.
If you don't receive the OTP email, check your spam folder or contact [support@kotanipay.com](mailto:support@kotanipay.com).
***
## Dashboard Overview
After logging in, you'll see the main dashboard. The **left sidebar** is your primary navigation.
| Section | What You Can Do |
| ------------------------ | -------------------------------------------------------------------- |
| **Collections** | Browse and search all deposit/collection transactions |
| **Onramp Transactions** | Fiat-to-crypto transaction history |
| **Payment Links** | Create and manage hosted payment links |
| **Payouts** | Browse and search all withdrawal/disbursement transactions |
| **Offramp Transactions** | Crypto-to-fiat transaction history |
| **Bulk Payments** | Upload and manage bulk disbursement batches |
| **Customers** | Create and manage mobile money customer profiles |
| **Wallets** | View deposit and payout balances per currency, transfer between them |
| **Crypto Wallets** | View crypto balances per chain |
| **API Keys** | Generate and manage API keys |
| **Settings** | Webhook URLs, account details, and configuration |
| **Users** | Invite and manage team members who have dashboard access |
| **Roles & Permissions** | Define roles and assign permissions to dashboard users |
***
## Country & Currency Filter
In the **top-right corner** of the dashboard, there is a **country and currency selector**. This filters all transaction lists and balance views to show only records for that country/currency combination.
For example, selecting **Kenya / KES** shows only KES transactions and the KES fiat wallet. If you operate in multiple countries, switch between them here to see the full picture.
***
## Next Steps
Set up API keys to authenticate your server-side integration
Understand deposit vs payout balances and how to settle funds
# Kotani Pay API v3
Source: https://documentation.kotanipay.com/v3/overview
Digital asset transactions using local payment channels
Kotani Pay connects your application to local payment channels across Africa — mobile money networks (M-PESA, MTN Money, Airtel Money, Orange Money), bank transfers, and blockchain networks. Use it to collect payments from users, disburse funds to them, or convert between fiat and crypto.
## Core Operations
Collect mobile money, bank, and card payments from users
Disburse funds to mobile money wallets and bank accounts
Accept fiat payment and deliver crypto to a wallet address
Accept crypto and disburse fiat via mobile money or bank transfer
Receive real-time transaction status updates
## Getting Started
Log in to [integrator.kotanipay.com](https://integrator.kotanipay.com). First-time users can set a password using the Forgot Password flow via OTP.
[Dashboard login guide →](/v3/getting-started/dashboard-login)
Call `POST /auth/login` with your email to receive a magic link. Click it to get a JWT, then call `GET /auth/api-key` to generate your API key.
[API key guide →](/v3/getting-started/api-keys)
Point requests at the sandbox URL and include your API key in the `Authorization: Bearer` header.
[Quickstart →](/v3/quickstart)
## Environments
| Environment | Base URL |
| -------------- | ---------------------------------- |
| **Sandbox** | `https://sandbox-api.kotanipay.io` |
| **Production** | `https://api.kotanipay.io` |
Use the sandbox environment for all development and testing — no real money moves.
## Try It Now
Fork the full API collection with pre-configured environments
## Use Cases
Kotani Pay is used for remittance, forex arbitrage, digital lending, insurance, savings, social protection, business financing, gig worker payments, and more. If you're moving money to or from Africa via local channels, this API covers it.
# Quickstart
Source: https://documentation.kotanipay.com/v3/quickstart
Get started with Kotani Pay API v3 in minutes
## Prerequisites
You need a Kotani Pay integrator account. **Sandbox accounts are self-service** — register now on the Kotani Pay dashboard. Production accounts require a provisioned account from the Kotani Pay team.
***
## 1. Get Your API Key
The fastest way is from the dashboard — no code required.
1. Log in to [integrator.kotanipay.com](https://integrator.kotanipay.com)
2. Navigate to **API Keys** in the left sidebar
3. Click **Generate New Key**
4. Copy and store the key securely — it won't be shown again
If you need a key with a signing secret (for webhook payload verification), use **Generate Secure Key** instead. This returns both a `key` and a `secret`. See [API Keys](/v3/getting-started/api-keys) for the difference.
**Prefer the API?** You can also generate a key programmatically via a magic link flow — see [API Keys](/v3/getting-started/api-keys) for that method.
***
## 2. Make Your First Call
Use the API key in the `Authorization: Bearer` header:
```bash theme={null}
curl -X GET https://sandbox-api.kotanipay.io/health \
-H "Authorization: Bearer "
```
A successful response confirms your key is valid:
```json theme={null}
{
"success": true,
"message": "Health check",
"data": {
"status": "ok"
}
}
```
***
## 3. Pick Your Flow
Now that you have a key, choose what you're building:
Customer pays you via mobile money, bank checkout, or card. Funds land in your deposit balance.
Send money to a customer's mobile money wallet or bank account from your payout balance.
Customer pays fiat via mobile money or bank; Kotani Pay sends crypto to their wallet.
Customer sends crypto to an escrow address; Kotani Pay disburses fiat to their mobile money or bank.
Generate a hosted checkout page — no frontend required. Share the URL and start collecting.
Disburse to hundreds of recipients at once via CSV upload — payroll, commissions, refunds.
***
## 4. Fund Your Payout Balance
Only needed if you're sending money out — withdrawals, offramp, or bulk payments.
Deposits credit your **deposit balance**. Withdrawals draw from your **payout balance**. These are separate — you need to transfer between them before you can disburse.
In the dashboard: **Wallets → Transfer Deposit Balance**
See [Balances & Settlement](/v3/essentials/balances-and-settlement) for the full explanation.
***
## 5. Set Up Webhooks
Every transaction request accepts a `callbackUrl` — Kotani Pay posts the result directly to that URL when the transaction settles. No extra configuration needed for this.
If you want callbacks to arrive with a signature you can verify, configure a webhook secret:
1. Log in to the dashboard → **Settings**
2. Copy the generated signing secret and store it as an environment variable
With a secret configured, every callback includes `X-Kotani-Signature`, `X-Kotani-Event`, and `X-Kotani-Integrator` headers. See [Webhooks](/v3/essentials/webhooks) for both delivery modes, payload structures, and signature verification.
***
## 6. Go Live
1. **Contact Kotani Pay** via your usual communication channel to request a production account
2. Once provisioned, log in to [integrator.kotanipay.com](https://integrator.kotanipay.com) and switch to **Production** using the environment selector in the top right
3. Generate a production API key from **API Keys** in the sidebar
4. Switch your base URL to `https://api.kotanipay.io`
See [Accessing the Dashboard](/v3/getting-started/dashboard-login) for how the environment gate and selector work.
Sandbox credentials do not work in production and vice versa. Always confirm the environment is set correctly before going live.
# Run in Postman
Source: https://documentation.kotanipay.com/v3/run-in-postman
Fork our complete API collection and start testing immediately
# 🚀 Run in Postman
Get started with the Kotani Pay API instantly using our comprehensive Postman collection. No setup required - just fork and start testing!
Fork the complete API collection with 57+ endpoints and pre-configured
environments
## What's Included
* **57 Endpoints** organized by functionality
* **Authentication flows** for login and API key generation
* **Wallet management** for both fiat and crypto assets
* **Payment processing** including deposits, withdrawals, and payouts
* **KYC compliance** endpoints for customer verification
* **Cross-border payments** and invoice management
* **Real-time webhooks** for transaction updates
* **Sandbox Environment**: `https://sandbox-api.kotanipay.io` - **Production
Environment**: `https://api.kotanipay.io` - **Environment Variables**:
Pre-set for immediate use - **Authentication Tokens**: Automatically managed -
**Base URLs**: Ready for both testing and production
* **Request Bodies**: Example data for all endpoints
* **Response Examples**: Expected API responses
* **Error Scenarios**: Common error cases and handling
* **Workflow Examples**: End-to-end transaction flows
* **Test Scripts**: Automated assertion checks
## Quick Setup Guide
Click the "Fork Kotani Pay API Collection" button above to add the
collection to your Postman workspace.
Both sandbox and production environments will be automatically imported with
the collection.
Update the environment variables with your API credentials: - `api_key`:
Your API key from the authentication flow - `jwt_token`: Will be
auto-populated after login - `base_url`: Already configured for each
environment
Begin with the Health Check endpoint to verify your setup, then explore
other endpoints organized by category.
## Testing Workflow
1. **Create Account**: Register at [integrator.kotanipay.com](https://integrator.kotanipay.com/register)
2. **Login**: Get your JWT token for API access 3. **Generate API Key**: Create a secure key
for transactions 4. **Verify Setup**: Test with any endpoint to confirm
authentication
1. **Create Customer**: Add a mobile money customer 2. **Initiate Deposit**:
Start a deposit transaction 3. **Check Status**: Monitor transaction
progress 4. **Handle Webhooks**: Process real-time updates
1. **Verify Balance**: Check wallet balance 2. **Initiate Payout**: Send
money to customers 3. **Track Status**: Monitor payout progress 4. **Confirm
Completion**: Verify successful transfer
Always use the **sandbox environment** for testing to avoid processing real
transactions. Switch to production only when you're ready to go live.
## Need Help?
* **API Documentation**: Explore our [complete API reference](/v3/api-reference/introduction)
* **Quick Start Guide**: Follow our [step-by-step quickstart](/v3/quickstart)
* **Support**: Contact us at [sales@kotanipay.com](mailto:sales@kotanipay.com)
The Postman collection includes automated authentication token management, so
you don't need to manually handle JWT tokens for most requests.
# Create Bulk Payment Batches
Source: https://documentation.kotanipay.com/v3/api-reference/bulk-payments/create-batches
POST /api/v3/dashboard/bulk-payments/api/batches/create
Create bulk payment batches from validated payment data.
# Get Bulk Payment Batch by ID
Source: https://documentation.kotanipay.com/v3/api-reference/bulk-payments/get-batch
GET /api/v3/dashboard/bulk-payments/api/batches/{batchId}
Get detailed information about a specific bulk payment batch.
# Get Batch Payments
Source: https://documentation.kotanipay.com/v3/api-reference/bulk-payments/get-batch-payments
GET /api/v3/dashboard/bulk-payments/api/batches/{batchId}/payments
Get all individual payments for a specific bulk payment batch.
# Get Bulk Payment Statistics
Source: https://documentation.kotanipay.com/v3/api-reference/bulk-payments/get-statistics
GET /api/v3/dashboard/bulk-payments/api/stats
Get statistics and analytics for bulk payments.
# Get Transaction Details
Source: https://documentation.kotanipay.com/v3/api-reference/bulk-payments/get-transaction
GET /api/v3/dashboard/bulk-payments/api/transactions/{transactionId}
Get detailed information about a specific bulk payment transaction including all batches.
# Get Available Wallets
Source: https://documentation.kotanipay.com/v3/api-reference/bulk-payments/get-wallets
GET /api/v3/dashboard/bulk-payments/api/wallets
Get available wallets that can be used for bulk payments.
# Get Bulk Payment Batches
Source: https://documentation.kotanipay.com/v3/api-reference/bulk-payments/list-batches
GET /api/v3/dashboard/bulk-payments/api/batches
Get a list of all bulk payment batches.
# Get Paginated Transactions
Source: https://documentation.kotanipay.com/v3/api-reference/bulk-payments/list-transactions
GET /api/v3/dashboard/bulk-payments/api/transactions
Get a paginated list of all bulk payment transactions.
# Preview Bulk Payment Batches
Source: https://documentation.kotanipay.com/v3/api-reference/bulk-payments/preview-batches
POST /api/v3/dashboard/bulk-payments/api/preview
Preview bulk payment batches before creating them to review fees and totals.
# Process Bulk Payment Batch
Source: https://documentation.kotanipay.com/v3/api-reference/bulk-payments/process-batch
POST /api/v3/dashboard/bulk-payments/api/batches/{batchId}/process
Process and execute a bulk payment batch to initiate all payments.
# Validate CSV File
Source: https://documentation.kotanipay.com/v3/api-reference/bulk-payments/validate-csv
POST /api/v3/dashboard/bulk-payments/api/validate-csv
Validate a CSV file for bulk payments with multi-currency support before creating batches.
# Generate Invoice
Source: https://documentation.kotanipay.com/v3/api-reference/cross-border/generate-invoice
POST /api/v3/cross-boarder/invoice
Generate Invoice
Generate an invoice for cross-border payments between different fiat currencies.
# Get Invoice
Source: https://documentation.kotanipay.com/v3/api-reference/cross-border/get-invoice
GET /api/v3/cross-boarder/invoice/{referenceId}
Get Invoice
Get invoice details for cross-border payments.
# Pay Invoice
Source: https://documentation.kotanipay.com/v3/api-reference/cross-border/pay-invoice
POST /api/v3/cross-boarder/invoice/pay
Pay Invoice
Pay an invoice for cross-border payments.
# Get Paid Invoice Status
Source: https://documentation.kotanipay.com/v3/api-reference/cross-border/pay-invoice-status
GET /api/v3/cross-boarder/invoice/pay/{referenceId}
Get Paid Invoice Status
Get the status of a paid invoice for cross-border payments.
# Create KYC Address
Source: https://documentation.kotanipay.com/v3/api-reference/kyc/create-address
POST /api/v3/kyc/address
This api will create a new kyc address
This API will create a new KYC address record for customer verification.
# Create KYC Basic Details
Source: https://documentation.kotanipay.com/v3/api-reference/kyc/create-basic-details
POST /api/v3/kyc
This api will create a new kyc basic details
This API will create a new KYC basic details record for customer verification.
# Create KYC Document
Source: https://documentation.kotanipay.com/v3/api-reference/kyc/create-document
POST /api/v3/kyc/document
This api will create a new kyc document
This API will create a new KYC document record for customer verification.
# Get KYC Status
Source: https://documentation.kotanipay.com/v3/api-reference/kyc/get-status
GET /api/v3/kyc/status/{kycId}
This api will get kyc status
This API will get KYC status for a specific KYC record.
# Get Integrator KYC Users
Source: https://documentation.kotanipay.com/v3/api-reference/kyc/list-integrator-users
GET /api/v3/kyc/integrator/users
This api will get all integrator kyc users
Get all KYC users associated with your integrator account.
# Cancel Settlement Batch
Source: https://documentation.kotanipay.com/v3/api-reference/settlements/batch/cancel
DELETE /api/v3/integrator/settlements/batch/{batchId}
Cancel an entire settlement batch. All child settlements in `PENDING` status are cancelled. Children already in `UNDER_REVIEW` or later are not affected.
# Create Batch Settlement
Source: https://documentation.kotanipay.com/v3/api-reference/settlements/batch/create
POST /api/v3/integrator/settlements/batch
Submit multiple settlement requests as a single batch. Each item in `requests[]` follows the same shape as a single settlement request. All children share one admin approval step.
# Get Settlement Batch
Source: https://documentation.kotanipay.com/v3/api-reference/settlements/batch/get
GET /api/v3/integrator/settlements/batch/{batchId}
Retrieve a settlement batch by ID, including the status of all child settlements.
# List Settlement Batches
Source: https://documentation.kotanipay.com/v3/api-reference/settlements/batch/list
GET /api/v3/integrator/settlements/batch
Retrieve all settlement batches belonging to the authenticated integrator, ordered by creation date descending.
# Settlement Fee Preview
Source: https://documentation.kotanipay.com/v3/api-reference/settlements/fee-preview
GET /api/v3/integrator/settlements/fee-preview
Preview settlement fees before submitting a request.
# Merge Settlements into Batch
Source: https://documentation.kotanipay.com/v3/api-reference/settlements/merge
POST /api/v3/integrator/settlements/merge
Group two or more existing `PENDING` or `UNDER_REVIEW` settlements into a single batch. The source settlements become children of the new batch and continue through their individual lifecycles under one approval.
# Update Notification Preferences
Source: https://documentation.kotanipay.com/v3/api-reference/settlements/notification-preferences
PATCH /api/v3/integrator/settlements/notification-preferences
Update the email and Slack notification settings for settlement events on your account. These notifications supplement signed webhook delivery and are intended for human awareness.
# Settlement Schedule
Source: https://documentation.kotanipay.com/v3/api-reference/settlements/schedule
GET /api/v3/integrator/settlements/schedule
Retrieve the current settlement schedule and cutoff settings.
# Get Settlement Config
Source: https://documentation.kotanipay.com/v3/api-reference/settlements/settlement-config
GET /api/v3/integrator/settlement-config
Retrieve the current settlement configuration for the authenticated integrator.
# List Settlement Beneficiaries
Source: https://documentation.kotanipay.com/v3/api-reference/settlements/settlement-config/beneficiaries
GET /api/v3/integrator/settlement-config/beneficiaries
Fetch the saved settlement beneficiaries for the authenticated integrator.
# Create Settlement Beneficiary
Source: https://documentation.kotanipay.com/v3/api-reference/settlements/settlement-config/create-beneficiary
POST /api/v3/integrator/settlement-config/beneficiaries
Add a new settlement beneficiary for the authenticated integrator.
# Delete Settlement Beneficiary
Source: https://documentation.kotanipay.com/v3/api-reference/settlements/settlement-config/delete-beneficiary
DELETE /api/v3/integrator/settlement-config/beneficiaries/{beneficiaryId}
Permanently remove a saved settlement beneficiary. Existing settlement requests that used this beneficiary are not affected.
# Update Settlement Beneficiary
Source: https://documentation.kotanipay.com/v3/api-reference/settlements/settlement-config/get-beneficiary
PATCH /api/v3/integrator/settlement-config/beneficiaries/{beneficiaryId}
Update a saved settlement beneficiary by ID.
# Update Settlement Config
Source: https://documentation.kotanipay.com/v3/api-reference/settlements/update-settlement-config
PATCH /api/v3/integrator/settlement-config
Update the settlement configuration for the authenticated integrator.