Payouts API
Payouts API
Send funds to bank accounts via EFT (Electronic Funds Transfer).
Payout Flow
- Check balance - Ensure sufficient funds available
- Create payout - Call
POST /api/v1/payoutswith recipient details - Await approval - Operator reviews and approves the payout
- Processing - Funds are transferred to recipient
- Receive webhook - Get notified of final status
Balance Management
Before creating payouts, check your available balance.
Check Balance
Endpoint: GET /api/v1/payouts/balance
Response Fields:
| Field | Type | Description |
|---|---|---|
merchant_id | string (UUID) | Your merchant ID |
balance_cents | integer | Total balance for payouts |
reserved_cents | integer | Reserved for pending/approved payouts |
totals | object | Cumulative totals (gross payments, fees, withdrawals) |
available_cents | integer | Available for new payouts (balance - reserved) |
currency | string | Currency code (ZAR) |
curl -X GET "https://api.qrpay2.com/api/v1/payouts/balance" \ -H "X-API-Key: $API_KEY"Response (200 OK):
{ "status": "success", "data": { "merchant_id": "01932e5d-7f8a-7890-b123-456789abcdef", "balance_cents": 500000, "reserved_cents": 100000, "totals": { "gross_cents": 550000, "fees_cents": 13750, "withdrawals_cents": 36250 }, "available_cents": 400000, "currency": "ZAR" }}Payout Fees
A payout fee is charged when creating a payout request. The fee is calculated at request time, added to the payout amount, and the total (gross_amount_cents) is reserved from your balance. Contact your account manager for specific fee rates applicable to your account.
Payout Methods
| Method | Code | Description | Processing Time |
|---|---|---|---|
| EFT | eft | Standard bank transfer to SA accounts | 1-3 business days |
Create Payout
Endpoint: POST /api/v1/payouts
Request Parameters:
| Field | Type | Required | Validation | Description |
|---|---|---|---|---|
amount_cents | integer | Yes | > 0 | Amount in cents (ZAR) |
currency | string | Yes | length = 3 | Currency code (must be “ZAR”) |
payout_method | string | Yes | enum: eft | Payout transfer method |
payment_details | object | Yes | See EFT Details | Bank account information |
merchant_reference | string | No | max 100 chars | Your unique reference |
description | string | No | max 500 chars | Payout description |
metadata | object | No | - | Additional custom data |
EFT Payment Details:
| Field | Type | Required | Validation | Description |
|---|---|---|---|---|
account_number | string | Yes | - | Bank account number |
branch_code | string | Yes | 6 digits | Bank branch code |
account_holder | string | Yes | - | Account holder full name |
bank_name | string | Yes | - | Name of the bank |
Request Example:
{ "amount_cents": 50000, "currency": "ZAR", "payout_method": "eft", "payment_details": { "account_number": "1234567890", "branch_code": "250655", "account_holder": "John Doe", "bank_name": "First National Bank" }, "merchant_reference": "PAYOUT-2025-001", "description": "Payout #12345", "metadata": { "order_id": "12345", "customer_id": "CUST-001" }}Response (201 Created):
{ "status": "success", "data": { "id": "01932e5d-7f8a-7890-b123-456789abcdef", "merchant_id": "01932e5d-7000-7890-b123-456789abcdef", "amount_cents": 50000, "fee_cents": 500, "gross_amount_cents": 50500, "currency": "ZAR", "status": "pending", "payout_method": "eft", "merchant_reference": "PAYOUT-2025-001", "description": "Payout #12345", "created_at": "2025-01-07T12:00:00Z", "updated_at": "2025-01-07T12:00:00Z" }}What Happens Next
- Payout fee is calculated and added to the payout amount
- Total amount (payout + fee) is immediately reserved from your balance
- Payout enters
pendingstatus - Operator reviews and approves/rejects
- Final status triggers webhook notification
Payout Lifecycle
States
| State | Description | Terminal? | Balance Impact |
|---|---|---|---|
pending | Awaiting operator approval | No | Reserved |
approved | Approved, ready for transfer | No | Reserved |
processing | Transfer initiated | No | Reserved |
completed | Transfer successful | Yes | Deducted |
failed | Transfer failed | Yes | Released |
rejected | Rejected by operator | Yes | Released |
State Transitions
pending → approved → processing → completed ↓ ↓ ↓rejected rejected failedGet Payout Status
Endpoint: GET /api/v1/payouts/{payout_id}
curl -X GET "https://api.qrpay2.com/api/v1/payouts/$PAYOUT_ID" \ -H "X-API-Key: $API_KEY"Response Examples:
Completed Payout:
{ "status": "success", "data": { "id": "01932e5d-7f8a-7890-b123-456789abcdef", "merchant_id": "01932e5d-7000-7890-b123-456789abcdef", "amount_cents": 50000, "fee_cents": 500, "gross_amount_cents": 50500, "currency": "ZAR", "status": "completed", "payout_method": "eft", "merchant_reference": "PAYOUT-2025-001", "description": "Payout #12345", "external_transfer_id": "TXN-ABC123XYZ789", "created_at": "2025-01-07T12:00:00Z", "updated_at": "2025-01-07T14:30:00Z", "completed_at": "2025-01-07T14:30:00Z" }}Failed Payout:
{ "status": "success", "data": { "id": "01932e5d-7f8a-7890-b123-456789abcdef", "merchant_id": "01932e5d-7000-7890-b123-456789abcdef", "amount_cents": 50000, "fee_cents": 500, "gross_amount_cents": 50500, "currency": "ZAR", "status": "failed", "payout_method": "eft", "merchant_reference": "PAYOUT-2025-001", "description": "Payout #12345", "failure_reason": "Invalid bank account number", "created_at": "2025-01-07T12:00:00Z", "updated_at": "2025-01-07T13:15:00Z" }}Rejected Payout:
{ "status": "success", "data": { "id": "01932e5d-7f8a-7890-b123-456789abcdef", "merchant_id": "01932e5d-7000-7890-b123-456789abcdef", "amount_cents": 50000, "fee_cents": 500, "gross_amount_cents": 50500, "currency": "ZAR", "status": "rejected", "payout_method": "eft", "merchant_reference": "PAYOUT-2025-001", "description": "Payout #12345", "rejection_reason": "Suspicious activity detected", "created_at": "2025-01-07T12:00:00Z", "updated_at": "2025-01-07T12:45:00Z" }}Key Response Fields:
id- Unique payout IDmerchant_id- Your merchant IDamount_cents- Payout amount (what recipient receives)fee_cents- Payout fee charged (calculated at request time, locked for this payout)gross_amount_cents- Total deducted from balance (amount + fee)status- Current status (pending, approved, processing, completed, failed, rejected)description- Optional description (if provided in request)external_transfer_id- Bank transfer reference (present when status is completed)failure_reason- Reason for failure (present when status is failed)rejection_reason- Reason for rejection (present when status is rejected)completed_at- Completion timestamp (present when status is completed)updated_at- Last update timestamp (always present)
List Payouts
Endpoint: GET /api/v1/payouts
Query Parameters:
| Parameter | Type | Default | Max | Description |
|---|---|---|---|---|
limit | integer | 50 | 100 | Results per page |
offset | integer | 0 | - | Pagination offset |
status | string | - | - | Filter by status (pending, approved, processing, completed, failed, rejected) |
curl -X GET "https://api.qrpay2.com/api/v1/payouts?limit=10&status=completed" \ -H "X-API-Key: $API_KEY"Response (200 OK):
{ "status": "success", "data": { "payouts": [ { "id": "01932e5d-7f8a-7890-b123-456789abcdef", "merchant_id": "01932e5d-7000-7890-b123-456789abcdef", "amount_cents": 50000, "fee_cents": 500, "gross_amount_cents": 50500, "currency": "ZAR", "status": "completed", "payout_method": "eft", "merchant_reference": "PAYOUT-2025-001", "created_at": "2025-01-07T12:00:00Z", "updated_at": "2025-01-07T14:30:00Z" } ], "total": 1 }}Response Fields:
| Field | Type | Description |
|---|---|---|
payouts | array | List of payout objects (see Key Response Fields above) |
total | integer | Total number of payouts matching the query |
Webhook Notifications
QRPay sends webhook notifications for all payout status changes. Configure your webhook URL in the merchant dashboard.
Event Types:
| Event Type | Description | Status |
|---|---|---|
payout.created | Payout created | pending |
payout.approved | Approved by operator | approved |
payout.processing | Transfer initiated | processing |
payout.completed | Transfer successful | completed |
payout.failed | Transfer failed | failed |
payout.rejected | Rejected by operator | rejected |
See Webhooks for full integration guide including signature verification.
Code Examples
cURL
#!/bin/bashAPI_BASE="https://api.qrpay2.com"API_KEY="sec1-prd-YOUR_KEY..."
curl -X POST "$API_BASE/api/v1/payouts" \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "amount_cents": 50000, "currency": "ZAR", "payout_method": "eft", "payment_details": { "account_number": "1234567890", "branch_code": "250655", "account_holder": "John Doe", "bank_name": "Standard Bank" }, "merchant_reference": "PAYOUT-2025-001", "description": "Payout #12345", "metadata": { "order_id": "12345", "customer_id": "CUST-001" } }'Python
import requestsimport os
API_BASE = "https://api.qrpay2.com"API_KEY = os.environ["QRPAY_API_KEY"]
def create_payout( amount_cents: int, account_details: dict, reference: str, description: str = None, metadata: dict = None) -> dict: payload = { "amount_cents": amount_cents, "currency": "ZAR", "payout_method": "eft", "payment_details": account_details, "merchant_reference": reference, }
if description: payload["description"] = description if metadata: payload["metadata"] = metadata
response = requests.post( f"{API_BASE}/api/v1/payouts", headers={ "X-API-Key": API_KEY, "Content-Type": "application/json", }, json=payload, )
if response.status_code == 201: return response.json()["data"] elif response.status_code == 400: error = response.json()["error"] if error["code"] == "INSUFFICIENT_BALANCE": raise Exception("Insufficient balance for payout") raise Exception(f"Validation error: {error['message']}") else: response.raise_for_status()
# Usagepayout = create_payout( amount_cents=50000, account_details={ "account_number": "1234567890", "branch_code": "250655", "account_holder": "John Doe", "bank_name": "First National Bank", }, reference="PAYOUT-2025-001", description="Payout #12345", metadata={ "order_id": "12345", "customer_id": "CUST-001", },)print(f"Payout created: {payout['id']}")Error Responses
| HTTP Code | Error Code | Description |
|---|---|---|
| 400 | VALIDATION_FAILED | Invalid request parameters |
| 400 | INSUFFICIENT_BALANCE | Not enough available balance |
| 400 | INVALID_PAYMENT_DETAILS | Bank details validation failed |
| 404 | PAYOUT_NOT_FOUND | Payout not found |
| 409 | CONFLICT | Duplicate merchant_reference |
Example Error Responses:
// 400 - Insufficient Balance{ "status": "error", "error": { "code": "INSUFFICIENT_BALANCE", "message": "Available balance (3000 cents) is less than requested amount (5000 cents)" }}
// 400 - Invalid Payment Details{ "status": "error", "error": { "code": "INVALID_PAYMENT_DETAILS", "message": "branch_code must be exactly 6 digits" }}
// 400 - Validation Failed{ "status": "error", "error": { "code": "VALIDATION_FAILED", "message": "amount_cents must be greater than 0" }}
// 404 - Payout Not Found{ "status": "error", "error": { "code": "PAYOUT_NOT_FOUND", "message": "Payout not found" }}