Callbacks (notifications)
Quand une transaction atteint un statut final, MoyaPay envoie une requête POST à votre serveur.
Où sont-ils envoyés ?
Aux URL de callback enregistrées sur votre compte par MoyaPay :
- une URL pour les payins ;
- une URL pour les payouts.
Pour les créer ou les modifier, contactez votre interlocuteur MoyaPay.
Contenu de la notification
En-têtes :
| En-tête | Valeur |
|---|---|
Content-Type | application/json |
X-Signature | Signature HMAC-SHA256 du corps, en hexadécimal |
Corps :
{
"code": 200,
"status": "SUCCESS",
"message": "Your transaction is SUCCESS",
"transactionID": "COMMANDE-2026-000123",
"amount": "1000",
"date": "2026-10-04T10:15:30.000Z",
"motif": ""
}
| Champ | Description |
|---|---|
status | Statut final : SUCCESS ou FAILED |
transactionID | Votre merchant_transaction_id |
amount | Montant de la transaction, sous forme de chaîne |
date | Date de création de la transaction (ISO 8601) |
motif | Motif de l'échec quand l'opérateur en fournit un, sinon chaîne vide |
code, message | Valeurs informatives. Ne basez pas votre logique dessus |
Vérifier la signature
Calculez le HMAC-SHA256 du corps brut de la requête avec votre secret de signature, puis comparez-le à X-Signature. Rejetez la notification si les deux ne correspondent pas.
La signature porte sur les octets exacts envoyés par MoyaPay. Si vous analysez le JSON puis le réencodez avant de calculer la signature, l'ordre ou le format des champs peut changer et la vérification échouera. Calculez-la sur le corps tel que reçu.
- Node.js
- PHP
- Python (Flask)
const crypto = require('crypto');
const express = require('express');
const app = express();
app.post(
'/api/moyapay/notify',
express.raw({ type: 'application/json' }), // corps brut (Buffer)
(req, res) => {
const received = req.get('X-Signature') || '';
const expected = crypto
.createHmac('sha256', process.env.MOYAPAY_CALLBACK_SECRET)
.update(req.body)
.digest('hex');
const valid =
received.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
if (!valid) return res.sendStatus(401);
const event = JSON.parse(req.body.toString('utf8'));
// Traitement idempotent : event.transactionID, event.status
res.sendStatus(200);
},
);
<?php
$raw = file_get_contents('php://input'); // corps brut
$received = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
$expected = hash_hmac('sha256', $raw, getenv('MOYAPAY_CALLBACK_SECRET'));
if (!hash_equals($expected, $received)) {
http_response_code(401);
exit;
}
$event = json_decode($raw, true);
// Traitement idempotent : $event['transactionID'], $event['status']
http_response_code(200);
import hashlib
import hmac
import os
from flask import Flask, abort, request
app = Flask(__name__)
@app.post("/api/moyapay/notify")
def notify():
raw = request.get_data() # corps brut (bytes)
received = request.headers.get("X-Signature", "")
expected = hmac.new(
os.environ["MOYAPAY_CALLBACK_SECRET"].encode(), raw, hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, received):
abort(401)
event = request.get_json(force=True)
# Traitement idempotent : event["transactionID"], event["status"]
return "", 200
Répondre à la notification
- Répondez avec un statut
2xxpour confirmer la réception. - Répondez en moins de 2 secondes : au-delà, MoyaPay abandonne l'appel. Confirmez d'abord, traitez ensuite (file d'attente, tâche asynchrone).
Fiabilité : ce qu'il faut prévoir
La notification est envoyée au moment où le statut final est enregistré. Il n'y a pas de planning de relance automatique si votre serveur est indisponible ou répond trop lentement à ce moment-là.
- Si vous n'avez pas reçu de callback pour une transaction après un délai raisonnable, interrogez son statut.
- Rendez votre traitement idempotent : une même transaction peut être notifiée plus d'une fois, et un
FAILEDpeut être suivi d'unSUCCESS(voir Statuts). - Gardez votre secret de signature côté serveur et ne le publiez jamais (dépôt de code, application mobile, etc.).