1. Endpoint et requête
POST https://awdpay.com/api/disbursement/v2/initiate
Authorization: Bearer <VOTRE_CLE_SECRETE>
Content-Type: application/json
{
"amount": 5000,
"currency": "XOF",
"customerEmail": "beneficiaire@exemple.com",
"customIdentifier": "VOTRE-REF-2026-000042",
"callbackUrl": "https://votre-domaine.example/awdpay/callback",
"test": false
}
| Champ | Obligatoire | Description |
|---|---|---|
amount | oui | Montant à créditer, hors frais. Doit respecter les bornes de votre compte marchand. |
currency | oui | Devise active : XOF, XAF, GNF, USD, EUR. |
customerEmail | oui | Adresse email du compte AWDpay du bénéficiaire. Voir § 7. |
customIdentifier | recommandé | Votre référence, reprise telle quelle dans le callback. |
callbackUrl | nécessaire pour recevoir un callback | URL HTTPS publique. Sans elle, aucune notification n'est émise. |
test | non | true = mode recette. Les mêmes contrôles s'appliquent qu'en production. |
2. Réponses
Succès
{
"success": true,
"message": "User credited with the balance",
"data": {
"trxId": "9G5KZEBZNW24",
"amount": 5000,
"currency": "XOF",
"customer": "beneficiaire@exemple.com"
}
}
Échec
{
"success": false,
"code": "CUSTOMER_NOT_FOUND",
"message": "Invalid user email provided"
}
code est stable. Traitez-le par programme plutôt que le
texte de message, susceptible d'évoluer.
3. Codes d'erreur
Refus synchrones — renvoyés dans la réponse HTTP
Aucune transaction n'est créée et aucun callback n'est émis.
| Code | HTTP | Signification | Action |
|---|---|---|---|
API_KEY_MISSING | 400 | En-tête Authorization absent | Ajouter l'en-tête |
API_KEY_INVALID | 400 | Clé inconnue | Vérifier la clé et l'environnement |
REQUIRED_FIELDS_MISSING | 400 | amount, currency ou customerEmail manquant | Compléter la requête |
INVALID_CALLBACK_URL | 400 | callbackUrl non HTTPS ou non publique | Fournir une URL HTTPS joignable |
AMOUNT_BELOW_MINIMUM | 400 | Sous le minimum autorisé | Ajuster le montant |
AMOUNT_ABOVE_MAXIMUM | 400 | Au-dessus du maximum autorisé | Ajuster le montant |
CUSTOMER_NOT_FOUND | 400 | Aucun compte AWDpay pour cet email | Voir § 7 |
INSUFFICIENT_BALANCE | 400 | Solde marchand insuffisant | Approvisionner le compte |
Échecs asynchrones — transmis par callback
| Code | Signification |
|---|---|
PROVIDER_FAILED | Le prestataire de paiement a refusé l'opération |
TRANSACTION_EXPIRED | Délai dépassé avant confirmation |
MANUAL_REVIEW_REQUIRED | Opération suspendue pour contrôle |
4. Callback structuré
Émis uniquement si callbackUrl a été fournie dans la requête.
POST <votre callbackUrl>
Content-Type: application/json
X-AWDPAY-Event: disbursement.failed
X-AWDPAY-Timestamp: 1786529820
X-AWDPAY-Signature: <hmac_hex>
X-AWDPAY-Signature-V3: sha256=<hmac_hex>
X-AWDPAY-Signature-Version: v2
X-AWDPAY-Callback-Id: cb_9a8dfb14556b
X-AWDPAY-Attempt: 1
{
"eventV2": "disbursement.failed",
"status": "failed",
"code": "CUSTOMER_NOT_FOUND",
"message": "The customerEmail does not match any AWDPay account",
"operation": "disbursement",
"awdpayTransactionId": "9G5KZEBZNW24",
"merchantReference": "VOTRE-REF-2026-000042",
"amount": 5000,
"fee": 175,
"total": 5175,
"currency": "XOF",
"environment": "live",
"callbackId": "cb_9a8dfb14556b",
"attempt": 1,
"createdAt": "2026-08-12T09:18:00.000Z",
"updatedAt": "2026-08-12T09:18:02.000Z",
"event": "withdraw.failed",
"api": "V2",
"type": "withdraw",
"trxId": "9G5KZEBZNW24",
"customIdentifier": "VOTRE-REF-2026-000042"
}
eventV2, awdpayTransactionId et merchantReference.
Événements : disbursement.success et disbursement.failed.
disbursement.pending existe mais n'est pas émis par défaut ; il peut être
activé à votre demande.
Répondez 2xx dès réception. Toute autre réponse est traitée comme un échec.
5. Relance automatique
| Réponse de votre serveur | Comportement AWDpay |
|---|---|
2xx | Livré, aucune relance |
5xx, 408, 429 | Relancé |
| timeout, DNS, connexion refusée | Relancé |
400, 401, 403, 404, autres 4xx | Abandon immédiat |
Six tentatives au maximum, espacées de 1 min, 5 min, 15 min, 1 h, 6 h.
X-AWDPAY-Callback-Id est identique sur toutes les tentatives d'un
même événement ; seul X-AWDPAY-Attempt s'incrémente.
callbackId déjà
traités et ignorez les doublons : un même événement peut vous parvenir plusieurs fois
si votre accusé de réception se perd.
Un 404 ou un 401 n'est jamais relancé : il signale une URL ou
une configuration erronée, qu'une relance ne corrigerait pas.
6. Vérifier la signature
Corps signé : <X-AWDPAY-Timestamp> + . + corps brut de la
requête. Clé : votre secret webhook. Algorithme : HMAC SHA-256.
Deux en-têtes portent le même condensat : X-AWDPAY-Signature (hexadécimal nu,
historique) et X-AWDPAY-Signature-V3 (préfixé sha256=, aligné sur
l'API V3). Vérifiez l'un ou l'autre.
const crypto = require('crypto');
// Le corps BRUT est indispensable : re-serialiser le JSON change les octets
// et invalide la signature.
app.post('/awdpay/callback', express.raw({ type: 'application/json' }), (req, res) => {
const timestamp = req.get('X-AWDPAY-Timestamp');
const recue = (req.get('X-AWDPAY-Signature-V3') || req.get('X-AWDPAY-Signature') || '')
.replace(/^sha256=/, '');
// Anti-rejeu : au-dela de 5 minutes on refuse, meme si la signature est bonne.
if (Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp)) > 300) {
return res.status(401).send('timestamp hors fenetre');
}
const attendue = crypto
.createHmac('sha256', process.env.AWDPAY_WEBHOOK_SECRET)
.update(`${timestamp}.${req.body.toString('utf8')}`)
.digest('hex');
// Comparaison a temps constant : un === classique fuit la position du
// premier octet different et permet de reconstituer la signature.
const a = Buffer.from(recue, 'utf8');
const b = Buffer.from(attendue, 'utf8');
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).send('signature invalide');
}
const evenement = JSON.parse(req.body.toString('utf8'));
if (dejaTraite(req.get('X-AWDPAY-Callback-Id'))) return res.sendStatus(200);
traiter(evenement);
return res.sendStatus(200); // accuser reception AVANT tout traitement long
});
7. CUSTOMER_NOT_FOUND
customerEmail doit être l'adresse du compte AWDpay du bénéficiaire,
et non son identifiant chez vous. Si la personne n'a pas de compte AWDpay, le
décaissement est refusé : AWDpay ne crée pas de compte à la volée.
Avant d'appeler l'API :
- Vérifiez que le bénéficiaire possède un compte AWDpay actif.
- Utilisez l'adresse exacte de ce compte. La casse est indifférente, les espaces parasites ne le sont pas.
- Fournissez
callbackUrlpour être notifié du résultat.
Sans callbackUrl, un échec reste invisible de votre côté : la réponse HTTP
est alors votre seule information.