Create Withdraw
Creates a new withdraw/transfer to a customer bank account. The withdraw will be processed asynchronously.
Endpoint
POST /api/v1/withdraws
Authentication
This endpoint requires HMAC Integration Authentication.
Request Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
amount | string | Yes | Amount as decimal string (e.g., "1000.00" = 10.00 THB) |
currency | string | No | Currency code. Defaults to "THB" |
referenceId | string | Yes | Your unique reference ID (alphanumeric, underscores, hyphens, dots, 1-255 chars) |
destinationType | string | Yes | Destination type: "bank_account" or "wallet" |
customerBankAccountName | string | Yes | Customer's bank account name |
customerBankAccountNumber | string | Yes | Customer's bank account number |
customerBankCode | string | Yes | 3-digit Thai bank code (e.g., "004" for KBANK) |
description | string | No | Withdraw description (max 255 characters) |
metadata | object | No | Additional metadata (key-value pairs) |
idempotencyKey | string | No | Unique key to prevent duplicate withdraws (recommended) |
Integration Context
Your merchantId and integrationId are automatically provided from your integration
credentials. You don't need to include them in the request.
Example Request
import crypto from 'crypto';
const API_KEY = process.env.PAYMENT_API_KEY!;
const API_SECRET = process.env.PAYMENT_API_SECRET!;
async function createWithdraw() {
const method = 'POST';
const path = '/api/v1/withdraws';
const body = JSON.stringify({
amount: '1000.00',
currency: 'THB',
referenceId: `payout-${Date.now()}`,
destinationType: 'bank_account',
customerBankAccountName: 'John Doe',
customerBankAccountNumber: '1234567890',
customerBankCode: '004',
description: 'Payout for order #12345',
metadata: {
order_id: '12345',
},
idempotencyKey: `payout-12345-${Date.now()}`,
});
const timestamp = Date.now().toString();
const message = `${timestamp}.${body}`;
const signature = crypto
.createHmac('sha256', API_SECRET)
.update(message)
.digest('hex');
const response = await fetch(`https://api.example.com${path}`, {
method,
headers: {
'X-API-Key': API_KEY,
'X-Timestamp': timestamp,
'X-Signature': `sha256=${signature}`,
'Content-Type': 'application/json',
},
body,
});
const result = await response.json();
if (result.success) {
console.log('Withdraw created:', result.data.id);
console.log('Status:', result.data.status);
return result.data;
} else {
console.error('Error:', result.error);
throw new Error(result.error.message);
}
}
createWithdraw();Response
Success Response (201 Created)
{
"success": true,
"data": {
"id": "660e8400-e29b-41d4-a716-446655440001",
"amount": "1000.00",
"currency": "THB",
"status": "pending",
"referenceId": "payout-12345",
"destinationType": "bank_account",
"bankAccountNumber": "1234****7890",
"bankAccountName": "John Doe",
"bankCode": "004",
"destinationId": null,
"description": "Payout for order #12345",
"metadata": {
"order_id": "12345"
},
"paidAt": null,
"canceledAt": null,
"createdAt": "2024-01-01T00:00:00.000Z",
"updatedAt": "2024-01-01T00:00:00.000Z"
}
}Error Response (400 Bad Request)
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "amount must be a valid decimal number",
"metadata": {
"field": "amount"
}
}
}Matching Payout Conflict (409 Conflict)
Within one merchant account, a payout is considered a match when it has the same decimal amount and the payout account holder names are more than 90% similar after removing only formatting such as case, whitespace, and punctuation. Titles and other name content are not removed. A matching payout blocks creation when either condition is true:
- its status is
awaiting_verification,pending, orprocessing, regardless of age; or - it was created less than 15 minutes ago, including a recent
paid,failed, orcanceledpayout.
A terminal match stops blocking after 15 minutes. Different amounts and different merchants are independent. A different bank account number does not bypass the guard when the merchant, amount, and payout name still match.
{
"success": false,
"error": {
"code": "STATE_CONFLICT",
"message": "A matching payout is still active or was created within the last 15 minutes",
"metadata": {
"existingWithdrawId": "8f00bae0-d586-49d2-8355-d03d0351fce8",
"existingWithdrawStatus": "processing",
"reason": "active_payout"
}
}
}For a recent terminal match, reason is recent_payout and metadata also includes
retryAfter, an ISO timestamp for when the 15-minute window ends.
Withdraw Status
After creation, withdraws typically start in pending status and progress to processing as they're handled by the payment provider. awaiting_verification, pending, and processing are active statuses and keep blocking a matching payout regardless of age. An indeterminate provider result remains processing; do not retry it as a new withdrawal until BroPay has reconciled the provider outcome.
Idempotency
Use Idempotency Keys
Always include an idempotencyKey in your requests to prevent duplicate withdraws if you need to
retry a request.
If you make the same request twice with the same caller-provided idempotencyKey, you'll receive the same withdraw object with HTTP 200. This is useful for handling network errors and retries.
If you don't provide an idempotencyKey, the system will auto-generate one, but you won't be able to replay the original request safely. A new or omitted key does not bypass the matching-payout guard and returns HTTP 409 when that guard matches.
Error Codes
| Code | Description |
|---|---|
VALIDATION_ERROR | Invalid request parameters |
INVALID_AMOUNT | Amount must be a valid decimal number |
INVALID_BANK_CODE | Bank code must be a valid 3-digit Thai bank code |
INSUFFICIENT_BALANCE | Merchant account has insufficient balance |
STATE_CONFLICT | A matching payout is active or less than 15 minutes old |
DUPLICATE_REFERENCE_ID | Reference ID already exists for this merchant |
INVALID_CREDENTIALS | Authentication failed |
Related Endpoints
- List Withdraws - List all withdraws
- Retrieve Withdraw - Get withdraw details
- Cancel Withdraw - Cancel a withdraw
See Also
- Withdraws Overview
- Webhooks - Receive withdraw status updates
- Error Handling