Aller au contenu principal
Tous les articles
Technique13 min de lecture

Tester un webhook en local : tunnel HTTPS, événement de test, journal et rotation du secret

Tester un webhook en local alors que localhost est refusé : tunnel HTTPS, événement de test signé, journal des livraisons, tentatives et rotation du secret.

Pour tester un webhook en local, exposez votre serveur de développement derrière un tunnel HTTPS, déclarez l'URL publique du tunnel comme endpoint avec votre clé de test, puis envoyez-vous l'événement de test : un payment.succeeded synthétique, signé comme un vrai. L'API refuse localhost, les adresses privées et toute URL non HTTPS dès l'enregistrement : le tunnel est le chemin normal, pas un contournement. Ce guide couvre la déclaration de l'endpoint, la boucle de développement, la lecture du journal des livraisons, le calendrier des nouvelles tentatives, la suspension, la rotation du secret sans coupure et la séparation entre sandbox et production.

Déclarer l'endpoint, depuis le portail ou par l'API

ChariPay est une plateforme de paiement marocaine, un PSP opéré par Chari Money, établissement de paiement agréé par Bank Al-Maghrib. Ses webhooks sont des requêtes signées que nos serveurs envoient au vôtre quand quelque chose se passe : un paiement réussi, un remboursement, une échéance d'abonnement. L'adresse qui les reçoit s'appelle un endpoint. Vous la déclarez depuis le portail ou par l'API ; par l'API, c'est la clé utilisée qui décide si l'endpoint appartient à la sandbox ou à la production.

ChampRôleÀ retenir
urladresse de réceptionHTTPS public, port 443 ; localhost, adresses privées et URL non HTTPS refusés dès l'enregistrement
enabledEventsévénements reçusliste explicite recommandée ; vide, elle vaut tous les événements présents et futurs
descriptionlibellépour reconnaître l'endpoint dans le portail
customHeadersen-têtes de routage statiques, optionnelsajoutés à chaque livraison ; ne leur donnez jamais le nom d'un en-tête envoyé par ChariPay

Par l'API, la déclaration tient en un appel :

bash
curl -X POST 'https://api-psp.charipay.ma/api/v1/partner/webhooks/endpoints' \
  -H 'X-CHARI-PAY-API-KEY: chari_sk_test_...' \
  -H 'Content-Type: application/json' \
  -d '{
    "url": "https://votre-boutique.ma/webhooks/charipay",
    "description": "Paiements boutique",
    "enabledEvents": ["payment.succeeded", "refund.succeeded", "refund.failed"]
  }'

La réponse contient le signingSecret (whsec_…), affiché une seule fois — à la création, puis à chaque rotation — et jamais renvoyé par une lecture ultérieure. Rangez-le aussitôt dans un coffre ou une variable d'environnement, avec les mêmes précautions qu'une clé : les règles sont dans protéger ses clés d'API. Lire, modifier — URL, liste d'événements, en-têtes, activation — ou supprimer un endpoint se fait par GET, PATCH et DELETE sur /api/v1/partner/webhooks/endpoints/{id}, tous décrits dans le module « Webhooks » de la documentation.

Une session ou un lien de paiement peut en outre désigner sa propre URL de réception — config.urls.notification pour une session, notificationUrl pour un lien —, qui reçoit les mêmes événements signés. C'est utile pour router une boutique précise ; pour la boucle de développement, partez d'un endpoint déclaré, car c'est sur lui que s'envoie l'événement de test.

Pourquoi localhost est refusé, et comment tester un webhook en local

La règle est posée dès l'enregistrement : l'URL doit être en HTTPS public, sur le port 443, et localhost, les adresses privées ou les URL non HTTPS sont refusés. Le webhook part de nos serveurs vers le vôtre : une adresse localhost ou privée ne désigne rien qui soit joignable depuis Internet, et une URL en clair ferait circuler des données de paiement sans chiffrement. En développement, la réponse est un tunnel HTTPS : un service qui attribue à votre poste une adresse publique en https:// et relaie les requêtes vers votre serveur local. N'importe quel tunnel qui fournit une URL HTTPS publique sur le port 443 convient ; nous n'en recommandons aucun en particulier.

La boucle de développement, dans l'ordre :

  1. 1Lancez votre serveur local avec une route de réception — par exemple /webhooks/charipay — qui conserve le corps brut de la requête.
  2. 2Ouvrez un tunnel HTTPS vers ce serveur et notez l'URL publique qu'il vous attribue.
  3. 3Déclarez cette URL, suivie de votre route, comme endpoint de sandbox — depuis le portail, ou par l'appel ci-dessus avec votre clé de test — et stockez le signingSecret dans votre configuration locale.
  4. 4Envoyez l'événement de test et regardez la requête arriver dans les journaux de votre serveur.
  5. 5Lisez le journal des livraisons pour voir ce que votre serveur a répondu.
  6. 6Faites le ménage : si l'URL du tunnel change à chaque lancement, mettez à jour l'endpoint existant par PATCH plutôt que d'en créer un nouveau, et supprimez les endpoints de développement abandonnés.

