Skip to main content

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

EventDescriptionWhen
withdrawal.pendingWithdrawal createdImmediately after creation
withdrawal.processingProcessing in progressWhen gateway starts processing
withdrawal.success✅ Transfer successfulFunds received by beneficiary
withdrawal.failed❌ Transfer failedError 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

FieldTypeDescription
eventStringEvent type
timestampDateTimeEvent date/time
data.referenceStringWithdrawal reference
data.statusStringCurrent status
data.amountDoubleAmount
data.currencyStringCurrency
data.beneficiaryNameStringBeneficiary name
data.beneficiaryPhoneStringPhone number
data.gatewayNameStringGateway
data.createdAtDateTimeCreation date
data.processedAtDateTimeProcessing date
data.externalReferenceStringExternal reference (gateway)
data.failureReasonStringError code (if failed)
data.failureMessageStringError message (if failed)
data.metadataObjectYour 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

HeaderDescription
X-AWDPay-SignatureHMAC-SHA256 signature of payload
X-AWDPay-TimestampSend 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

CodeDescriptionRecommended action
insufficient_beneficiary_accountInsufficient/inactive beneficiary accountVerify number
invalid_phone_numberInvalid phone numberFix format
daily_limit_exceededDaily limit reachedRetry tomorrow
operator_unavailableOperator temporarily unavailableRetry later
beneficiary_not_registeredBeneficiary not registeredUse another number
transaction_rejectedTransaction rejected by operatorContact support

Best practices

✅ Do

  • Respond quickly — Return 200 OK in 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:

AttemptDelay
1Immediate
21 minute
35 minutes
430 minutes
52 hours

After 5 failed attempts, the webhook is marked as permanently failed.


Next step

➡️ Overview — Back to main documentation