API Décaissement V2

Créditer le compte AWDpay d'un bénéficiaire depuis votre solde marchand. Codes d'erreur stables, callbacks signés, relance automatique des échecs transitoires.

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
}
ChampObligatoireDescription
amountouiMontant à créditer, hors frais. Doit respecter les bornes de votre compte marchand.
currencyouiDevise active : XOF, XAF, GNF, USD, EUR.
customerEmailouiAdresse email du compte AWDpay du bénéficiaire. Voir § 7.
customIdentifierrecommandéVotre référence, reprise telle quelle dans le callback.
callbackUrlnécessaire pour recevoir un callbackURL HTTPS publique. Sans elle, aucune notification n'est émise.
testnontrue = 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"
}
Le champ 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.

CodeHTTPSignificationAction
API_KEY_MISSING400En-tête Authorization absentAjouter l'en-tête
API_KEY_INVALID400Clé inconnueVérifier la clé et l'environnement
REQUIRED_FIELDS_MISSING400amount, currency ou customerEmail manquantCompléter la requête
INVALID_CALLBACK_URL400callbackUrl non HTTPS ou non publiqueFournir une URL HTTPS joignable
AMOUNT_BELOW_MINIMUM400Sous le minimum autoriséAjuster le montant
AMOUNT_ABOVE_MAXIMUM400Au-dessus du maximum autoriséAjuster le montant
CUSTOMER_NOT_FOUND400Aucun compte AWDpay pour cet emailVoir § 7
INSUFFICIENT_BALANCE400Solde marchand insuffisantApprovisionner le compte

Échecs asynchrones — transmis par callback

CodeSignification
PROVIDER_FAILEDLe prestataire de paiement a refusé l'opération
TRANSACTION_EXPIREDDélai dépassé avant confirmation
MANUAL_REVIEW_REQUIREDOpé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"
}
Les cinq derniers champs sont historiques et conservés pour ne pas casser les intégrations existantes. Les nouvelles intégrations doivent utiliser 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 serveurComportement AWDpay
2xxLivré, aucune relance
5xx, 408, 429Relancé
timeout, DNS, connexion refuséeRelancé
400, 401, 403, 404, autres 4xxAbandon 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.

La déduplication est à votre charge. Conservez les 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 :

  1. Vérifiez que le bénéficiaire possède un compte AWDpay actif.
  2. Utilisez l'adresse exacte de ce compte. La casse est indifférente, les espaces parasites ne le sont pas.
  3. Fournissez callbackUrl pour être notifié du résultat.

Sans callbackUrl, un échec reste invisible de votre côté : la réponse HTTP est alors votre seule information.