À l'étape 3, il ne vous manque qu'une clé de test pour enregistrer un endpoint de sandbox : Commencer en mode test. Trois champs — nom, e-mail professionnel, entreprise —, un lien d'activation reçu par e-mail pour choisir votre mot de passe, puis le portail, où vous créez vous-même votre clé de test après une ré-authentification. Le mode test est gratuit, sans limite de durée, et la vérification de votre entreprise ne conditionne que le passage en production.

Envoyer l'événement de test et vérifier la réponse

Chaque endpoint déclaré dispose d'un événement de test, à déclencher depuis le portail ou par POST /api/v1/partner/webhooks/endpoints/{id}/test. Il envoie un payment.succeeded synthétique portant Test: true, signé exactement comme un vrai événement, et il part même si payment.succeeded ne figure pas dans votre liste enabledEvents. L'appel renvoie l'identifiant de la livraison, que vous relisez ensuite par GET /api/v1/partner/webhooks/events/{deliveryId} pour voir la réponse de votre serveur.

Trois vérifications font un bon test :

  • La signature est acceptée. Si votre vérification échoue sur l'événement de test, elle échouera sur les vrais. Deux causes à vérifier d'abord : un corps re-sérialisé au lieu du corps brut, ou le secret d'un autre endpoint.
  • La réponse est un `2xx`, et elle est rapide. Répondez 2xx une fois le dédoublonnage et la mise à jour métier enregistrés ; le travail lourd part en tâche de fond. Toute réponse autre qu'un 2xx, ou une absence de réponse, déclenche de nouvelles tentatives.
  • Aucun effet métier. L'événement de test ne correspond à aucun paiement réel : votre code le reconnaît à Test: true et ne marque aucune commande payée.

Le code du récepteur — vérification sur le corps brut, dédoublonnage, accusé de réception — n'est pas repris ici : il est expliqué pas à pas dans réussir son intégration des webhooks, avec les cinq erreurs classiques. Une fois l'événement de test au vert, passez à un vrai paiement de sandbox avec tester un paiement en mode test : carte de test, retour sur votre page, événement réel.

Lire le journal des livraisons

Le journal des livraisons répond aux deux questions de tout débogage : avons-nous envoyé, et qu'a répondu votre serveur ? Il se consulte dans le portail ou par l'API. GET /api/v1/partner/webhooks/events liste les événements émis pour l'environnement de votre clé, du plus récent au plus ancien, avec des filtres par endpoint, type d'événement, statut de livraison et période. GET /api/v1/partner/webhooks/events/{deliveryId} détaille une livraison : nombre de tentatives, code HTTP renvoyé par votre serveur à la dernière tentative, prochaine tentative prévue, et le corps exact qui a été signé et envoyé — de quoi recalculer la signature localement quand votre vérification échoue.

Six en-têtes de livraison sont à connaître, graphie et casse comprises :

En-têteCe qu'il porteÀ quoi il vous sert
X-CHARI-SIGNATUREla signature de la livraisonvérifier l'authenticité
X-CHARI-SIGNATURE-NEXTla signature avec le nouveau secret, pendant une rotation seulementbasculer de secret sans coupure
X-CHARI-TIMESTAMPl'horodatage d'envoi, en millisecondes depuis epochentrer dans la vérification, rejeter un rejeu
Chari-Webhook-Idl'identifiant de la livraisonrelier une requête reçue à une livraison
Chari-Event-Idl'identifiant de l'événement, stable sur toutes les tentatives et sur tous vos endpointsdédoublonner
Chari-Event-Typele type de l'événement, par exemple payment.succeededrouter le traitement

Dédupliquez sur Chari-Event-Id, pas sur Chari-Webhook-Id — ce dernier identifie la livraison, pas l'événement. Quand vous enquêtez sur une commande, notez les deux : l'un vous dit de quel événement il s'agit, l'autre quelle livraison vous regardez. Pour retrouver la commande derrière un événement réel, la méthode est dans relier un paiement à sa commande.

Nouvelles tentatives, suspension et réactivation

Toute réponse autre qu'un 2xx — ou une absence de réponse — déclenche de nouvelles tentatives, selon un calendrier fixe :

TentativeMoment
1reimmédiate
2e1 min après la précédente
3e5 min après la précédente
4e30 min après la précédente
5e1 h après la précédente
suivantestoutes les 6 h, jusqu'à 16 tentatives sur environ 72 heures

