Guides
Recevoir des webhooks
Recevez les événements sur votre serveur et vérifiez qu'Abuna les a envoyés.
Abuna envoie un POST HTTPS à votre serveur quand quelque chose se produit dans votre application, comme une facture payée ou un abonnement annulé. Chaque requête est signée avec votre clé de signature webhook, pour que vous puissiez vérifier qu'elle vient d'Abuna.
Ajoutez un point de terminaison
Dans le tableau de bord, ouvrez Webhooks et définissez l'URL du point de terminaison. Seul un propriétaire d'équipe peut la modifier, et uniquement depuis le tableau de bord. Une clé secrète ne peut pas rediriger vos webhooks.
Le mode Test et le mode Réel ont chacun leur propre point de terminaison et leur propre clé de signature. Une nouvelle application Test ne copie pas le point de terminaison Réel, pour que les événements Test n'atteignent jamais un serveur destiné à de vrais clients.
L'URL du point de terminaison doit :
- Utiliser
https. - Avoir un hôte, et aucun nom d'utilisateur ni mot de passe dedans.
- Ne pas pointer vers
localhost, ni vers une adresse privée, de bouclage, link-local ou de NAT opérateur. Abuna vérifie l'adresse à laquelle elle se connecte à chaque requête et à chaque redirection, pas seulement l'URL que vous enregistrez.
Pour recevoir des événements sur votre propre machine, exposez-la via un tunnel HTTPS public et utilisez cette URL. Le Démarrage rapide contient une commande pour cela.
Obtenez votre clé de signature
Abuna affiche chaque clé de signature une seule fois : à la création de l'application, et quand vous renouvelez la clé. Une clé Test ressemble à whsec_test_... et une clé Réelle à whsec_live_.... Conservez-la sur votre serveur, à côté de votre clé secrète.
Ce qu'Abuna envoie
Chaque livraison est un corps JSON avec ces en-têtes :
X-Abuna-Event: le type d'événement, commeinvoice.paid.X-Abuna-Delivery: l'identifiant de cette livraison. Les nouvelles tentatives d'une livraison le gardent. Un renvoi en obtient un nouveau.X-Abuna-Environment:testoulive.X-Abuna-Signature: quand Abuna a signé la requête, et la signature. Voyez Vérifier la signature.
Le corps a toujours les cinq mêmes champs. id est l'identifiant de l'événement. data dépend du type. Le mode figure dans le corps comme dans l'en-tête, car seul le corps est signé.
{
"created_at": 1791143442,
"data": {
"amount": 5000,
"credit": 0,
"currency": "XAF",
"invoice_id": "hUPYtkyo",
"invoice_number": "ACME-000001",
"kind": "period",
"metadata": {
"user_id": "42"
},
"paid_at": 1791143442,
"period_end": 1793821842,
"period_id": "tKhKXYjr",
"period_start": 1791143442,
"subscription": {
"activated_at": 1791143442,
"app_id": "VyYjSLf1",
"cancel_at": null,
"canceled_at": null,
"created_at": 1791143442,
"current_period_end": 1793821842,
"current_period_start": 1791143442,
"customer_id": "OljRiqBE",
"ended_reason": null,
"id": "coRNVNUz",
"manage_token": "d6f6ca3a8b285784aed37bd593bab9dea12e",
"metadata": {
"user_id": "42"
},
"price_id": "UKjuDVyN",
"starts_at": 1791143442,
"status": "active"
},
"subscription_id": "coRNVNUz"
},
"environment": "test",
"id": "01M447FXSP98350QH3TKE3YG6D",
"type": "invoice.paid"
}Vérifiez la signature
X-Abuna-Signature contient des paires séparées par des virgules. t est le moment où Abuna a signé la requête, en secondes Unix. Chaque v1 est un HMAC-SHA256 hexadécimal, calculé avec votre clé de signature, sur t, un point, et le corps brut : "<t>.<raw body>".
X-Abuna-Signature: t=1727600000,v1=5f2b8c1e9a4d7f3b6e0c8a2d5f9b1e4c7a3d6f0b8e2c5a9d1f4b7e3c6a0d8f2bPour chaque requête :
- Lisez le corps brut, en octets, avant de l'analyser.
- Séparez l'en-tête sur les virgules, et chaque paire sur le premier
=. Gardeztet chaquev1. - Si
test à plus de 300 secondes avant ou après l'horloge de votre serveur, répondez400. Cela empêche de rejouer une ancienne requête. - Calculez le HMAC-SHA256 de
t, d'un., et du corps brut avec votre clé de signature, et encodez-le en hexadécimal. - Comparez le résultat avec chaque
v1, avec une comparaison à temps constant. Acceptez la requête si l'un d'eux correspond. - Si aucun ne correspond, répondez
400et ignorez l'événement.
L'en-tête a généralement un seul v1. Pendant qu'un renouvellement de la clé se chevauche, il en a deux : un fait avec la nouvelle clé et un avec l'ancienne. Accepter toute correspondance garde votre point de terminaison fonctionnel, quelle que soit la clé que détient votre serveur. Ignorez les paires que vous ne connaissez pas, pour qu'un nouveau schéma de signature ajouté plus tard ne vous casse pas.
import crypto from "node:crypto";
import http from "node:http";
// header is "t=1727600000,v1=<hex>", with a second v1 while a secret rotation overlaps.
function verifyAbunaSignature(rawBody, header, secret) {
const pairs = (header ?? "").split(",").map((part) => part.split("="));
const timestamp = pairs.find(([key]) => key === "t")?.[1] ?? "";
if (!/^\d+$/.test(timestamp) || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = crypto.createHmac("sha256", secret).update(timestamp + ".").update(rawBody).digest();
return pairs.some(([key, value]) => {
const received = Buffer.from(key === "v1" ? value ?? "" : "", "hex");
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
});
}
http
.createServer((req, res) => {
const chunks = [];
req.on("data", (chunk) => chunks.push(chunk));
req.on("end", () => {
const rawBody = Buffer.concat(chunks);
if (!verifyAbunaSignature(rawBody, req.headers["x-abuna-signature"], process.env.ABUNA_WEBHOOK_SECRET)) {
res.writeHead(400).end();
return;
}
const event = JSON.parse(rawBody);
console.log("Received", event.type, event.id);
res.writeHead(200).end();
});
})
.listen(3000);Obtenez le corps brut dans votre framework
Dans Express, express.json() remplace le corps par du JSON analysé. Utilisez express.raw({ type: "application/json" }) sur la route du webhook, pour que req.body soit les octets qu'Abuna a envoyés.
import express from "express";
const app = express();
// express.raw keeps the bytes. Mount it on the webhook route, before any express.json().
app.post("/webhooks/abuna", express.raw({ type: "application/json" }), (req, res) => {
if (!verifyAbunaSignature(req.body, req.get("X-Abuna-Signature"), process.env.ABUNA_WEBHOOK_SECRET)) {
return res.sendStatus(400);
}
const event = JSON.parse(req.body);
console.log("Received", event.type, event.id);
res.sendStatus(200);
});
app.listen(3000);Dans Laravel, $request->getContent() renvoie le corps brut. Passez-le à la fonction PHP ci-dessus.
<?php
// routes/api.php. API routes have no CSRF check, so Abuna's POST reaches the handler.
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::post('/webhooks/abuna', function (Request $request) {
// getContent() returns the raw body, before Laravel decodes the JSON.
$rawBody = $request->getContent();
if (!verify_abuna_signature($rawBody, $request->header('X-Abuna-Signature', ''), config('services.abuna.webhook_secret'))) {
abort(400);
}
$event = json_decode($rawBody, true);
logger('Received ' . $event['type'] . ' ' . $event['id']);
return response()->noContent();
});Répondez avec un 2xx
Une livraison réussit quand votre point de terminaison répond avec un statut 2xx en 10 secondes. Abuna suit jusqu'à 4 redirections, et vérifie chacune selon les règles d'URL ci-dessus. Toute autre réponse, un délai dépassé ou une erreur de connexion est une tentative échouée.
Abuna essaie une livraison jusqu'à 10 fois. Après une tentative échouée, il attend environ 1, 3, 9, 27, 81 et 243 minutes, puis 6 heures entre chacune des dernières tentatives, à 10 % près. Les tentatives couvrent environ une journée. Après la 10e tentative échouée, la livraison est marquée failed et n'est plus réessayée.
Pour traiter les événements sans risque :
- Répondez d'abord, puis faites le travail lent en arrière-plan, pour rester dans les 10 secondes.
- Enregistrez l'
idde chaque événement et sautez celui que vous avez déjà traité. Les nouvelles tentatives et les renvois portent le même événement. - Ne vous fiez pas à l'ordre. Abuna envoie les livraisons en parallèle, et une nouvelle tentative peut arriver après un événement plus récent. Voyez Gérer les événements dans le désordre.
Gérez les événements dans le désordre
Les identifiants d'événement se trient dans l'ordre où les événements ont eu lieu, alors comparez-les comme de simples chaînes pour savoir lequel est venu en premier. N'utilisez pas created_at : il est en secondes entières, et les événements d'un même changement le partagent souvent. Un paiement, par exemple, enregistre invoice.paid et subscription.recovered ensemble, dans cet ordre. Voyez Ordre des événements pour chaque changement.
Chaque événement au sujet d'un abonnement porte l'abonnement entier dans data.subscription, tel qu'il est juste après cet événement. Vous n'avez pas besoin de rejouer les événements dans l'ordre pour connaître l'état d'un abonnement :
- Stockez, pour chaque abonnement, l'identifiant du plus récent événement que vous lui avez appliqué.
- Quand un événement arrive, comparez son
idavec celui stocké. S'il est plus récent, enregistrezdata.subscriptioncomme état de l'abonnement et stockez le nouvel identifiant. - S'il est plus ancien, un état plus récent est déjà enregistré. Gardez l'état stocké, et faites quand même le travail ponctuel que l'événement demande, comme un reçu.
Donnez accès au client tant que data.subscription.status est active ou past_due, et retirez-le à canceled. Un renouvellement que le client n'a pas payé à la fin de la période payée est past_due jusqu'à ce qu'il le paie ou que le délai de paiement s'épuise.
Types d'événement
session.completed: un client a terminé une session.data.typedit laquelle : pourcheckout, le premier paiement est passé etdata.subscription_idest le nouvel abonnement.data.metadataest ce que vous avez passé à sa création.session.expired: une session a expiré ou vous l'avez expirée, sans se terminer.subscription.created: un abonnement a démarré et attend son premier paiement.subscription.paid: le premier paiement est passé et l'abonnement est actif.subscription.renewed: la période de facturation suivante et sa facture ont été émises, par défaut 3 jours avant la fin de la période payée. La nouvelle période commence quand la période payée se termine.subscription.past_due: la période payée s'est terminée et le renouvellement n'est pas encore payé. Un renouvellement payé avant la fin de la période ne l'envoie jamais. Le client garde l'accès jusqu'à la fin de l'abonnement.subscription.recovered: le renouvellement impayé a été payé, et l'abonnement est de nouveauactive. Il arrive aussi quand un paiement qui arrive après une annulation pour non-paiement ramène l'abonnement.subscription.updated: une annulation à la fin de la période payée a été programmée ou annulée, le tarif a changé, une rétrogradation a été programmée ou annulée, ou une mise à niveau en attente de paiement a expiré.data.cancel_atest la date de fin, ounull.data.price_idest le tarif actuel, etdata.changedit ce qui a changé au tarif.data.updated_byestmerchantoucustomer.plan_offer.created,plan_offer.confirmed,plan_offer.canceled,plan_offer.expired: un changement de formule en attente du client a été créé, accepté, retiré ou remplacé, ou a expiré.subscription.canceled: l'abonnement a pris fin, y compris à la fin de la période payée.data.reasonestmerchant,customer,non_paymentoucheckout_expired.invoice.created: une facture a été émise pour une période de facturation.data.kindestplan_changepour la première période d'une mise à niveau.invoice.paid: une facture a été payée.invoice.payment_failed: un paiement sur une facture n'est pas passé.price_change.scheduled,price_change.canceled,price_change.applied: un nouveau montant pour un tarif a été programmé, annulé ou remplacé, ou a pris effet le jour prévu.data.reasonsurprice_change.canceledestmerchantoureplaced.customer.updated: le nom, l'e-mail ou le numéro de téléphone d'un client a changé.data.updated_byestmerchantoucustomer.
Les événements d'abonnement, de facture et d'offre de formule portent les métadonnées de l'abonnement. Les événements de session portent les metadata de la session elle-même, pas celles de l'abonnement. Passez des métadonnées à chaque session que vous créez, pour que ses événements puissent identifier votre utilisateur. Sans elles, les événements de session portent {}.
Chaque événement est aussi conservé dans la liste d'événements de votre application, même sans point de terminaison défini. Voyez Événements pour le data de chaque type.
Chaque événement dont le data a un subscription_id a aussi data.subscription, l'objet abonnement complet tel qu'il est juste après l'événement.
Envoyez un événement test
Pour vérifier votre point de terminaison, cliquez sur Envoyer un événement test dans le tableau de bord, ou appelez POST/v1/apps/{appID}/webhooks/test. Abuna envoie tout de suite un événement ping signé et vous dit comment votre point de terminaison a répondu. Il est essayé une seule fois, et il n'est enregistré ni comme événement ni comme livraison.
curl -X POST https://api.abuna.app/v1/apps/8ddhXCDW/webhooks/test \
-H "Authorization: Bearer sk_test_..."{
"delivered": true,
"response_status": 200,
"error": null
}delivered vaut true quand votre point de terminaison a répondu 2xx. Si aucune réponse n'est revenue, response_status est null et error dit pourquoi. Vous pouvez envoyer 10 événements test par minute. Sans point de terminaison, l'appel renvoie 422 avec le code webhook_url_missing.
{
"id": "01J9ZQ8E1F2G3H4J5K6M7N8P9Q",
"type": "ping",
"environment": "test",
"created_at": 1727600000,
"data": {
"message": "A test event sent from the Abuna dashboard."
}
}Renvoyez les événements échoués
Une fois votre point de terminaison réparé, listez les livraisons échouées avec GET/v1/apps/{appID}/webhooks?status=failed, puis renvoyez chacune avec POST/v1/apps/{appID}/webhooks/{id}/redeliver. Un renvoi envoie à nouveau le même corps, comme une nouvelle livraison, vers votre URL de point de terminaison actuelle. Il est signé avec votre clé actuelle et réessayé comme n'importe quel autre.
curl "https://api.abuna.app/v1/apps/8ddhXCDW/webhooks?status=failed&limit=100" \
-H "Authorization: Bearer sk_test_..."
curl -X POST https://api.abuna.app/v1/apps/8ddhXCDW/webhooks/R3vH6sLd/redeliver \
-H "Authorization: Bearer sk_test_..."Voyez Livraisons de webhooks pour les champs de chaque livraison. Pour trouver les événements que votre serveur n'a jamais traités, quel que soit leur statut de livraison, parcourez la liste d'événements. Voyez Rattraper les événements manqués.
Renouvelez la clé de signature
Si votre clé fuite, un propriétaire d'équipe peut cliquer sur Renouveler la clé secrète sur la page Webhooks. Vous ne pouvez pas la renouveler avec une clé secrète. Abuna affiche la nouvelle clé une seule fois et signe avec elle tout de suite.
Pendant les 24 heures suivantes, X-Abuna-Signature porte un second v1, signé avec l'ancienne clé. Mettez la nouvelle clé sur votre serveur dans ce délai. Si vous renouvelez encore une fois dans les 24 heures, la clé que vous venez de remplacer devient l'ancienne, et celle d'avant cesse de fonctionner.
Étapes suivantes
- ÉvénementsLe journal de tout ce qui s'est passé dans une application, et chaque type d'événement.
- Livraisons de webhooksVoyez chaque webhook envoyé à votre serveur, renvoyez-en un, ou envoyez un test.
- Liste de contrôle avant le passage en RéelTout ce qu'il faut vérifier avant d'encaisser de vrais paiements.