Webhooks
Prérequis
- Un compte marchand Paylet.
- Un endpoint HTTPS public capable de recevoir des
POSTJSON. - Un jeton Bearer (connexion dashboard) pour gérer les abonnements.
S'abonner aux événements
Un abonnement webhook est un endpoint (URL) associé à une liste d'événements. Créez-le avec votre jeton Bearer :
curl -X POST https://api.paylet.fr/v1/webhook-endpoints \
-H "Authorization: Bearer $PAYLET_JWT" \
-H "Content-Type: application/json" \
-d '{
"url": "https://exemple.com/webhooks/paylet",
"events": ["transaction.settled", "topup.completed"]
}'Réponse 201 :
{
"webhook_endpoint": {
"id": "<uuid>",
"url": "https://exemple.com/webhooks/paylet",
"events": ["transaction.settled", "topup.completed"],
"secret": "whsec_<secret-hmac>",
"status": "active",
"created_at": "2026-08-21T12:00:00Z"
}
}Le secret n'est renvoyé qu'une fois
Le champ secret sert à vérifier la signature des envois. Conservez-le précieusement : il n'est renvoyé qu'à la création.
Pour lister ou supprimer un abonnement :
curl https://api.paylet.fr/v1/webhook-endpoints \ -H "Authorization: Bearer $PAYLET_JWT"
Événements disponibles
| Événement | Déclencheur | Champs clés de data |
|---|---|---|
transaction.settled | Un paiement M2M a été réglé. | transaction_id, amount_minor, status, settled_at |
topup.completed | Un rechargement Stripe a crédité le compte. | topup_id, amount_minor, currency |
balance.updated | Le solde a bougé (optionnel, sur abonnement). | solde courant après mouvement |
Exemples de charges utiles
{
"event": "transaction.settled",
"data": {
"transaction_id": "<uuid>",
"amount_minor": 1500000,
"currency": "EUR",
"status": "settled",
"settled_at": "2026-08-21T12:00:00Z"
}
}{
"event": "topup.completed",
"data": {
"topup_id": "<uuid>",
"amount_minor": 1000000,
"currency": "EUR"
}
}Vérifier la signature HMAC
Chaque envoi est signé : Paylet ajoute l'en-tête X-Webhook-Signature contenant le HMAC-SHA256 du corps brut (octets exacts, non reformatés), calculé avec votre secret. Vérifiez toujours cette signature avant de traiter l'événement.
import hashlib
import hmac
import os
from fastapi import FastAPI, HTTPException, Request
app = FastAPI()
WEBHOOK_SECRET = os.environ["PAYLET_WEBHOOK_SECRET"].encode("utf-8")
@app.post("/webhooks/paylet")
async def paylet_webhook(request: Request):
body = await request.body()
signature = request.headers.get("X-Webhook-Signature", "")
# HMAC-SHA256 du corps brut, comparé en temps constant.
expected = hmac.new(WEBHOOK_SECRET, body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, signature):
raise HTTPException(status_code=400, detail="signature invalide")
payload = await request.json()
print(payload["event"], payload["data"])
return {"received": True}Précision sur l'encodage
L'en-tête transporte la signature en hexadécimal, convention identique à celle des primitives de signature du SDK (sign_payload / signPayload). Utilisez le même encodage des deux côtés pour que les signatures coïncident.
Erreurs courantes
| code | HTTP | Signification |
|---|---|---|
| invalid_request | 400 | URL non HTTPS en production, ou corps malformé. |
| authentication_required | 401 | Jeton Bearer absent ou expiré. |
| not_found | 404 | Endpoint webhook inexistant. |