Enregistrer Webhook
Endpoints pour créer, lister et supprimer des webhooks de notification.
Créer un webhook
POST /api/external/webhooksHeaders
| Header | Type | Obligatoire | Description |
|---|---|---|---|
Authorization | String | Oui | ApiKey {client_id}:{client_secret} |
Content-Type | String | Oui | application/json |
hmac | String | Oui | Signature HMAC-SHA512 du body (en savoir plus) |
Request Body
| Champ | Type | Obligatoire | Défaut | Description |
|---|---|---|---|---|
url | String | Oui | -- | URL pour recevoir les notifications (HTTPS par défaut) |
events | Array | Oui | -- | Liste des événements à souscrire. Doit être un array non-vide avec au moins un événement valide du tableau ci-dessous. Omettre le champ retourne 400 {"errors": {"events": ["can't be blank"]}}. |
secret | String | Non | auto-généré | Clé pour signature HMAC-SHA256 des livraisons. Si omis, une valeur aléatoire est générée automatiquement. |
description | String | Non | null | Description libre du webhook pour identification interne |
allow_insecure | Boolean | Non | false | Permet les URLs HTTP (non-HTTPS). La sécurité des données est la responsabilité du client. |
Événements disponibles (uniquement PIX — les autres produits ne sont pas dans le périmètre d'Owem aujourd'hui) :
| É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 (worker toutes les 5 min) | 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) | Actif |
pix.payout.processing | processing | PIX envoyé, en attente de confirmation | Actif |
pix.payout.confirmed | settled | PIX envoyé et confirmé (terminal) | Actif |
pix.payout.failed | rejected | PIX envoyé rejeté (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 | 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 | Actif |
webhook.test | test | Test manuel. Disponible uniquement via portail Admin/Merchant — il n'y a pas d'endpoint External API pour déclencher | Déclenchement manuel |
pix.charge.cancelled n'est pas encore déclenché
L'événement est dans l'enum des abonnements valides, mais aucun code en production ne déclenche de notifications de ce type aujourd'hui (flux d'annulation de QR code non implémenté). Vous pouvez l'inclure dans le tableau events — l'API l'accepte — mais aucune notification n'arrivera à votre endpoint tant que le flux ne sera pas implémenté.
Tout événement hors de ce tableau est rejeté
À la création d'un webhook, validate_events dans le backend vérifie chaque item contre l'enum @valid_events (schéma Fluxiq.Schemas.Webhooks.Webhook). Les événements inconnus (boleto.paid, account.created, sta.file.*, etc.) retournent 400 avec l'erreur events: contains invalid events: .... Avant la session 163, ces noms étaient dans l'enum comme aspirationnels — ils ont été supprimés car il n'y avait pas de dispatchers en code.
Payloads de chaque événement
Des exemples de payload complets pour chaque événement sont dans Payloads des webhooks.
Exemple
BODY='{"url":"https://votresite.com.br/webhook","events":["pix.charge.paid","pix.payout.confirmed"]}'
HMAC=$(echo -n "$BODY" | openssl dgst -sha512 -hmac "$CLIENT_SECRET" | awk '{print $2}')
curl -X POST https://api.owem.com.br/api/external/webhooks \
-H "Authorization: ApiKey $CLIENT_ID:$CLIENT_SECRET" \
-H "Content-Type: application/json" \
-H "hmac: $HMAC" \
-d "$BODY"Réponse de succès (201)
{
"worked": true,
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"url": "https://votresite.com.br/webhook",
"events": ["pix.charge.paid", "pix.payout.confirmed"],
"secret": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
"description": null,
"is_active": true,
"created_at": "2026-03-07T15:30:00Z"
}Format du id
Le id du webhook est un UUID v4 canonique (36 caractères avec tirets). Utilisez cette valeur directement dans DELETE /api/external/webhooks/:id.
Réponse d'erreur (422)
{
"worked": false,
"detail": "URL deve utilizar HTTPS"
}Uniquement HTTPS par défaut
L'URL du webhook doit utiliser HTTPS. Les URLs HTTP seront rejetées, sauf si allow_insecure: true est envoyé à l'enregistrement.
Important — Secret du webhook
Le champ secret retourné dans la réponse d'enregistrement est la clé utilisée pour signer les livraisons du webhook (HMAC-SHA256). Stockez cette valeur en sécurité dès réception — c'est elle qui valide qu'une notification vient réellement d'Owem Pay.
Ne confondez pas avec client_secret :
client_secret= authentification de vos requêtes à l'API (header Authorization)secretdu webhook = vérification de la signature des livraisons reçues (header X-Owem-Signature)
Si vous n'envoyez pas le champ secret à l'enregistrement, une valeur aléatoire sera générée automatiquement et retournée dans la réponse.
Voir Validation de webhooks pour des exemples de comment vérifier la signature.
Récupérer le secret plus tard
Le secret est retourné à la fois dans POST /api/external/webhooks (création) et dans GET /api/external/webhooks et GET /api/external/webhooks/:id (consultation). Si vous avez perdu la valeur, il suffit de consulter le webhook à nouveau via GET.
Dans une future version, ce comportement pourrait être restreint (afficher uniquement à la création) ; nous recommandons de stocker le secret dans un secret manager (vault, SSM, etc.) au moment de l'enregistrement.
URLs HTTP
Par défaut, les webhooks exigent HTTPS pour garantir la sécurité des données en transit. Pour utiliser HTTP, envoyez allow_insecure: true lors de l'enregistrement du webhook.
Attention
Les URLs HTTP transmettent les données sans chiffrement. La sécurité et la confidentialité des informations transitées sont sous l'entière responsabilité du client. Owem Pay effectue la livraison du webhook normalement, mais ne se responsabilise pas en cas d'interception ou de fuite de données sur des connexions non chiffrées.
Les URLs privées sont toujours bloquées
Même avec allow_insecure: true, les URLs pointant vers des adresses privées/internes sont rejetées :
localhost/127.x.x.x- RFC1918 :
10.x.x.x,192.168.x.x,172.16-31.x.x - TLDs internes :
.local,.internal
Les webhooks doivent pointer vers des URLs publiques accessibles sur internet.
Lister les webhooks
GET /api/external/webhooksHeaders
| Header | Type | Obligatoire | Description |
|---|---|---|---|
Authorization | String | Oui | ApiKey {client_id}:{client_secret} |
Exemple
curl -X GET https://api.owem.com.br/api/external/webhooks \
-H "Authorization: ApiKey $CLIENT_ID:$CLIENT_SECRET"Réponse de succès (200)
Retourne un tableau d'objets (non enveloppé dans {"worked": true}). Chaque item contient les mêmes champs que l'enregistrement, y compris secret.
[
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"url": "https://votresite.com.br/webhook",
"events": ["pix.charge.paid", "pix.payout.confirmed"],
"description": null,
"account_id": 10014,
"is_active": true,
"allow_insecure": false,
"status": "active",
"secret": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
"created_at": "2026-03-07T15:30:00",
"updated_at": "2026-03-07T15:30:00"
}
]| Champ | Type | Description |
|---|---|---|
id | string (UUID) | Identifiant du webhook |
url | string | URL de destination |
events | array | Événements souscrits |
description | string ou null | Description optionnelle |
account_id | integer ou null | Compte associé. null = webhook global pour l'API key (si supporté) |
is_active | boolean | false = webhook en pause, aucune delivery n'est déclenchée |
allow_insecure | boolean | true = URLs HTTP acceptées |
status | string | Dérivé de is_active — "active" ou "inactive" |
secret | string | Clé HMAC-SHA256 utilisée pour signer les livraisons. Retournée dans le LIST pour permettre la récupération si le client a perdu la valeur originale |
created_at / updated_at | string ISO 8601 | Timestamps en UTC, format NaiveDateTime (sans suffixe Z, ex. : "2026-03-07T15:30:00"). Différent des autres champs de date dans les payloads de webhook (paid_at, returned_at, expired_at) qui utilisent DateTime ISO 8601 avec Z final. Supposez toujours UTC pour les champs de l'objet webhook |
Supprimer un webhook
DELETE /api/external/webhooks/:idHeaders
| Header | Type | Obligatoire | Description |
|---|---|---|---|
Authorization | String | Oui | ApiKey {client_id}:{client_secret} |
Path Parameters
| Paramètre | Type | Obligatoire | Description |
|---|---|---|---|
id | String (UUID) | Oui | ID du webhook (UUID v4 retourné par l'endpoint de création) |
Exemple
curl -X DELETE https://api.owem.com.br/api/external/webhooks/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
-H "Authorization: ApiKey $CLIENT_ID:$CLIENT_SECRET"Réponse de succès (204)
HTTP 204 No Content — body vide. Le webhook a été supprimé avec succès ; aucune delivery en attente ne sera déclenchée.
Premier appel : 204. Suivants : 404
Le premier appel réussi retourne 204 No Content. Les appels suivants avec le même id retournent 404 { "errors": { "not_found": "webhook not found" } } — le webhook a déjà été supprimé. Ce n'est pas une idempotence stricte HTTP (où tout appel retournerait 204) — c'est le comportement standard de DELETE quand la ressource cesse d'exister. Écrivez votre intégration pour accepter à la fois 204 et 404 comme « le webhook n'est plus actif ».
Réponse d'erreur (400)
Format de id invalide (pas un UUID) :
{
"errors": {
"bad_request": "id must be a valid UUID"
}
}Réponse d'erreur (404)
Le webhook n'existe pas ou n'appartient pas à votre compte :
{
"errors": {
"not_found": "webhook not found"
}
}