Webhooks -- Vue d'ensemble
Les webhooks permettent à votre application de recevoir des notifications en temps réel sur les événements de la plateforme Owem Pay. Quand un événement se produit, Owem Pay envoie un HTTP POST à l'URL enregistrée.
Comment ça fonctionne
- Enregistrez une URL de webhook dans votre compte
- Quand un événement se produit (ex. : PIX reçu), Owem Pay envoie un HTTP POST à votre URL
- Votre application traite la notification et répond avec un status
2xx(200, 201 ou 204)
Événements disponibles
Owem Pay livre uniquement des événements liés au PIX. D'autres produits (boleto, comptes, STA, virements non-PIX) ne sont pas dans le périmètre. Toute tentative d'abonnement à des événements hors du tableau ci-dessous est rejetée avec events: contains invalid events: ....
| Événement | Status body | Description | Déclenchement |
|---|---|---|---|
pix.charge.created | created | QR code généré ou cash-in initié | Actif |
pix.charge.paid | paid | PIX reçu et liquidé | Actif |
pix.charge.expired | expired | QR code expiré sans paiement (vérifié toutes les 5 min par worker) | Actif |
pix.charge.cancelled | cancelled | QR code annulé avant le paiement | Enregistré, pas encore déclenché |
pix.payout.queued | queued | PIX envoyé mis en file par rate limit (ClientLimiter par merchant ou bucket DICT BACEN). Retry automatique toutes les ~3s, TTL maximum 2h | Actif |
pix.payout.processing | processing | PIX envoyé, en attente de confirmation BACEN | Actif |
pix.payout.confirmed | settled | PIX envoyé et confirmé (terminal) | Actif |
pix.payout.failed | rejected | PIX envoyé rejeté par le SPI (terminal) | Actif |
pix.payout.returned | returned | PIX envoyé remboursé | Actif |
pix.refund.requested | requested | Demande de remboursement reçue (infraction BACEN) ; blocage conservatoire créé sur le solde du client | Actif |
pix.refund.completed | settled / completed | Analyse de la défense finalisée et remboursement exécuté (ou libéré) | Actif |
pix.return.received | settled | Remboursement PIX reçu (crédit) | Actif |
pix.infraction.created | ACKNOWLEDGED | Infraction PIX signalée par la contrepartie via BACEN DICT ; exige une défense ou génère un blocage conservatoire automatique (>R$1k) | Actif |
pix.infraction.resolved | CLOSED / CANCELLED | Infraction résolue (admin close, auto-deny ou contrepartie a annulé) | Actif |
pix.infraction.defense_submitted | defense_submitted | Défense soumise par le merchant (portail ou API) ; attend l'analyse BACEN | Actif |
webhook.test | test | Test manuel. Disponible uniquement via Admin/Merchant portal — l'External API n'expose pas d'endpoint pour déclencher un test | Déclenchement manuel (pas External API) |
pix.charge.cancelled n'est pas encore déclenché
L'événement est dans l'enum et peut être souscrit, mais le système n'a pas de flux d'annulation de QR code aujourd'hui. Si vous vous abonnez, le POST /webhooks répond 201 normalement — mais aucune notification n'arrivera. Continuez à surveiller pix.charge.expired pour le cycle naturel de vie du QR.
Sécurité
Chaque notification inclut des headers de sécurité et d'identification pour la validation :
| Header | Description |
|---|---|
X-Owem-Signature | Signature HMAC-SHA256 du payload (préfixe sha256=). Dans des cas rares (webhook enregistré sans secret), la valeur littérale est unsigned — voir note ci-dessous |
X-Owem-Timestamp | Unix timestamp en secondes de l'envoi |
X-Owem-Event-Id | UUID unique de la delivery (pour déduplication) |
X-Owem-Event-Type | Type de l'événement (ex. : pix.charge.paid) |
Content-Type | Toujours application/json |
User-Agent | Toujours Owem-Webhook/1.0 — utilisez pour le whitelisting dans firewalls/WAF. L'évolution future suivra le pattern Owem-Webhook/{version} ; filtrez par préfixe Owem-Webhook/ pour être immunisé aux nouvelles versions |
Signature unsigned quand le webhook n'a pas de secret
Si le webhook a été enregistré sans champ secret (scénario legacy), le header X-Owem-Signature vaut littéralement unsigned. Cela désactive la validation HMAC de votre côté. En pratique, POST /api/external/webhooks génère un secret aléatoire de 64 caractères quand le champ est omis (depuis session 80), donc le scénario n'apparaît que dans des enregistrements très anciens ou via admin bypass. Si vous recevez unsigned, enregistrez un nouveau webhook avec secret explicite et supprimez l'ancien.
SHA256 dans les webhooks vs SHA512 dans l'API
L'API utilise HMAC-SHA512 pour authentifier les requêtes que vous envoyez. Les webhooks envoyés par Owem Pay utilisent HMAC-SHA256 dans la signature X-Owem-Signature. Ce sont des algorithmes différents -- chacun dans son contexte.
Validation de la signature
Validez la signature pour garantir que la notification a été envoyée par Owem Pay :
const crypto = require('crypto');
function validateWebhook(rawBody, timestamp, signature, secret) {
// rawBody is the RAW request body string (before any JSON parse)
// timestamp is unix seconds (e.g., 1712160000)
const message = `${timestamp}.${rawBody}`;
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(message)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}Utilisez le body RAW, pas re-sérialisé
Vous devez utiliser le corps exact de la requête HTTP tel que les octets sont arrivés dans votre application. Si vous faites JSON.parse puis JSON.stringify, les octets résultants ne seront pas identiques à ceux qu'Owem a utilisé pour signer, et la validation échouera.
En Express/Node : utilisez express.raw({ type: 'application/json' }) ou gardez le body avant tout middleware de parse.
Dans d'autres frameworks : configurez pour capturer le raw body avant le middleware JSON.
Tri des clés dans les WEBHOOKS : PAS nécessaire
Pour la validation des webhooks (HMAC-SHA256) vous N'avez PAS besoin de trier les clés — utilisez le body raw tel que reçu dans le HTTP request d'Owem.
⚠️ Attention — différence vs envoi de requêtes : Dans la signature HMAC-SHA512 des REQUÊTES que vous envoyez, le tri alphabétique des clés EST obligatoire (le serveur Owem réordonne avant de valider). Ne confondez pas les deux scénarios :
- Webhook reçu (HMAC-SHA256) : validez le body raw sans réordonner
- Request envoyée (HMAC-SHA512) : triez vos clés alphabétiquement avant de signer
Validez toujours
Ne traitez jamais un webhook sans valider la signature. Cela protège contre les requêtes falsifiées.
De plus, validez que le X-Owem-Timestamp est dans ± 5 minutes de l'heure actuelle (protection anti-replay — le serveur ne rejette pas les webhooks « anciens » par défaut ; cette vérification appartient à votre endpoint comme défense en profondeur) et dédupliquez les événements par X-Owem-Event-Id (protection contre les retries).
Retry Policy
Si votre URL retourne un status différent de 2xx (ou timeout après 30 s), Owem Pay effectue jusqu'à 8 tentatives avec backoff exponentiel. Le total du temps entre la première et la huitième tentative est d'environ 7h45min :
| Tentative | Délai depuis la tentative précédente | Temps cumulé |
|---|---|---|
| 1re | — (immédiat, eager via Task.start) | ~50–200 ms |
| 2e | 30 secondes | ~30 s |
| 3e | 2 minutes | ~2,5 min |
| 4e | 10 minutes | ~12,5 min |
| 5e | 30 minutes | ~42,5 min |
| 6e | 1 heure | ~1,75 h |
| 7e | 2 heures | ~3,75 h |
| 8e | 4 heures | ~7,75 h |
Après 8 tentatives sans succès, le webhook_delivery est marqué avec le status failed et n'est pas renvoyé automatiquement. Vous pouvez demander un replay manuel au support Owem en fournissant le X-Owem-Event-Id (ou avoir un opérateur avec accès admin qui replay via le portail).
Status d'une delivery
Chaque delivery passe par les status : pending (créée, en attente de livraison) → delivered (2xx reçu) OU failed (8 tentatives épuisées) OU expired (replay protection).
À propos d'expired : quand le worker Oban va traiter la première tentative et que la delivery a déjà plus de 5 minutes depuis sa création (inserted_at), l'envoi est abandonné et le status passe directement à expired. Cela empêche que des retraitements tardifs (par accumulation de file, pod redémarré, etc.) déclenchent des notifications d'événements déjà anciens. Les replays manuels demandés au support Owem passent par le flag interne manual_replay: true et bypass ce guard — le client reçoit la notification normalement.
Durabilité
Avant de tenter la première livraison, l'événement est persisté dans webhook_deliveries dans PostgreSQL. Si le pod qui déclenche le webhook tombe pendant la livraison, Oban reprend automatiquement au prochain retry — aucun événement n'est perdu.
Idempotence
Votre application doit être idempotente : si elle reçoit le même événement plus d'une fois (identifié par X-Owem-Event-Id), elle doit le traiter sans dupliquer les effets.
Replay manuel via admin
Si une delivery a échoué et que vous devez la renvoyer, l'équipe Owem peut exécuter un replay manuel via le dashboard admin. Contactez le support avec le event_id de la delivery.
Livraisons dupliquées (race condition connue)
Le système utilise un mécanisme de « eager delivery » pour accélérer la première livraison + un Oban worker comme fallback durable de retry. Dans les scénarios de forte concurrence, ces deux chemins peuvent déclencher le même webhook en parallèle (fenêtre de ~1 seconde). Dans cette situation vous recevez le même payload 2 fois via HTTP, mais avec le même X-Owem-Event-Id — c'est le même événement, pas un retry.
Pour éviter l'impact dupliqué :
- Dédupliquez par
X-Owem-Event-Id(recommandé — UUID unique par delivery, stable dans les retries et dans la race condition ci-dessus) - Ou alternativement dédupliquez par
end_to_end_id+event_typequand cela a du sens pour l'événement
C'est un comportement attendu, pas une erreur. Les retries légitimes (après 5xx/timeout) réutilisent également le même X-Owem-Event-Id.
External ID dans les webhooks
Quand une transaction a été créée avec external_id, ce champ est inclus dans le payload du webhook à l'intérieur de l'objet data. Utilisez-le pour corréler l'événement avec la commande dans votre système sans avoir besoin de faire une consultation supplémentaire.
Exigences de l'endpoint
- L'URL doit utiliser HTTPS (sauf si
allow_insecure: truedans l'enregistrement) - Doit répondre avec le status
2xxdans les 30 secondes (receive_timeoutconfiguré dans le worker de livraison) - Le body de la réponse est ignoré
- Recommandé de répondre rapidement (
200 OKimmédiat) et traiter l'événement de façon asynchrone de votre côté ; les délais longs réduisent le throughput et augmentent la chance de retries
Étapes suivantes
- Enregistrer Webhook -- créer, lister et supprimer des webhooks
- Payloads des événements -- exemples de chaque type d'événement