Guides
Sessions
Envoyez un client depuis votre application pour s'abonner, payer une facture, changer de formule, annuler ou gérer sa facturation, ramenez-le, et sachez avec certitude quand il a terminé.
Une session est une page hébergée que votre serveur crée pour un client. Le client agit sur la page d'Abuna et revient sur votre site. Abuna envoie ensuite à votre serveur un webhook qui porte vos propres metadata, pour que vous sachiez de quel utilisateur il s'agissait.
Actions et types de session
Chaque action client utilise un seul endpoint, POST/v1/apps/{appID}/sessions. Le type que vous envoyez choisit l'action. Chaque type renvoie le même objet session et envoie les mêmes événements session.completed et session.expired, avec le type de la session dedans.
| Action | À envoyer avec | Type | Se termine quand | Expire | Événements sur lesquels compter |
|---|---|---|---|---|---|
| Vendre un tarif à un nouvel abonné | price_id | checkout | Le premier paiement réussit. | 24 heures après la création. | session.completed, et subscription.paid pour un paiement tardif. |
| Faire payer une facture ouverte à un client | invoice_id | payment | Cette facture est payée via la session. | 24 heures, ou plus tôt si la facture ne peut plus être payée par son intermédiaire. | invoice.paid |
| Faire confirmer à un client le passage à un autre tarif | subscription_id, price_id | plan_change | Le changement est confirmé, et payé s'il coûte quelque chose. | 7 jours, ou la fin de la période en cours si elle arrive plus tôt. | subscription.updated |
| Faire confirmer une annulation à un client | subscription_id | cancel | Le client confirme. | 24 heures après la création. | subscription.updated pour une annulation programmée, subscription.canceled quand elle prend fin. |
| Envoyer un client gérer son abonnement, avec un lien de retour vers votre application | subscription_id, return_url | portal | Le client l'ouvre pour la première fois. | 1 heure après la création, s'il ne l'a pas ouverte. | Les événements de ce que le client fait dans l'espace client. |
Vérifiez data.type dans votre gestionnaire de webhook, pour que chaque type atteigne le bon code.
Comment ça marche
- Votre serveur crée une session du type de l'action avec votre clé secrète et récupère une session avec une
url. - Vous redirigez le client vers cette
url. - Le client agit sur la page : il s'abonne, paie une facture, confirme un changement de formule ou une annulation, ou gère son abonnement dans l'espace client.
- Abuna ramène le client vers votre URL de succès, ou votre URL de retour pour une session d'espace client.
- Abuna envoie
session.completedà votre point de terminaison de webhook. Vous agissez dessus quand il arrive.
Les sections suivantes détaillent le démarrage d'un abonnement. Les autres actions suivent les mêmes étapes.
Démarrez un abonnement depuis votre serveur
Créez la session depuis votre serveur avec votre clé secrète, dans l'en-tête Authorization. N'appelez jamais cela depuis un navigateur ou une application mobile. Une clé publiable ou une connexion au tableau de bord ne peut pas créer de session : les deux obtiennent 403 avec le code forbidden.
Appelez Créer une session de paiement avec type checkout et le price_id à vendre. Mettez votre propre identifiant d'utilisateur dans metadata. Abuna le copie sur l'abonnement. Les événements d'abonnement portent ces métadonnées ; les événements de session portent les métadonnées passées sur cette session. Passez-les à nouveau quand vous créez une autre session.
curl https://api.abuna.app/v1/apps/8ddhXCDW/sessions \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{
"type": "checkout",
"price_id": "Zt6YbN3q",
"customer": {"email": "ana@example.com"},
"success_url": "https://example.com/welcome",
"cancel_url": "https://example.com/pricing",
"metadata": {"user_id": "42"}
}'{
"id": "A4kN1oSs",
"type": "checkout",
"status": "open",
"url": "https://app.abuna.app/checkout/7f2c9e4a1b8d3f6e0c5a9d2b7e4f1a8c3d6b",
"success_url": "https://example.com/welcome",
"cancel_url": "https://example.com/pricing",
"return_url": null,
"metadata": {
"user_id": "42"
},
"customer_id": null,
"price_id": "Zt6YbN3q",
"subscription_id": null,
"invoice_id": null,
"plan_offer_id": null,
"at_period_end": null,
"expires_at": 1727686400,
"completed_at": null,
"created_at": 1727600000
}La réponse a le statut 201 et une session checkout. La session est open et expire 24 heures après sa création, à expires_at. Créer une session de paiement liste chaque paramètre et chaque erreur.
Préremplissez le client
Si vous savez déjà qui paie, dites-le à Abuna, et le client ne le saisit pas à nouveau. Envoyez l'un de ces deux, pas les deux :
customer_id: un client existant dans l'application. La page de paiement utilise son nom, son e-mail et son numéro de téléphone.customer: l'un deemail,nameetphone_number. Écrivez le numéro de téléphone avec son indicatif pays, comme+237671234567.
La page de paiement affiche chaque champ prérempli verrouillé, pour que le client ne puisse pas le modifier. Il remplit le reste. Un client a besoin d'une adresse e-mail et d'un numéro de téléphone pour s'abonner.
Envoyez le client vers la session
Redirigez le navigateur du client vers l'url de la session. Créez une nouvelle session chaque fois qu'un client démarre un paiement. L'url appartient à un seul client, alors ne la publiez nulle part en public. Pour un lien que tout le monde peut utiliser, utilisez plutôt un lien de paiement.
import http from "node:http";
http
.createServer(async (req, res) => {
if (req.method === "POST" && req.url === "/subscribe") {
const user = await currentUser(req);
const session = await createCheckoutSession(user); // the request above
res.writeHead(303, { Location: session.url }).end();
return;
}
res.writeHead(404).end();
})
.listen(3000);Sur la page, le client remplit ses informations. Abuna crée le client et un abonnement pending, définit customer_id et subscription_id de la session, et affiche l'étape de paiement sur la même page. Une fois le premier paiement réussi, l'abonnement devient active et la session devient completed, dans la même étape.
Ouvrir à nouveau l'url montre où le client s'est arrêté. Cela ne crée jamais un second abonnement.
Gérez le retour
Les sessions de paiement, de facture et les mises à niveau payées affichent un bouton « Retour » après le paiement. Les sessions d'annulation et les changements de formule sans paiement dû reviennent directement à votre URL de succès. Les sessions d'espace client affichent un lien « Retour » vers votre URL de retour.
Une fois le paiement passé, le client voit un écran de fin avec un lien vers votre URL de succès. Abuna choisit l'URL dans cet ordre :
- La
success_urlque vous avez passée à la création de la session. - L'URL de succès de votre application, sous Paramètres, Général, URL de redirection. Abuna la copie sur la session à sa création, donc changer le réglage plus tard ne modifie pas les sessions existantes.
- Sans l'une ni l'autre, le client reste sur l'écran de fin d'Abuna.
Abuna ajoute les identifiants de session et d'abonnement à l'URL de succès de l'une de deux façons :
- Si l'URL contient
{SESSION_ID}ou{SUBSCRIPTION_ID}, Abuna remplace chacun par l'identifiant. - Sinon, Abuna ajoute les paramètres de requête
session_idetsubscription_id. Il conserve vos propres paramètres de requête et le fragment#.
https://example.com/welcome/{SESSION_ID}
https://example.com/welcome/A4kN1oSs
https://example.com/welcome?plan=pro#start
https://example.com/welcome?plan=pro&session_id=A4kN1oSs&subscription_id=x9QbL2sK#startL'URL d'annulation fonctionne de la même façon : la cancel_url que vous avez passée, ou l'URL d'annulation de votre application. La page de paiement y renvoie un client qui part avant de payer, et depuis la page d'une session expirée. Abuna n'y ajoute rien.
Sur votre page de succès, vous pouvez lire la session avec GET/v1/apps/{appID}/sessions/{id} pour afficher le bon message.
const url = new URL(req.url, "https://example.com");
const sessionId = url.searchParams.get("session_id");
const response = await fetch(
"https://api.abuna.app/v1/apps/8ddhXCDW/sessions/" + encodeURIComponent(sessionId),
{ headers: { Authorization: "Bearer sk_test_..." } },
);
const session = await response.json();
// The ID came from the URL, so check the session is this user's.
if (!response.ok || session.metadata.user_id !== String(user.id)) {
return showPage("pricing");
}
if (session.status === "completed") {
return showPage("welcome");
}
// Paid but not confirmed yet, or a mobile money approval still pending.
return showPage("confirming-your-payment");Accordez l'accès sur le webhook, pas sur la redirection
N'importe qui peut saisir votre URL de succès dans un navigateur, et un client peut fermer l'onglet avant d'y arriver. Accordez l'accès quand session.completed atteint votre point de terminaison de webhook. Abuna l'envoie une fois le premier paiement réussi, que le client soit revenu ou non.
{
"id": "01J9ZQ8E1F2G3H4J5K6M7N8P9Q",
"type": "session.completed",
"environment": "test",
"created_at": 1727600600,
"data": {
"session_id": "A4kN1oSs",
"type": "checkout",
"subscription_id": "x9QbL2sK",
"customer_id": "Cu5tM8rA",
"price_id": "Zt6YbN3q",
"metadata": {
"user_id": "42"
},
"completed_at": 1727600600,
"subscription": {
"id": "x9QbL2sK",
"app_id": "8ddhXCDW",
"customer_id": "Cu5tM8rA",
"price_id": "Zt6YbN3q",
"status": "active",
"starts_at": 1727600000,
"activated_at": 1727600100,
"canceled_at": null,
"cancel_at": null,
"current_period_start": 1727600000,
"current_period_end": 1730192000,
"ended_reason": null,
"metadata": {
"user_id": "42"
},
"created_at": 1727600000,
"manage_token": "8c1f4a7d2e9b3c6f0a5d8e1b4c7f2a9d3e6b"
}
}
}data.metadata est ce que vous avez passé à la création de la session. data.subscription_id et data.customer_id sont l'abonnement et le client qu'Abuna a créés. Conservez-les à côté de votre utilisateur. data.subscription est tout l'abonnement, désormais active.
Vérifiez l'en-tête X-Abuna-Signature avant de faire confiance au corps. Recevoir des webhooks explique le schéma, et l'exemple ci-dessous l'utilise.
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", async () => {
const rawBody = Buffer.concat(chunks);
if (!verifyAbunaSignature(rawBody, req.headers["x-abuna-signature"], process.env.ABUNA_WEBHOOK_SECRET)) {
res.writeHead(400).end();
return;
}
res.writeHead(200).end();
const event = JSON.parse(rawBody);
if (await alreadyHandled(event.id)) return;
if (event.type === "session.completed" && event.data.type === "checkout") {
const { metadata, subscription_id, customer_id } = event.data;
await grantAccess(metadata.user_id, subscription_id, customer_id);
}
await markHandled(event.id);
});
})
.listen(3000);Payez une facture en retard depuis votre application
Une session payment envoie un client payer une facture ouverte, par exemple depuis un bouton « Payer maintenant » sur votre page de facturation pendant que l'abonnement est past_due. Créez-en une avec type payment et l'identifiant de la facture dans invoice_id. Voyez Créer une session de paiement.
Pour trouver la facture, listez les factures avec status=open, ou conservez l'invoice_id de l'événement invoice.created quand Abuna l'émet. success_url, cancel_url et metadata sont facultatifs, comme pour une session de paiement.
Créez la session
curl https://api.abuna.app/v1/apps/8ddhXCDW/sessions \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{
"type": "payment",
"invoice_id": "V2nC8xEi",
"success_url": "https://example.com/billing/paid",
"cancel_url": "https://example.com/billing",
"metadata": {"user_id": "42"}
}'{
"id": "z8TmY5Pq",
"type": "payment",
"status": "open",
"url": "https://app.abuna.app/pay/3a8d1f6c9e2b5a7d0f4c8e1b6a9d3f7c2e5b",
"success_url": "https://example.com/billing/paid",
"cancel_url": "https://example.com/billing",
"return_url": null,
"metadata": {
"user_id": "42"
},
"customer_id": "Cu5tM8rA",
"price_id": null,
"subscription_id": "x9QbL2sK",
"invoice_id": "V2nC8xEi",
"plan_offer_id": null,
"at_period_end": null,
"expires_at": 1727686400,
"completed_at": null,
"created_at": 1727600000
}L'url de la session est une page de paiement pour cette facture, et invoice_id la nomme. La facture doit être ouverte. Sinon vous obtenez l'une de ces erreurs :
409already_paid: la facture est déjà payée.409invoice_not_open: la facture est annulée, par exemple parce que son abonnement a été annulé.422invalidavec un élémenterrorspourinvoice_id: l'application n'a aucune facture avec cet identifiant.
Redirigez et gérez le retour
Redirigez le client vers l'url. Une fois la facture payée via la session, la session se termine et le client voit un bouton « Retour » vers votre URL de succès. Abuna remplace {SESSION_ID}, {SUBSCRIPTION_ID} et {INVOICE_ID} dedans. Sans ces espaces réservés, il ajoute les paramètres de requête session_id, subscription_id et invoice_id. L'URL d'annulation est le chemin de retour d'un client qui part sans payer.
http
.createServer(async (req, res) => {
const url = new URL(req.url, "https://example.com");
const user = await currentUser(req);
// The "Pay now" button on your billing page posts here.
if (req.method === "POST" && url.pathname === "/billing/pay") {
const session = await createPaymentSession(user); // the request above
res.writeHead(303, { Location: session.url }).end();
return;
}
// success_url: Abuna adds session_id, subscription_id, and invoice_id.
if (req.method === "GET" && url.pathname === "/billing/paid") {
const response = await fetch(
"https://api.abuna.app/v1/apps/8ddhXCDW/sessions/" +
encodeURIComponent(url.searchParams.get("session_id")),
{ headers: { Authorization: "Bearer sk_test_..." } },
);
const session = await response.json();
if (!response.ok || session.metadata.user_id !== String(user.id)) {
return showPage(res, "billing");
}
return showPage(res, session.status === "completed" ? "payment-received" : "confirming-your-payment");
}
res.writeHead(404).end();
})
.listen(3000);Confirmez via le webhook
{
"id": "01J9ZQ8F1G2H3J4K5M6N7P8Q9R",
"type": "session.completed",
"environment": "test",
"created_at": 1727600600,
"data": {
"session_id": "z8TmY5Pq",
"type": "payment",
"invoice_id": "V2nC8xEi",
"subscription_id": "x9QbL2sK",
"customer_id": "Cu5tM8rA",
"metadata": {
"user_id": "42"
},
"completed_at": 1727600600,
"subscription": {
"id": "x9QbL2sK",
"app_id": "8ddhXCDW",
"customer_id": "Cu5tM8rA",
"price_id": "Zt6YbN3q",
"status": "active",
"starts_at": 1727600000,
"activated_at": 1727600100,
"canceled_at": null,
"cancel_at": null,
"current_period_start": 1727600000,
"current_period_end": 1730192000,
"ended_reason": null,
"metadata": {
"user_id": "42"
},
"created_at": 1727600000,
"manage_token": "8c1f4a7d2e9b3c6f0a5d8e1b4c7f2a9d3e6b"
}
}
}Utilisez invoice.paid pour savoir que la facture est réglée. Il se déclenche quelle que soit la façon dont le client a payé. Utilisez session.completed pour savoir que le client a terminé le paiement vers lequel vous l'avez envoyé. Il porte vos metadata.
// Inside the verified webhook handler above.
if (event.type === "invoice.paid") {
// Fires however the invoice was paid: this session, a pay link, or a retry.
await markInvoicePaid(event.data.subscription_id, event.data.invoice_id);
}
if (event.type === "session.completed" && event.data.type === "payment") {
// The customer finished the payment you sent them to.
await notifyUser(event.data.metadata.user_id, "Thanks, your payment went through.");
}Quand une session de paiement expire
Une session de paiement expire après 24 heures, ou plus tôt une fois que sa facture ne peut plus être payée par son intermédiaire : elle a été payée autrement, elle a été annulée, ou l'abonnement a pris fin. Abuna envoie alors session.expired. Une session dont la facture a été payée autrement expire. Elle ne se termine pas, alors n'attendez pas session.completed pour régler la facture.
Les liens de paiement qu'Abuna envoie déjà dans les e-mails et sur Telegram continuent de fonctionner seuls. Une session de paiement ne les change pas.
Ajoutez un bouton de mise à niveau à votre application
Une session plan_change envoie un client confirmer le passage à un autre tarif. Appelez Créer une session de changement de formule avec l'identifiant de l'abonnement dans subscription_id et le nouveau price_id. success_url, cancel_url et metadata sont facultatifs.
Il suit les règles de changement de formule. Le tarif doit être l'une des options de formule de l'abonnement, sinon vous obtenez 422 price_not_eligible. L'abonnement doit être active ou past_due, sinon vous obtenez 409 plan_change_unavailable. Un abonnement inconnu obtient 422 invalid avec un élément errors pour subscription_id. Il remplace toute offre en attente ou session plan_change, et l'ancienne session obtient session.expired. Abuna envoie plan_offer.created, mais n'écrit pas et n'envoie pas de message au client : c'est vous qui le redirigez.
Créez la session
curl https://api.abuna.app/v1/apps/8ddhXCDW/sessions \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{
"type": "plan_change",
"subscription_id": "x9QbL2sK",
"price_id": "Hc8LmV2s",
"success_url": "https://example.com/plan/changed",
"cancel_url": "https://example.com/plan",
"metadata": {"user_id": "42"}
}'{
"id": "G2wP6hCs",
"type": "plan_change",
"status": "open",
"url": "https://app.abuna.app/plan-change/5d2b8e1f4a7c0d3e6b9f2a5c8e1d4b7a0f3c",
"success_url": "https://example.com/plan/changed",
"cancel_url": "https://example.com/plan",
"return_url": null,
"metadata": {
"user_id": "42"
},
"customer_id": "Cu5tM8rA",
"price_id": "Hc8LmV2s",
"subscription_id": "x9QbL2sK",
"invoice_id": null,
"plan_offer_id": "q2WkR5oF",
"at_period_end": null,
"expires_at": 1728204800,
"completed_at": null,
"created_at": 1727600000
}L'url est la page de confirmation. Elle montre la formule actuelle, la nouvelle, et ce que le changement coûte maintenant. price_id est le nouveau tarif, et plan_offer_id est l'offre derrière la session.
Redirigez et gérez le retour
Redirigez le client vers l'url. Le client n'atteint votre URL de succès qu'une fois le changement terminé :
- Mise à niveau : après le paiement, le client peut choisir « Retour » pour revenir sur votre site. Une mise à niveau qui coûte
0le ramène directement quand il confirme. - Rétrogradation : quand il confirme. Elle est programmée pour la fin de la période payée, et le client garde la formule actuelle jusque-là.
Votre URL d'annulation est le lien « Retour » sur les étapes de confirmation et de paiement.
http
.createServer(async (req, res) => {
const url = new URL(req.url, "https://example.com");
const user = await currentUser(req);
// The "Upgrade" button posts here.
if (req.method === "POST" && url.pathname === "/plan/upgrade") {
const session = await createPlanChangeSession(user); // the request above
res.writeHead(303, { Location: session.url }).end();
return;
}
// success_url: the customer confirmed, and paid if the upgrade cost anything.
if (req.method === "GET" && url.pathname === "/plan/changed") {
return showPage(res, "plan-changed");
}
// cancel_url: the "Back to" link on the confirm and pay steps.
if (req.method === "GET" && url.pathname === "/plan") {
return showPage(res, "plans");
}
res.writeHead(404).end();
})
.listen(3000);Confirmez via le webhook
{
"id": "01J9ZQ8G1H2J3K4M5N6P7Q8R9S",
"type": "session.completed",
"environment": "test",
"created_at": 1727600900,
"data": {
"session_id": "G2wP6hCs",
"type": "plan_change",
"subscription_id": "x9QbL2sK",
"customer_id": "Cu5tM8rA",
"metadata": {
"user_id": "42"
},
"completed_at": 1727600900,
"plan_offer_id": "q2WkR5oF",
"previous_price_id": "Zt6YbN3q",
"price_id": "Hc8LmV2s",
"direction": "upgrade",
"effective_at": 1727600900,
"subscription": {
"id": "x9QbL2sK",
"app_id": "8ddhXCDW",
"customer_id": "Cu5tM8rA",
"price_id": "Hc8LmV2s",
"status": "active",
"starts_at": 1727600000,
"activated_at": 1727600100,
"canceled_at": null,
"cancel_at": null,
"current_period_start": 1727600900,
"current_period_end": 1730192900,
"ended_reason": null,
"metadata": {
"user_id": "42"
},
"created_at": 1727600000,
"manage_token": "8c1f4a7d2e9b3c6f0a5d8e1b4c7f2a9d3e6b"
}
}
}direction est upgrade ou downgrade. effective_at est le moment où le nouveau tarif commence : maintenant pour une mise à niveau, la fin de la période payée pour une rétrogradation.
Les événements plan_offer.*, subscription.updated et invoice.* se déclenchent toujours comme pour tout changement de formule. Comptez sur deux d'entre eux :
session.completedpour savoir que le client a terminé le flux que vous avez démarré. Il porte vosmetadata.subscription.updatedpour savoir que le tarif a changé. Pour une rétrogradation, il arrive à la fin de la période, après la fin de la session.
// Inside the verified webhook handler above.
if (event.type === "session.completed" && event.data.type === "plan_change") {
// The customer finished the flow. A downgrade starts later, at effective_at.
const { metadata, direction, effective_at } = event.data;
await notifyUser(metadata.user_id, direction === "upgrade" ? "You're on the new plan." : "Your plan changes on " + new Date(effective_at * 1000).toDateString());
}
if (event.type === "subscription.updated" && event.data.change?.type === "price_changed") {
// The price actually changed: switch the features on your side now.
await setPlan(event.data.subscription_id, event.data.change.new_price_id);
}Quand une session de changement de formule expire
Une session de changement de formule dure 7 jours, ou jusqu'à la fin de la période en cours si elle arrive avant. Elle expire aussi, avec session.expired, quand son offre est remplacée, annulée ou expirée, ou quand une mise à niveau qu'elle a démarrée reste impayée et expire. Une mise à niveau que le client a confirmée mais pas encore payée garde la session ouverte jusqu'à son paiement ou son expiration.
Ajoutez un bouton d'annulation à votre application
Une session cancel envoie un client confirmer une annulation. Appelez Créer une session d'annulation avec l'identifiant de l'abonnement dans subscription_id. success_url, cancel_url et metadata sont facultatifs, comme pour une session de paiement. Pour annuler sans demander au client, utilisez plutôt Annuler un abonnement.
at_period_end est facultatif aussi. Envoyez true pour terminer l'abonnement à la fin de la période payée, ou false pour le terminer tout de suite. Laissez-le de côté et le réglage Quand les clients annulent de votre application, customer_cancel, décide. Abuna lit le réglage quand vous créez la session, et l'at_period_end de la session contient toujours le résultat.
Créez la session
curl https://api.abuna.app/v1/apps/8ddhXCDW/sessions \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{
"type": "cancel",
"subscription_id": "x9QbL2sK",
"success_url": "https://example.com/account/canceled",
"cancel_url": "https://example.com/account",
"metadata": {"user_id": "42"}
}'{
"id": "n9RcL3Xu",
"type": "cancel",
"status": "open",
"url": "https://app.abuna.app/cancel/9e4b7a1d3f6c0e8b2d5a9f1c4e7b0d3a6f2c",
"success_url": "https://example.com/account/canceled",
"cancel_url": "https://example.com/account",
"return_url": null,
"metadata": {
"user_id": "42"
},
"customer_id": "Cu5tM8rA",
"price_id": null,
"subscription_id": "x9QbL2sK",
"invoice_id": null,
"plan_offer_id": null,
"at_period_end": true,
"expires_at": 1727686400,
"completed_at": null,
"created_at": 1727600000
}L'abonnement doit être active ou past_due, et pas déjà programmé pour être annulé. Sinon vous obtenez l'une de ces erreurs :
409subscription_canceled: l'abonnement a déjà pris fin.409subscription_not_active: l'abonnement estpending: sa première période n'est pas encore payée. Pour laisser le client l'abandonner, envoyez-le plutôt vers l'espace client, où Annuler le termine tout de suite.409cancel_already_scheduled: une annulation est déjà programmée pour la fin de la période.422invalidavec un élémenterrorspoursubscription_id: l'application n'a aucun abonnement avec cet identifiant.
Redirigez et gérez le retour
Redirigez le client vers l'url. La page montre ce qui va se passer et la date de fin, avec un bouton Confirmer et un lien « Retour » vers votre URL d'annulation. Ce qui se passe suit les règles d'annulation :
- À la fin de la période : l'abonnement reste
active, et le client garde l'accès jusqu'à la fin de la période payée. - Période impayée : si la période en cours n'est pas payée, comme pendant que l'abonnement est
past_due, ou est déjà terminée, l'abonnement prend fin tout de suite à la place. La page le dit au client avant qu'il confirme. - Tout de suite : l'abonnement prend fin maintenant, et ses factures ouvertes sont annulées.
La session se termine quand le client confirme, que l'annulation soit programmée ou immédiate. Abuna l'envoie ensuite vers votre URL de succès, avec {SESSION_ID} et {SUBSCRIPTION_ID} remplis, ou session_id et subscription_id ajoutés, comme pour les autres types.
http
.createServer(async (req, res) => {
const url = new URL(req.url, "https://example.com");
const user = await currentUser(req);
// The "Cancel subscription" button posts here.
if (req.method === "POST" && url.pathname === "/account/cancel") {
const session = await createCancelSession(user); // the request above
res.writeHead(303, { Location: session.url }).end();
return;
}
// success_url: the customer confirmed. Abuna adds session_id and subscription_id.
if (req.method === "GET" && url.pathname === "/account/canceled") {
return showPage(res, "sorry-to-see-you-go");
}
// cancel_url: the "Back to" link. The customer changed their mind.
if (req.method === "GET" && url.pathname === "/account") {
return showPage(res, "account");
}
res.writeHead(404).end();
})
.listen(3000);Confirmez via le webhook
{
"id": "01J9ZQ8H1J2K3M4N5P6Q7R8S9T",
"type": "session.completed",
"environment": "test",
"created_at": 1727600300,
"data": {
"session_id": "n9RcL3Xu",
"type": "cancel",
"subscription_id": "x9QbL2sK",
"customer_id": "Cu5tM8rA",
"metadata": {
"user_id": "42"
},
"completed_at": 1727600300,
"cancel_at": 1730192000,
"immediate": false,
"subscription": {
"id": "x9QbL2sK",
"app_id": "8ddhXCDW",
"customer_id": "Cu5tM8rA",
"price_id": "Zt6YbN3q",
"status": "active",
"starts_at": 1727600000,
"activated_at": 1727600100,
"canceled_at": null,
"cancel_at": 1730192000,
"current_period_start": 1727600000,
"current_period_end": 1730192000,
"ended_reason": null,
"metadata": {
"user_id": "42"
},
"created_at": 1727600000,
"manage_token": "8c1f4a7d2e9b3c6f0a5d8e1b4c7f2a9d3e6b"
}
}
}immediate vaut true quand l'abonnement a pris fin tout de suite. cancel_at est le moment où une annulation programmée termine l'abonnement, et null quand il a pris fin tout de suite.
subscription.updated (une annulation programmée) ou subscription.canceled (une annulation immédiate) se déclenchent toujours, comme pour toute annulation. Comptez sur deux événements :
session.completedpour savoir que le client a confirmé l'annulation vers laquelle vous l'avez envoyé. Il porte vosmetadata.subscription.canceledpour retirer l'accès. Pour une annulation programmée, il arrive plus tard, àcancel_at.
// Inside the verified webhook handler above.
if (event.type === "session.completed" && event.data.type === "cancel") {
// The customer confirmed. Access ends on subscription.canceled, not here.
const { metadata, immediate, cancel_at } = event.data;
await notifyUser(metadata.user_id, immediate ? "Your subscription has ended." : "Your subscription ends on " + new Date(cancel_at * 1000).toDateString());
}
if (event.type === "subscription.canceled") {
// Right away for an immediate cancel. At cancel_at for a scheduled one.
await revokeAccess(event.data.subscription_id);
}Quand une session d'annulation expire
Une session d'annulation que le client ne confirme pas expire 24 heures après sa création, et Abuna envoie session.expired. L'abonnement ne change pas.
Ajoutez un lien Gérer la facturation à votre application
Une session portal envoie un client vers l'espace client pour un abonnement, avec un lien de retour vers votre application. Là, il peut changer de formule, payer, annuler, connecter Telegram et mettre à jour ses coordonnées. Appelez Créer une session d'espace client avec l'identifiant de l'abonnement dans subscription_id et une return_url. metadata est facultatif. L'abonnement peut avoir n'importe quel statut, y compris pending et canceled.
success_url et cancel_url ne s'appliquent pas à une session d'espace client. Envoyer l'une ou l'autre renvoie 400 avec le code invalid_json. return_url suit les mêmes règles d'URL que les deux autres.
Créez la session
curl https://api.abuna.app/v1/apps/8ddhXCDW/sessions \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{
"type": "portal",
"subscription_id": "x9QbL2sK",
"return_url": "https://example.com/account",
"metadata": {"user_id": "42"}
}'{
"id": "D1vT7rQf",
"type": "portal",
"status": "open",
"url": "https://app.abuna.app/portal/2c7f0a3d6b9e1c4f8a2d5b7e0c3f6a9d1b4e",
"success_url": null,
"cancel_url": null,
"return_url": "https://example.com/account",
"metadata": {
"user_id": "42"
},
"customer_id": "Cu5tM8rA",
"price_id": null,
"subscription_id": "x9QbL2sK",
"invoice_id": null,
"plan_offer_id": null,
"at_period_end": null,
"expires_at": 1727603600,
"completed_at": null,
"created_at": 1727600000
}L'url est l'espace client, avec un lien « Retour » vers votre return_url en haut. Un abonnement inconnu renvoie 422 avec le code invalid et un élément errors pour subscription_id.
Redirigez et laissez le client revenir
Créez une nouvelle session chaque fois que le client clique sur votre bouton « Gérer la facturation », et redirigez-le tout de suite vers son url. L'url doit être ouverte dans l'heure suivant sa création. Après cela, elle affiche un lien invalide. Une fois ouverte, elle continue de fonctionner comme l'espace client.
Le lien « Retour » reste pendant que le client circule dans l'espace client : changer de formule, connecter Telegram, mettre à jour ses coordonnées, et payer. Sur la page de paiement, les liens de retour et de fin renvoient vers votre return_url, et « Gérer l'abonnement » renvoie vers l'espace client. Chacun de ces liens est votre return_url exactement comme vous l'avez envoyée : Abuna n'ajoute aucun paramètre de requête ni identifiant. Mettez ce dont votre page a besoin dans l'URL à la création de la session.
Ce que le client voit dépend du statut de l'abonnement :
pending: la première facture avec sa date limite de paiement et un bouton Payer, et un bouton Annuler qui termine l'abonnement tout de suite. Pas de changement de formule.activeoupast_due: la formule, les factures, les coordonnées, un bouton Annuler, et les tarifs vers lesquels il peut changer, s'il y en a. Un abonnementpast_dueaffiche aussi sa facture impayée avec un bouton Payer.canceled: la formule, la date de son annulation, les factures, et leurs coordonnées, qu'ils peuvent encore mettre à jour. Rien à payer, et aucun changement de formule ni annulation.
http
.createServer(async (req, res) => {
const url = new URL(req.url, "https://example.com");
const user = await currentUser(req);
// The "Manage billing" button posts here. Make a new session every time.
if (req.method === "POST" && url.pathname === "/account/billing") {
const session = await createPortalSession(user); // the request above
res.writeHead(303, { Location: session.url }).end();
return;
}
// return_url: the "Back to" link at the top of the portal.
if (req.method === "GET" && url.pathname === "/account") {
return showPage(res, "account");
}
res.writeHead(404).end();
})
.listen(3000);Agissez sur ce que fait le client
{
"id": "01J9ZQ8J1K2M3N4P5Q6R7S8T9V",
"type": "session.completed",
"environment": "test",
"created_at": 1727600020,
"data": {
"session_id": "D1vT7rQf",
"type": "portal",
"subscription_id": "x9QbL2sK",
"customer_id": "Cu5tM8rA",
"metadata": {
"user_id": "42"
},
"completed_at": 1727600020,
"subscription": {
"id": "x9QbL2sK",
"app_id": "8ddhXCDW",
"customer_id": "Cu5tM8rA",
"price_id": "Zt6YbN3q",
"status": "active",
"starts_at": 1727600000,
"activated_at": 1727600100,
"canceled_at": null,
"cancel_at": null,
"current_period_start": 1727600000,
"current_period_end": 1730192000,
"ended_reason": null,
"metadata": {
"user_id": "42"
},
"created_at": 1727600000,
"manage_token": "8c1f4a7d2e9b3c6f0a5d8e1b4c7f2a9d3e6b"
}
}
}Une session d'espace client se termine quand le client l'ouvre pour la première fois, et session.completed se déclenche alors. Cela ne signifie pas que le client a changé quelque chose. Chaque chose qu'il fait dans l'espace client envoie son propre événement, comme toujours :
- Changer de formule :
subscription.updated, etinvoice.createdetinvoice.paidpour une mise à niveau avec quelque chose à payer. - Annuler, ou conserver une annulation programmée :
subscription.updated, puissubscription.canceledquand l'abonnement prend fin. - Payer une facture :
invoice.paid, ouinvoice.payment_failed. - Mettre à jour ses coordonnées :
customer.updated.
// Inside the verified webhook handler above.
// A portal session.completed only means the customer opened the portal.
// Act on what they did there, from these events.
if (event.type === "subscription.updated") {
await syncSubscription(event.data.subscription_id, event.data.price_id, event.data.cancel_at);
}
if (event.type === "subscription.canceled") {
await revokeAccess(event.data.subscription_id);
}
if (event.type === "invoice.paid") {
await markInvoicePaid(event.data.subscription_id, event.data.invoice_id);
}
if (event.type === "customer.updated") {
await syncContact(event.data.customer_id, event.data.email, event.data.phone_number);
}Quand une session d'espace client expire
Une session d'espace client que personne n'ouvre dans l'heure expire, et Abuna envoie session.expired. Une session ouverte s'est déjà terminée, donc elle n'expire jamais.
Le subscription_page_url de l'abonnement et les liens de l'espace client dans les e-mails d'Abuna continuent de fonctionner comme avant, sans lien de retour vers votre application.
Métadonnées
metadata est un objet de vos propres clés et valeurs. Il peut avoir jusqu'à 20 clés. Chaque clé fait de 1 à 40 caractères et chaque valeur est une chaîne de 500 caractères maximum. Des métadonnées hors de ces limites renvoient 422 avec le code invalid et un élément errors pour metadata. N'y mettez pas de secrets.
L'abonnement qu'une session de paiement crée commence avec les mêmes metadata. Chaque webhook qui porte un subscription_id, comme subscription.paid, invoice.paid ou subscription.canceled, porte aussi les metadata de cet abonnement. Pour les changer plus tard, mettez à jour l'abonnement.
Expiration
Une session de paiement dure 24 heures. Dans la minute environ suivant expires_at, une session ouverte devient expired et Abuna envoie session.expired. Sa page indique alors au client que le paiement a expiré, avec un lien vers votre URL d'annulation. Pour recommencer, créez une nouvelle session. Les autres types ont leurs propres règles : voyez l'expiration d'une session de paiement, d'un changement de formule, d'une annulation et d'une session d'espace client.
Pour expirer une session plus tôt, par exemple quand le client choisit une autre formule sur votre site, appelez POST/v1/apps/{appID}/sessions/{id}/expire.
curl -X POST https://api.abuna.app/v1/apps/8ddhXCDW/sessions/A4kN1oSs/expire \
-H "Authorization: Bearer sk_test_..."Une session déjà completed ou expired renvoie 409 avec le code session_not_open.
Une session se termine au plus une fois
Une session se termine dans l'un de deux états, completed ou expired, et ne change plus ensuite. Vous obtenez un session.completed ou un session.expired pour elle, jamais les deux. Une session expirée ne se termine jamais.
Un client qui part pendant un paiement
Un paiement Mobile Money attend que le client le valide sur son téléphone. S'il ferme la page pendant l'attente, rien n'est perdu : quand il valide, l'abonnement devient active et session.completed arrive quand même. Votre prestataire informe Abuna de la validation via l'URL de rappel du prestataire que vous avez définie dans Connecter un prestataire de paiement.
Abuna n'expire pas une session pendant que son paiement est en attente, même après expires_at. Si ce paiement échoue, la session expire d'elle-même ensuite.
Si vous expirez vous-même une session pendant que son paiement est en attente et que le paiement réussit ensuite, l'abonnement devient active mais la session reste expired.
L'abonnement d'une session expirée
Si le client a rempli ses informations mais n'a pas payé avant l'expiration de la session, l'abonnement pending reste. Abuna a envoyé au client son lien de paiement par e-mail au démarrage. Si le client le paie avant que le délai de paiement de votre application s'épuise, l'abonnement devient active. Vous obtenez subscription.paid avec vos metadata, mais pas de session.completed. S'il ne paie pas, l'abonnement prend fin avec subscription.canceled et la raison checkout_expired.
Pour rattraper ces paiements tardifs, accordez aussi l'accès sur subscription.paid. Basez votre attribution sur subscription_id, pour que recevoir les deux événements n'accorde l'accès qu'une fois.
Réessayez sans risque avec une clé d'idempotence
Une erreur réseau peut cacher si votre requête a créé une session. Pour réessayer sans en créer une seconde, envoyez un en-tête Idempotency-Key. Utilisez une nouvelle valeur aléatoire pour chaque tentative, et la même valeur à chaque nouvelle tentative de celle-ci. Une nouvelle tentative avec la même clé et le même corps dans les 24 heures reçoit à nouveau la première réponse, et aucune seconde session n'est créée.
curl https://api.abuna.app/v1/apps/8ddhXCDW/sessions \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1c2b9e-4d3a-4c8e-9a7f-2e5b8d1c0a43" \
-d '{
"type": "checkout",
"price_id": "Zt6YbN3q",
"metadata": {"user_id": "42"}
}'import crypto from "node:crypto";
// Make the key once per checkout attempt, and send the same key on every retry.
const idempotencyKey = crypto.randomUUID();
async function createSession() {
return fetch("https://api.abuna.app/v1/apps/8ddhXCDW/sessions", {
method: "POST",
headers: {
Authorization: "Bearer sk_test_...",
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify({ type: "checkout", price_id: "Zt6YbN3q", metadata: { user_id: "42" } }),
});
}
let response;
for (let attempt = 1; attempt <= 3; attempt++) {
try {
response = await createSession();
if (response.status < 500 && response.status !== 409) break;
} catch {
// A network error: retry with the same key.
}
await new Promise((resolve) => setTimeout(resolve, attempt * 1000));
}Les requêtes idempotentes listent ce que renvoie chaque réutilisation d'une clé.
Testez en mode Test
Utilisez l'identifiant de votre application Test et sa clé sk_test_. Le flux est le même qu'en mode Réel, mais l'étape de paiement ne déplace aucun argent. Avec le simulateur Abuna, qui est le choix par défaut, choisissez une issue à l'étape de paiement :
- Paiement réussi : la session se termine tout de suite.
- Validation requise, puis réussite : le paiement est d'abord
pending, comme une demande Mobile Money, et réussit après environ 10 secondes. La page l'attend. - Paiement refusé et Solde insuffisant : le paiement échoue et le client peut réessayer.
Voyez Paiements simulés pour le comportement de chacun. Si vous avez connecté le bac à sable d'un prestataire, l'étape de paiement le propose aussi sous Tester avec. Choisissez-le, et ses numéros de test décident de l'issue. Voyez Numéros de test.
Les sessions d'annulation et d'espace client fonctionnent de la même façon en mode Test, sur vos abonnements Test.
En mode Test, success_url, cancel_url et return_url peuvent être des URL http sur localhost, 127.0.0.1 ou [::1], pour que vous puissiez revenir sur votre propre machine. Cela s'applique aussi à l'URL de succès et à l'URL d'annulation par défaut de votre application Test, pour chaque type de session. Le mode Réel n'accepte que les URL https, et toute autre URL renvoie 422 avec le code invalid et un élément errors avec le code url_invalid. Votre point de terminaison de webhook a toujours besoin d'une URL https publique dans les deux modes, alors exposez votre machine via un tunnel pour recevoir les événements.
Étapes suivantes
- SessionsEnvoyez un client vers une page hébergée pour démarrer un abonnement, payer une facture, changer de formule, annuler ou ouvrir son espace client. Puis listez, lisez et expirez ces sessions.
- Recevoir des webhooksRecevez les événements sur votre serveur et vérifiez qu'Abuna les a envoyés.
- Mode Test et mode RéelDéveloppez sur les données Test avec le simulateur Abuna ou les bacs à sable des prestataires, puis passez en Réel avec le même code.
- AbonnementsDémarrez, listez, lisez, mettez à jour, annulez, conservez et changez la formule de l'abonnement d'un client à un tarif.