Au-delà, la livraison est marquée échouée ; elle se rejoue en un clic depuis le portail. Un endpoint qui accumule les échecs est suspendu — le seuil n'est pas publié. Corrigez la cause, réactivez l'endpoint depuis le portail ou par POST /api/v1/partner/webhooks/endpoints/{id}/activate, puis vérifiez avec l'événement de test. Les événements en attente sont conservés pendant la suspension et reprennent dans l'ordre à la réactivation.

En développement, ce calendrier a une conséquence pratique : un tunnel fermé le soir accumule des échecs sur votre endpoint de sandbox. Désactivez l'endpoint par PATCH quand vous arrêtez de travailler : sinon, les échecs répétés peuvent le faire suspendre, et il faudra le réactiver. En production, après une indisponibilité plus longue que la fenêtre de tentatives, ne comptez plus sur le webhook : réconciliez par GET /v1/transactions, qui liste les transactions de la période.

La rotation du secret de webhook, sans coupure

Un secret se change quand un collaborateur part, quand il a pu fuiter, ou simplement à échéance régulière. La rotation est conçue pour ne perdre aucun événement :

  1. 1Demandez un nouveau secret depuis le portail ou par POST /api/v1/partner/webhooks/endpoints/{id}/rotate-secret : la réponse le contient, une seule fois.
  2. 2Pendant la rotation, chaque livraison porte deux signatures : X-CHARI-SIGNATURE, calculée avec l'ancien secret, et X-CHARI-SIGNATURE-NEXT, calculée avec le nouveau.
  3. 3Acceptez l'une ou l'autre : vérifiez avec le secret que vous détenez — l'ancien contre X-CHARI-SIGNATURE, le nouveau contre X-CHARI-SIGNATURE-NEXT.
  4. 4Déployez le nouveau secret sur tous vos serveurs, puis basculez votre vérification sur lui : contre X-CHARI-SIGNATURE-NEXT tant que cet en-tête est présent, contre X-CHARI-SIGNATURE ensuite.
  5. 5Retirez l'ancien secret de votre configuration une fois la bascule faite.

Tant que le champ previousSecretExpiresAt de l'endpoint est renseigné, l'ancien secret vérifie encore et les deux signatures sont envoyées. La rotation terminée, X-CHARI-SIGNATURE-NEXT disparaît et X-CHARI-SIGNATURE est calculée avec le nouveau secret, désormais le seul de l'endpoint : une vérification qui ne lirait que X-CHARI-SIGNATURE-NEXT rejetterait alors chaque livraison. La vérification elle-même ne change pas : c'est la même fonction, appelée avec un autre couple signature-secret.

javascript
const crypto = require('crypto');

function verify(rawBody, signature, timestamp, secret) {
  // ±5-minute anti-replay window — the timestamp is in milliseconds.
  if (Math.abs(Date.now() - Number(timestamp)) > 5 * 60 * 1000) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex');

  // A malformed signature must return false — never throw (500 → retries).
  if (!/^[0-9a-f]{64}$/i.test(signature)) return false;

  // Constant-time comparison: never `===`.
  return crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(signature, 'hex'));
}

Testez la rotation en sandbox avant d'en avoir besoin en production : demandez un nouveau secret, envoyez l'événement de test, vérifiez dans le journal que votre serveur a répondu 2xx avec l'ancien secret, puis avec le nouveau.

Sandbox et production : deux endpoints, deux secrets

Un endpoint appartient à un seul environnement, celui de la clé qui l'a déclaré quand vous passez par l'API : un endpoint de sandbox ne reçoit que les événements de sandbox, un endpoint de production que ceux de production, et leurs secrets diffèrent. Un endpoint ne change pas d'environnement : au lancement, vous en déclarez un nouveau avec votre clé de production. Côté code, vous ne changez que la clé et le secret de webhook ; les URL, les payloads et les codes d'erreur sont identiques. Un oubli à éviter : garder le secret de sandbox en production — votre vérification rejetterait alors chaque notification.

Choisissez vos événements parmi les vingt que documente le module « Catalogue d'événements » de la documentation — paiements (payment.succeeded, payment.failed, order.paid), abonnements, remboursements, mouvements du compte, distribution — et routez toujours sur l'en-tête Chari-Event-Type. payment.failed n'est envoyé que si vous l'avez demandé à la création de la session (notifyOnFailure) : ne bâtissez aucun flux sur lui. Pour un lien réglé en espèces en agence, suivez l'état du lien : dès que le réseau confirme le dépôt, le lien passe « Payé » et vous êtes notifié dans le portail ; côté serveur, l'état du lien se lit par GET /v1/payment-links/{reference}.

Enfin, un même événement peut arriver plus d'une fois, et dans le désordre : c'est par conception. Le raisonnement complet — pourquoi dédoublonner, et où — est dans l'idempotence des paiements.

Questions fréquentes

