Skip to content

Payouts API

Payouts API

Send funds to bank accounts via EFT (Electronic Funds Transfer).

Payout Flow

  1. Check balance - Ensure sufficient funds available
  2. Create payout - Call POST /api/v1/payouts with recipient details
  3. Await approval - Operator reviews and approves the payout
  4. Processing - Funds are transferred to recipient
  5. 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:

FieldTypeDescription
merchant_idstring (UUID)Your merchant ID
balance_centsintegerTotal balance for payouts
reserved_centsintegerReserved for pending/approved payouts
totalsobjectCumulative totals (gross payments, fees, withdrawals)
available_centsintegerAvailable for new payouts (balance - reserved)
currencystringCurrency code (ZAR)
Terminal window
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

MethodCodeDescriptionProcessing Time
EFTeftStandard bank transfer to SA accounts1-3 business days

Create Payout

Endpoint: POST /api/v1/payouts

Request Parameters:

FieldTypeRequiredValidationDescription
amount_centsintegerYes> 0Amount in cents (ZAR)
currencystringYeslength = 3Currency code (must be “ZAR”)
payout_methodstringYesenum: eftPayout transfer method
payment_detailsobjectYesSee EFT DetailsBank account information
merchant_referencestringNomax 100 charsYour unique reference
descriptionstringNomax 500 charsPayout description
metadataobjectNo-Additional custom data

EFT Payment Details:

FieldTypeRequiredValidationDescription
account_numberstringYes-Bank account number
branch_codestringYes6 digitsBank branch code
account_holderstringYes-Account holder full name
bank_namestringYes-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

  1. Payout fee is calculated and added to the payout amount
  2. Total amount (payout + fee) is immediately reserved from your balance
  3. Payout enters pending status
  4. Operator reviews and approves/rejects
  5. Final status triggers webhook notification

Payout Lifecycle

States

StateDescriptionTerminal?Balance Impact
pendingAwaiting operator approvalNoReserved
approvedApproved, ready for transferNoReserved
processingTransfer initiatedNoReserved
completedTransfer successfulYesDeducted
failedTransfer failedYesReleased
rejectedRejected by operatorYesReleased

State Transitions

pending → approved → processing → completed
↓ ↓ ↓
rejected rejected failed

Get Payout Status

Endpoint: GET /api/v1/payouts/{payout_id}

Terminal window
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 ID
  • merchant_id - Your merchant ID
  • amount_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:

ParameterTypeDefaultMaxDescription
limitinteger50100Results per page
offsetinteger0-Pagination offset
statusstring--Filter by status (pending, approved, processing, completed, failed, rejected)
Terminal window
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:

FieldTypeDescription
payoutsarrayList of payout objects (see Key Response Fields above)
totalintegerTotal 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 TypeDescriptionStatus
payout.createdPayout createdpending
payout.approvedApproved by operatorapproved
payout.processingTransfer initiatedprocessing
payout.completedTransfer successfulcompleted
payout.failedTransfer failedfailed
payout.rejectedRejected by operatorrejected

See Webhooks for full integration guide including signature verification.

Code Examples

cURL

#!/bin/bash
API_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 requests
import 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()
# Usage
payout = 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 CodeError CodeDescription
400VALIDATION_FAILEDInvalid request parameters
400INSUFFICIENT_BALANCENot enough available balance
400INVALID_PAYMENT_DETAILSBank details validation failed
404PAYOUT_NOT_FOUNDPayout not found
409CONFLICTDuplicate 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"
}
}