📍 Lecture connexe : Stripe, Paystack, Flutterwave et Wave en 2026 : intégrer un processeur de paiement — pour la vue d’ensemble du paysage paiement.
Un service en abonnement encaisse de petites sommes répétées plutôt qu’un montant ponctuel. Cela paraît anodin mais transforme l’architecture de paiement : il faut stocker une autorisation de carte, déclencher la charge à intervalle régulier, gérer les échecs (carte expirée, fonds insuffisants), notifier le client, et savoir quand mettre fin à l’abonnement. Paystack offre un module de subscriptions qui prend en charge la mécanique récurrente sans qu’on ait à implémenter le moteur de cycle de vie. Ce tutoriel construit une intégration complète à partir d’un projet Node.js neuf, depuis la création d’un plan jusqu’à la gestion fine des relances.
Prérequis
- Node.js 22 LTS ou supérieur, vérifier avec
node --version - Compte Paystack en mode test, créé sur dashboard.paystack.com
- Une URL HTTPS publique pour les webhooks (Cloudflare Tunnel, ngrok ou un domaine de staging)
- Niveau attendu : intermédiaire — vous avez déjà fait au moins un appel API authentifié
- Temps estimé : environ 75 minutes
Étape 1 — Comprendre le modèle subscription Paystack
Paystack distingue trois entités. Le Plan représente l’offre commerciale : un nom (« Pro mensuel »), un montant (en kobo pour le NGN, en centimes pour les autres devises), un intervalle (parmi daily, weekly, monthly, quarterly, biannually, annually, intervalles documentés dans la référence officielle) et une devise. Le Customer représente le client final, identifié par son email. La Subscription lie un Customer à un Plan en utilisant une autorisation de carte préalablement obtenue.
La nuance importante est qu’on ne peut pas créer une subscription tant que le client n’a pas effectué au moins une transaction réussie. Cette transaction génère une authorization_code, qui est le token réutilisable permettant à Paystack de débiter automatiquement la carte. Le flux complet est donc : créer le plan, faire payer le client une première fois pour obtenir l’autorisation, puis créer la subscription avec cette autorisation.
Cette séquence en deux temps déroute parfois, mais elle est cohérente avec les standards des paiements récurrents : le client donne explicitement son consentement à être débité de manière récurrente lors de la première charge.
Étape 2 — Initialiser le projet et la clé secrète
On démarre une API Express minimale. La clé secrète Paystack est visible dans Settings → API Keys & Webhooks du tableau de bord. Les clés de test commencent par sk_test_.
mkdir paystack-subs && cd paystack-subs
npm init -y
npm install express axios dotenv
npm install -D typescript tsx @types/node @types/express
npx tsc --init
On utilise axios directement plutôt qu’un SDK communautaire pour rester proche de l’API HTTP officielle. Paystack ne maintient pas de SDK Node.js officiel ; ceux disponibles sur npm sont des wrappers tiers qui peuvent prendre du retard sur l’API. Le coût d’écrire les appels HTTP soi-même est faible et garantit la maîtrise.
# .env
PAYSTACK_SECRET=sk_test_...
PAYSTACK_BASE=https://api.paystack.co
APP_URL=http://localhost:3000
On crée ensuite un client HTTP préconfiguré avec le Bearer Token, ce qui évite de répéter le header à chaque appel.
// src/paystack.ts
import axios from 'axios'
import 'dotenv/config'
export const paystack = axios.create({
baseURL: process.env.PAYSTACK_BASE,
headers: {
Authorization: `Bearer ${process.env.PAYSTACK_SECRET}`,
'Content-Type': 'application/json',
},
})
Étape 3 — Créer un plan
Le plan est créé une fois pour toutes côté commerçant, généralement via un script de seed ou une page d’administration. Une fois créé, son code (PLN_xxxxxx) est stable et peut être stocké en configuration applicative.
// src/scripts/createPlan.ts
import { paystack } from '../paystack'
async function main() {
const r = await paystack.post('/plan', {
name: 'Pro mensuel',
amount: 500000, // 5000 NGN, en kobo
interval: 'monthly',
currency: 'NGN',
})
console.log('Plan code:', r.data.data.plan_code)
}
main().catch(console.error)
L’unité du champ amount est la plus petite unité monétaire de la devise : kobo pour le NGN, pesewa pour le GHS, centime pour le XOF. C’est une source de bug classique : envoyer 5000 en pensant 5000 NGN alors que Paystack interprète 50 NGN. La règle vaut pour tous les endpoints qui prennent un montant en paramètre.
L’exécution du script affiche un plan_code qu’on copie dans la configuration. Si on a plusieurs plans (mensuel, annuel, premium), on les crée tous à la file et on stocke leurs codes dans une table de configuration ou un fichier de seed.
Étape 4 — Initialiser la première transaction
Avant de pouvoir créer une subscription, il faut une autorisation de carte. On initialise une transaction normale via POST /transaction/initialize, on récupère l’authorization_url, on redirige le client. Une fois le paiement confirmé, Paystack envoie un webhook charge.success qui contient l’authorization_code dans l’objet de la charge.
app.post('/api/checkout', async (req, res) => {
const { email, amount } = req.body
const r = await paystack.post('/transaction/initialize', {
email,
amount, // en kobo
callback_url: `${process.env.APP_URL}/checkout/return`,
metadata: { intent: 'subscription_setup' },
})
res.json({ authorizationUrl: r.data.data.authorization_url })
})
Le client est redirigé vers une page Paystack hébergée qui présente la saisie carte et déclenche le 3DS si nécessaire. Le champ metadata est libre et permet de transporter du contexte : on l’utilise pour marquer cette transaction comme une étape d’onboarding subscription, ce qui sera utile dans le webhook handler.
Étape 5 — Recevoir et vérifier le webhook
Le webhook Paystack signe son payload avec un HMAC SHA-512 calculé à partir de la clé secrète, transmis dans le header x-paystack-signature. La vérification est non négociable : sans elle, n’importe qui peut envoyer un faux charge.success.
import crypto from 'node:crypto'
app.post(
'/webhooks/paystack',
express.raw({ type: 'application/json' }),
async (req, res) => {
const expected = crypto
.createHmac('sha512', process.env.PAYSTACK_SECRET!)
.update(req.body)
.digest('hex')
const got = req.headers['x-paystack-signature']
if (expected !== got) return res.status(401).send('bad sig')
const event = JSON.parse(req.body.toString())
if (event.event === 'charge.success') {
const data = event.data
if (data.metadata?.intent === 'subscription_setup') {
await createSubscription(data.customer.email, data.authorization.authorization_code)
}
}
res.sendStatus(200)
},
)
Trois subtilités. Le HMAC est calculé sur le body brut, donc on utilise express.raw sur cette route et on parse le JSON manuellement après vérification. La comparaison de signatures avec === est techniquement vulnérable au timing attack ; pour des intégrations à enjeu élevé, on utilise crypto.timingSafeEqual avec deux Buffers de même longueur. Et le champ data.authorization.authorization_code est ce qu’on stocke pour pouvoir débiter le client à l’avenir : c’est le token de paiement réutilisable, équivalent du customer.payment_method chez Stripe.
Étape 6 — Créer la subscription
Avec l’autorisation en main, on crée la subscription via POST /subscription. Paystack lie alors le customer au plan et déclenche automatiquement la charge à chaque cycle.
async function createSubscription(email: string, authorization: string) {
// 1. Récupérer ou créer le customer
let r = await paystack.get(`/customer/${encodeURIComponent(email)}`).catch(() => null)
if (!r) {
r = await paystack.post('/customer', { email })
}
const customerCode = r.data.data.customer_code
// 2. Créer la subscription
const sub = await paystack.post('/subscription', {
customer: customerCode,
plan: process.env.PLAN_CODE!,
authorization,
})
return sub.data.data.subscription_code
}
Le retour contient un subscription_code (SUB_xxxxxx) qu’on stocke côté commerçant, lié au customer interne. C’est cette référence qui servira à interroger ou annuler la subscription plus tard. La première charge récurrente sera émise au prochain cycle (par exemple, 1 mois après la première transaction si l’intervalle est monthly).
Étape 7 — Gérer les échecs de paiement récurrent
Une carte expirée, un compte fermé, un fonds insuffisant : tout abonnement de plus de quelques mois finit par rencontrer un échec. Paystack émet le webhook invoice.payment_failed dans ce cas et tente automatiquement plusieurs fois avant d’abandonner. La logique applicative doit anticiper ces événements.
// Dans le handler webhook, ajouter :
if (event.event === 'invoice.payment_failed') {
const sub = event.data.subscription.subscription_code
// Notifier le client par email pour mise à jour de la carte
// Ne PAS désactiver le service immédiatement : Paystack retentera
}
if (event.event === 'subscription.disable') {
// Paystack a abandonné les retries : couper le service
const sub = event.data.subscription_code
// Marquer l'abonnement comme inactif côté DB
}
La règle d’or est de ne jamais couper le service à la première erreur. Paystack tente de débiter à plusieurs reprises sur une fenêtre de quelques jours, et les transitions de statut sont notifiées par les webhooks invoice.payment_failed (échec ponctuel) et subscription.disable (abandon définitif). Une UI qui dit immédiatement « votre abonnement est suspendu » à la première erreur génère du support inutile.
Étape 8 — Mettre à jour la carte d’un client existant
Quand le client signale une carte expirée ou souhaite changer de moyen de paiement, on lui demande d’effectuer une nouvelle transaction d’un faible montant (par exemple 100 NGN qu’on remboursera ensuite, ou une carte sans charge via la Card Preauthorization API). La nouvelle authorization_code obtenue remplace l’ancienne dans la subscription via PUT /subscription/{code}/manage/email.
Cette UX est moins fluide qu’un simple « formulaire de mise à jour de carte », car Paystack n’expose pas l’autorisation existante en lecture pour la modifier directement. C’est un compromis lié à la sécurité PCI : le commerçant ne stocke jamais les détails de carte, seulement des tokens opaques.
Étape 9 — Annuler une subscription
Pour annuler, on appelle POST /subscription/disable avec le code de la subscription et un token spécifique généré côté Paystack pour cette opération. On peut aussi annuler depuis le tableau de bord, ce qui déclenche le même webhook subscription.disable.
app.post('/api/subs/:code/cancel', async (req, res) => {
const { code } = req.params
// 1. Récupérer le token d'annulation
const sub = await paystack.get(`/subscription/${code}`)
const token = sub.data.data.email_token
// 2. Désactiver la subscription
await paystack.post('/subscription/disable', { code, token })
res.json({ ok: true })
})
L’annulation est immédiate du point de vue de Paystack : aucune charge ne sera plus émise. Côté commerçant, on choisit de couper l’accès au service immédiatement (politique stricte) ou à la fin du cycle déjà payé (politique amicale), selon le contrat client. La plupart des SaaS choisissent la deuxième option pour réduire les frictions.
Étape 10 — Tester le scénario complet
Paystack fournit des cartes de test documentées dans Test Payments. La carte 4084 0840 8408 4081 avec CVV 408 et OTP 123456 simule un paiement réussi. Pour les tests de subscription, le mécanisme se déclenche au cycle suivant, donc on attend l’intervalle ou on utilise l’option start_date rétroactive lors de la création pour observer une charge immédiate.
On déroule un scénario complet : initialiser une transaction, payer avec la carte test, observer le webhook charge.success arriver, vérifier que la subscription a été créée, puis utiliser le tableau de bord Paystack pour déclencher manuellement une « test charge » sur la subscription afin de simuler le cycle. Tous ces signaux doivent s’enchaîner sans erreur dans les logs serveur.
Étape 11 — Réconciliation comptable et facturation
Une fois la subscription en production, la dimension comptable devient prépondérante. Chaque charge récurrente doit générer une facture client (PDF avec mentions légales, TVA si applicable) et alimenter le journal comptable. Paystack n’émet pas la facture commerciale officielle ; il fournit uniquement la trace technique de la transaction. La génération du PDF reste à la charge de l’application : à chaque webhook charge.success sur une subscription, on déclenche un job qui crée le PDF, l’envoie par email au client, et l’archive dans un bucket S3 ou équivalent.
Le rapprochement entre l’application et Paystack se fait quotidiennement via l’endpoint GET /transaction avec un filtre temporel. On parcourt toutes les transactions Paystack sur la fenêtre de 24 heures, on les compare aux entrées de la table de facturation, et on lève une alerte sur tout écart. Cette boucle de contrôle attrape les webhooks perdus (rare mais possible quand l’application a connu une indisponibilité prolongée) et les fraudes internes éventuelles.
Pour l’export comptable, Paystack expose les exports CSV depuis le tableau de bord. Pour une intégration plus poussée, on programme une routine quotidienne qui pousse les données dans un format compatible avec l’outil comptable utilisé : compte de ventes, compte de TVA collectée, compte client par numéro de subscription. Sage, Odoo, QuickBooks et Pennylane disposent tous d’API d’import CSV ou de connecteurs natifs.
Étape 12 — Relances et dunning
Le dunning est le processus de relance qu’on applique quand un paiement récurrent échoue. Paystack gère la couche technique (réessais automatiques) mais pas la couche relationnelle (emails au client, rappels, dégradation progressive du service). Une stratégie de dunning bien rodée récupère une part substantielle des subscriptions qui auraient été perdues sans intervention, et la différence entre une retry policy basique et une retry policy intelligente (relances personnalisées, fenêtres temporelles optimisées, multi-canal) se mesure en points entiers de revenu récurrent annuel.
Le pattern recommandé est une séquence de quatre emails sur 14 jours. Jour 0 : email de notification immédiat à la première échec, ton neutre, lien direct vers la mise à jour du moyen de paiement. Jour 3 : second rappel, ton plus pressant, mention que le service sera dégradé dans 7 jours. Jour 7 : troisième rappel avec dégradation effective (ex : passer le compte en mode lecture seule). Jour 14 : email final annonçant la fermeture, avec offre éventuelle de réactivation à tarif réduit.
Côté implémentation, chaque webhook invoice.payment_failed incrémente un compteur sur la subscription et déclenche le job d’email correspondant à l’étape. Le compteur se remet à zéro dès qu’une charge réussit. Cette logique simple, couplée à un templating d’email professionnel, fait la différence entre un SaaS qui perd 5 % de revenu en attrition et un SaaS qui en perd 15 %.
Erreurs fréquentes
| Erreur | Cause | Solution |
|---|---|---|
| « Customer has no authorizations » | Tentative de créer une subscription sans charge préalable | Faire payer le client une première fois avant de lier la subscription |
| Webhook signature invalide | Body parsé en JSON avant le HMAC | Utiliser express.raw sur la route webhook |
| Plan créé en NGN, charge en XOF échoue | Devise du plan figée à la création | Créer un plan distinct par devise cible |
| Subscription qui ne se renouvelle pas | Carte expirée non remplacée | Surveiller invoice.payment_failed et notifier le client |
| Montant facturé décalé d’un facteur 100 | Confusion entre unité majeure et mineure | Toujours envoyer en kobo / centime, jamais en unité majeure |
Étape 13 — Production et sécurité opérationnelle
Avant le passage en mode live, une revue rapide s’impose. Vérifier que tous les webhooks ont leur signature contrôlée, que les clés secrètes ne sont jamais loggées, et que la table des subscriptions a un index unique sur le subscription_code Paystack pour empêcher la création de doublons en cas de bug applicatif. Un test de chaos simple : couper le serveur pendant 5 minutes, vérifier qu’au redémarrage les webhooks manqués sont rejoués par Paystack et que l’état applicatif converge vers le bon résultat sans intervention manuelle.
Ressources
- Paystack Subscriptions — guide officiel
- Paystack Plan API
- Paystack Subscription API
- Paystack Webhooks — signature HMAC SHA-512
- Cartes de test Paystack
- Article connexe : Webhooks paiement sécurisés : signature et protection contre le rejeu
- Article connexe : Idempotency keys : éviter les doubles paiements