Peut-on déclarer une adresse localhost comme endpoint de webhooks ?

Non. L'URL doit être en HTTPS public, sur le port 443 : localhost, les adresses privées et les URL non HTTPS sont refusés dès l'enregistrement. En développement, ouvrez un tunnel HTTPS vers votre serveur local et déclarez l'URL publique qu'il vous attribue, avec votre clé de test. Si cette URL change, mettez l'endpoint à jour par PATCH au lieu d'en créer un autre.

Comment envoyer un événement de test à son endpoint ?

Depuis le portail, ou par POST /api/v1/partner/webhooks/endpoints/{id}/test. L'appel envoie un payment.succeeded synthétique portant Test: true, signé comme un vrai, même si cet événement ne figure pas dans votre liste. Il renvoie l'identifiant de la livraison : relisez-la par GET /api/v1/partner/webhooks/events/{deliveryId} pour voir ce que votre serveur a répondu. Vous validez ainsi votre signature avant tout paiement.

Comment distinguer l'événement de test d'un vrai paiement ?

L'événement de test porte Test: true et ne correspond à aucun paiement réel. Sa signature est identique à celle d'un vrai événement : ne comptez donc pas sur elle pour faire la différence. Votre code doit reconnaître Test: true, répondre 2xx et ne déclencher aucun effet métier — aucune commande marquée payée, aucun e-mail envoyé au client.

Combien de fois un webhook est-il renvoyé quand mon serveur ne répond pas ?

Sans réponse 2xx, la première tentative immédiate est suivie de nouvelles tentatives à 1 min, 5 min, 30 min, 1 h, puis toutes les 6 h, jusqu'à 16 tentatives sur environ 72 heures. Au-delà, la livraison est marquée échouée et se rejoue en un clic depuis le portail. Après une panne plus longue, réconciliez par GET /v1/transactions.

Que faire quand un endpoint de webhooks est suspendu ?

Un endpoint est suspendu après des échecs répétés. Corrigez d'abord la cause dans le journal des livraisons — code HTTP, temps de réponse, signature —, puis réactivez-le depuis le portail ou par POST /api/v1/partner/webhooks/endpoints/{id}/activate. Les événements en attente sont conservés et reprennent dans l'ordre ; vérifiez ensuite avec l'événement de test.

Comment faire la rotation du secret de webhook sans perdre d'événements ?

Demandez un nouveau secret depuis le portail ou par POST …/endpoints/{id}/rotate-secret. Pendant la rotation, chaque livraison porte X-CHARI-SIGNATURE avec l'ancien secret et X-CHARI-SIGNATURE-NEXT avec le nouveau : acceptez l'une ou l'autre. Déployez le nouveau secret sur tous vos serveurs, basculez votre vérification, puis retirez l'ancien de votre configuration : la rotation terminée, le nouveau secret se vérifie contre X-CHARI-SIGNATURE. Répétez l'exercice en sandbox avant de le faire en production.

Le secret de webhook est-il le même en sandbox et en production ?

Non. Chaque endpoint appartient à un seul environnement — par l'API, celui de la clé qui l'a déclaré — et reçoit son propre secret : celui de sandbox ne vérifie jamais une livraison de production. Au passage en production, déclarez un nouvel endpoint avec votre clé de production et stockez son secret à part : la clé et le secret de webhook sont les deux seules choses qui changent.

Que faire si l'on a perdu son secret de signature ?

Le secret ne s'affiche qu'à la création et à la rotation : aucune lecture ne le renvoie. Lancez une rotation depuis le portail ou par l'API pour obtenir un nouveau secret, affiché une fois. Pendant la rotation, vérifiez X-CHARI-SIGNATURE-NEXT avec ce nouveau secret, puis X-CHARI-SIGNATURE une fois la rotation terminée ; rangez-le cette fois dans un coffre, à côté de votre clé d'API.

Prochaine étape

Déclarez un endpoint de sandbox derrière un tunnel HTTPS, envoyez-vous l'événement de test et lisez la réponse dans le journal : Commencer en mode test vous donne la clé qu'il faut. Codez ensuite le récepteur avec réussir son intégration des webhooks, puis déroulez un vrai paiement de sandbox avec tester un paiement en mode test.

Écrit par Équipe ChariPay.

À lire ensuite

Technique5 min de lecture

Réussir son intégration des webhooks

Réussir son intégration des webhooks : les cinq erreurs qui reviennent dans presque toutes les intégrations, et le traitement en quinze lignes qui les évite.

Prêt à essayer par vous-même ?

Créez votre compte et encaissez en mode test dès aujourd'hui : c'est gratuit, sans validation à attendre. Une question ? Notre équipe répond aux commerçants comme aux développeurs.

  • Gratuit, sans engagement
  • Mode test dès l'inscription
  • Aucune validation à attendre