Disbursement Webhooks
AWDPay sends real-time notifications when your withdrawal status changes.
Configuration
Configure your webhook URL when creating the withdrawal via the callbackUrl parameter:
curl -X POST "https://app.awdpay.com/api/withdraws/initiate" \
-H "Authorization: Bearer $AWDPAY_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"amount": 5000,
"currency": "XOF",
"beneficiaryName": "Amadou Diallo",
"beneficiaryPhone": "+221770123456",
"country": "SN",
"callbackUrl": "https://your-server.com/webhooks/disbursements"
}'
Supported events
| Event | Description | When |
|---|---|---|
withdrawal.pending | Withdrawal created | Immediately after creation |
withdrawal.processing | Processing in progress | When gateway starts processing |
withdrawal.success | ✅ Transfer successful | Funds received by beneficiary |
withdrawal.failed | ❌ Transfer failed | Error during transfer |
Payload format
Example: Successful withdrawal
{
"event": "withdrawal.success",
"timestamp": "2025-01-15T10:30:45Z",
"data": {
"reference": "WTD1704067200000ABC123",
"status": "success",
"amount": 5000.00,
"currency": "XOF",
"beneficiaryName": "Amadou Diallo",
"beneficiaryPhone": "+221770123456",
"gatewayName": "wave-senegal",
"createdAt": "2025-01-15T10:30:00Z",
"processedAt": "2025-01-15T10:30:45Z",
"externalReference": "WAVE_TXN_987654",
"metadata": {
"orderReference": "PAYOUT-8831",
"userId": "usr_12345"
}
}
}
Example: Failed withdrawal
{
"event": "withdrawal.failed",
"timestamp": "2025-01-15T10:31:00Z",
"data": {
"reference": "WTD1704067200000DEF456",
"status": "failed",
"amount": 10000.00,
"currency": "XOF",
"beneficiaryName": "Fatou Ndiaye",
"beneficiaryPhone": "+221771234567",
"gatewayName": "orange-money-sn",
"createdAt": "2025-01-15T10:28:00Z",
"processedAt": "2025-01-15T10:31:00Z",
"failureReason": "insufficient_beneficiary_account",
"failureMessage": "Beneficiary account not eligible",
"metadata": {
"orderReference": "PAYOUT-8832"
}
}
}
Payload fields
| Field | Type | Description |
|---|---|---|
event | String | Event type |
timestamp | DateTime | Event date/time |
data.reference | String | Withdrawal reference |
data.status | String | Current status |
data.amount | Double | Amount |
data.currency | String | Currency |
data.beneficiaryName | String | Beneficiary name |
data.beneficiaryPhone | String | Phone number |
data.gatewayName | String | Gateway |
data.createdAt | DateTime | Creation date |
data.processedAt | DateTime | Processing date |
data.externalReference | String | External reference (gateway) |
data.failureReason | String | Error code (if failed) |
data.failureMessage | String | Error message (if failed) |
data.metadata | Object | Your custom data |
Webhook security
Mandatory verification
AWDPay signs each webhook with an HMAC-SHA256 signature. Always verify the signature before processing the payload.
Security headers
| Header | Description |
|---|---|
X-AWDPay-Signature | HMAC-SHA256 signature of payload |
X-AWDPay-Timestamp | Send timestamp (Unix) |
Signature verification
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, timestamp, secret) {
// 1. Check that timestamp is not too old (5 min max)
const currentTime = Math.floor(Date.now() / 1000);
if (currentTime - parseInt(timestamp) > 300) {
throw new Error('Webhook timestamp expired');
}
// 2. Build message to sign
const signedPayload = `${timestamp}.${JSON.stringify(payload)}`;
// 3. Calculate expected signature
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// 4. Compare securely
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
Complete example (Express.js)
const express = require('express');
const app = express();
app.post('/webhooks/disbursements', express.json(), (req, res) => {
const signature = req.headers['x-awdpay-signature'];
const timestamp = req.headers['x-awdpay-timestamp'];
const payload = req.body;
// Verify signature
try {
const isValid = verifyWebhookSignature(
payload,
signature,
timestamp,
process.env.AWDPAY_WEBHOOK_SECRET
);
if (!isValid) {
console.error('Invalid webhook signature');
return res.status(401).send('Invalid signature');
}
} catch (error) {
console.error('Webhook verification failed:', error.message);
return res.status(401).send(error.message);
}
// Process event
const { event, data } = payload;
switch (event) {
case 'withdrawal.success':
handleSuccessfulWithdrawal(data);
break;
case 'withdrawal.failed':
handleFailedWithdrawal(data);
break;
case 'withdrawal.processing':
handleProcessingWithdrawal(data);
break;
}
// Always respond 200 quickly
res.status(200).send('OK');
});
async function handleSuccessfulWithdrawal(data) {
const { reference, amount, beneficiaryName, metadata } = data;
// Update your database
await db.payouts.update({
where: { orderReference: metadata.orderReference },
data: {
status: 'completed',
awdpayReference: reference,
completedAt: new Date()
}
});
// Notify beneficiary
await sendSMS(data.beneficiaryPhone,
`You have received ${amount} XOF from AWDPay.`
);
}
async function handleFailedWithdrawal(data) {
const { reference, failureReason, metadata } = data;
// Log failure
console.error(`Withdrawal ${reference} failed: ${failureReason}`);
// Update and alert
await db.payouts.update({
where: { orderReference: metadata.orderReference },
data: {
status: 'failed',
failureReason: failureReason
}
});
// Alert support team
await alertSupport(`Payout failed: ${reference}`);
}
Webhook error codes
| Code | Description | Recommended action |
|---|---|---|
insufficient_beneficiary_account | Insufficient/inactive beneficiary account | Verify number |
invalid_phone_number | Invalid phone number | Fix format |
daily_limit_exceeded | Daily limit reached | Retry tomorrow |
operator_unavailable | Operator temporarily unavailable | Retry later |
beneficiary_not_registered | Beneficiary not registered | Use another number |
transaction_rejected | Transaction rejected by operator | Contact support |
Best practices
✅ Do
- Respond quickly — Return
200 OKin less than 5 seconds - Process asynchronously — Put heavy tasks in a queue
- Idempotence — Handle duplicates (same
reference) - Log — Keep all received webhooks
❌ Don't
- Don't block — Avoid long operations in handler
- Don't ignore errors — Log and alert on failures
- Don't trust blindly — Always verify signature
Retry management
AWDPay retries sending webhooks on failure:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
After 5 failed attempts, the webhook is marked as permanently failed.
Next step
➡️ Overview — Back to main documentation