Create Withdrawal
Overview
Initiate a withdrawal (payout) from your SyncPay balance to an external destination. Use this endpoint to:
- Send funds to bank accounts
- Send funds to mobile money wallets
- Send funds to crypto wallets
- Cash out your earnings
Important: You must first create a Payout Destination before you can withdraw funds.
Authentication
Type: API Key (required)
Required Headers
| Header | Required | Description |
|---|---|---|
Authorization |
Yes | Format: Bearer sk_test_... or Bearer sk_live_... |
Content-Type |
Yes | Must be application/json |
Request
Method & Path
POST /api/v1/payouts/withdrawals
Request Body
{
"destination_id": "dest_1a2b3c4d5e6f",
"amount": "100.00",
"currency": "NGN",
"reference": "WD-12345",
"idempotency_key": "unique-key-12345"
}
Request Fields
| Field | Type | Required | Description |
|---|---|---|---|
destination_id |
string | Yes | ID of payout destination (from Create Payout Destination) |
amount |
string | Yes | Amount to withdraw (decimal string) |
currency |
string | Yes | Currency code (must match destination currency) |
reference |
string | Yes | Your unique reference for this withdrawal |
idempotency_key |
string | No | Prevents duplicate withdrawals (recommended) |
Dependencies & Prerequisites
Before creating a withdrawal:
- Check balance: Use Get Balances to ensure sufficient funds
- Create destination: Use Create Payout Destination to add withdrawal destination
- Verify destination: Optionally use Resolve Bank Account for bank transfers
Links:
- See: Create Payout Destination
- See: Get Balances
Example Use Case
Scenario: Your business needs to transfer funds from your SyncPay balance to your business bank account for operational expenses. You have ₦100,000 in available balance and want to withdraw it to your GTBank account that you've previously configured as a payout destination. The system should verify sufficient balance, process the withdrawal, and provide tracking information.
Implementation:
// Step 1: Check balance
const balances = await fetch('https://api.usesyncpay.com/api/v1/accounts/balances', {
headers: { 'Authorization': 'Bearer sk_live_abc123xyz...' }
}).then(r => r.json());
const ngnBalance = balances.balances.find(b => b.currency === 'NGN');
console.log(`Available: ₦${ngnBalance.available_balance}`);
// Step 2: Get destination ID (saved from earlier)
const destinationId = 'dest_1a2b3c4d5e6f';
// Step 3: Create withdrawal
const response = await fetch('https://api.usesyncpay.com/api/v1/payouts/withdrawals', {
method: 'POST',
headers: {
'Authorization': 'Bearer sk_live_abc123xyz...',
'Content-Type': 'application/json'
},
body: JSON.stringify({
destination_id: destinationId,
amount: '100000.00',
currency: 'NGN',
reference: `WD-${Date.now()}`,
idempotency_key: `withdrawal-${Date.now()}`
})
});
const withdrawal = await response.json();
console.log('Withdrawal created:', withdrawal.withdrawal_id);
console.log('Status:', withdrawal.status);
Response
200 – Success
Returns the created withdrawal details.
{
"withdrawal_id": "wd_1a2b3c4d5e6f",
"status": "PENDING",
"provider": null,
"provider_reference": "PAY-12345"
}
Response Fields
| Field | Type | Description |
|---|---|---|
withdrawal_id |
string | Unique withdrawal identifier |
status |
string | Withdrawal status (see statuses below) |
provider |
string|null | Payment provider (not exposed for security) |
provider_reference |
string | Transaction reference ID |
Withdrawal Statuses
| Status | Description | Is Final? |
|---|---|---|
PENDING |
Withdrawal created, queued for processing | No |
PROCESSING |
Being processed by payment provider | No |
COMPLETED |
Successfully sent to destination | Yes |
FAILED |
Withdrawal failed | Yes |
CANCELLED |
Cancelled before processing | Yes |
400 – Bad Request
Validation errors or insufficient funds.
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Insufficient balance",
"details": {
"available_balance": "50000.00",
"requested_amount": "100000.00"
}
}
}
Common causes:
- Insufficient available balance
- Amount below minimum withdrawal limit
- Invalid destination ID
- Currency mismatch (destination currency ≠ withdrawal currency)
- Invalid reference format
- Amount format error (must be string, not number)
401 – Unauthorized
{
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid API key"
}
}
404 – Not Found
{
"error": {
"code": "NOT_FOUND",
"message": "Payout destination not found"
}
}
Cause: The destination_id doesn't exist or doesn't belong to your organization.
409 – Conflict
{
"error": {
"code": "CONFLICT",
"message": "Withdrawal request is being processed. Please retry shortly."
}
}
Cause: A withdrawal with the same idempotency key is already being processed.
Minimum Withdrawal Amounts
Each currency has a minimum withdrawal amount:
| Currency | Minimum |
|---|---|
| USD | $10.00 |
| NGN | ₦5,000 |
| GHS | GH₵50 |
| KES | KSh500 |
| USDT_TRC20 | $10.00 |
| USDT_ERC20 | $50.00 |
Attempting to withdraw below the minimum will return a 400 Bad Request error.
Withdrawal Processing Time
Processing times vary by destination type and currency:
| Destination Type | Typical Time |
|---|---|
| Bank Transfer (NGN) | 5-30 minutes |
| Bank Transfer (USD) | 1-3 business days |
| Mobile Money | 1-5 minutes |
| Crypto (TRC20) | 5-15 minutes |
| Crypto (ERC20) | 10-30 minutes |
Note: Times are estimates and may vary based on network conditions and blockchain congestion.
Idempotency
What is Idempotency?
Idempotency ensures that retrying the same withdrawal request multiple times only creates one withdrawal.
How to Use
const idempotencyKey = `withdrawal-${orderId}-${timestamp}`;
const withdrawal = await createWithdrawal({
destination_id: destinationId,
amount: '100.00',
currency: 'USD',
reference: `WD-${orderId}`,
idempotency_key: idempotencyKey // Same key = same withdrawal
});
Behavior
- First request: Creates withdrawal, returns
200 OK - Duplicate request (within 30 min): Returns cached response, no new withdrawal created
- After 30 minutes: Idempotency key expires, new withdrawal can be created
Best practice: Always include an idempotency key for production withdrawals.
Important Notes
Balance Deduction
When a withdrawal is created:
- Amount is immediately deducted from your available balance
- Funds are moved to "in-flight" state
- If withdrawal fails, funds are returned to your balance
- If withdrawal succeeds, funds are gone permanently
Withdrawal Fees
Some providers charge withdrawal fees:
- Bank transfers: Usually free or minimal fee
- Mobile money: May include processing fees
- Crypto: Blockchain network fees apply (deducted from withdrawal amount)
Fees are shown in the withdrawal quote (if you create one first) and deducted automatically.
Currency Matching
The withdrawal currency must match the destination currency:
// CORRECT
{
destination_id: "dest_ngn_bank_account", // NGN destination
currency: "NGN", // NGN withdrawal
amount: "100000.00"
}
// WRONG
{
destination_id: "dest_ngn_bank_account", // NGN destination
currency: "USD", // USD withdrawal - MISMATCH!
amount: "100.00"
}
Reference Format
The reference field:
- Must be unique across your withdrawals
- Used for reconciliation
- Visible in your transaction history
- Helps you track withdrawals in your system
Recommendation:
const reference = `WD-${internalOrderId}-${timestamp}`;
Withdrawal Lifecycle
1. Created
- Status:
PENDING - Funds deducted from available balance
- Queued for processing
2. Processing
- Status:
PROCESSING - Sent for processing
- Validating destination details
3. Completed
- Status:
COMPLETED - Funds sent to destination successfully
- Customer receives funds
4. Failed
- Status:
FAILED - Funds returned to your available balance
- Check status history for failure reason
Monitoring Withdrawals
Option 1: Poll Status
async function waitForWithdrawalCompletion(withdrawalId, maxAttempts = 60) {
const finalStatuses = ['COMPLETED', 'FAILED', 'CANCELLED'];
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
const response = await fetch(
`https://api.usesyncpay.com/api/v1/payouts/withdrawals/${withdrawalId}`,
{ headers: { 'Authorization': 'Bearer sk_live_...' } }
);
const withdrawal = await response.json();
if (finalStatuses.includes(withdrawal.status)) {
return withdrawal;
}
// Wait 10 seconds
await new Promise(resolve => setTimeout(resolve, 10000));
}
throw new Error('Withdrawal status check timed out');
}
Option 2: Webhooks (Recommended)
Set up webhook notifications to receive real-time withdrawal updates. See Webhook Documentation.
Testing
Test Mode Behavior
In test mode (sk_test_...):
- Withdrawals are created but no real money moves
- All statuses are simulated
- Processing times are instant or near-instant
- Transaction references are simulated
Simulating Success/Failure
Contact support to manually:
- Mark test withdrawal as completed
- Mark test withdrawal as failed
- Test webhook delivery
Related Endpoints
- Create Payout Destination - Add withdrawal destination first
- List Payout Destinations - View your destinations
- Get Balances - Check available funds
- Resolve Bank Account - Verify bank details before withdrawal
Next Steps
After creating a withdrawal:
- Monitor status using polling or webhooks
- Handle failures by retrying or alerting user
- Reconcile completed withdrawals with your accounting system
- Update user balance in your application