Le moteur d’abonnements de lahsab, c’est Stripe où collectionMethod vaut toujours send_invoice : on émet une facture datée, on relance, et le client paie par virement CCP / BaridiMob quand il veut. Il n’y a pas de carte à débiter, donc rien ne se prélève tout seul, et la mécanique tout entière découle de là.
La règle d’or
Stripe-carte peut avancer l’horloge dès le renouvellement, parce qu’il a une carte à débiter : le paiement est présumé. lahsab n’a rien à débiter. Avancer la période sur une facture émise mais non payée, ce serait accorder de l’accès contre une promesse, et vous laisser croire qu’un client est à jour alors que l’argent n’est jamais arrivé.
Donc l’horloge ne bouge jamais sur une promesse. Elle bouge sur un paiement.
En pratique, votre code d’entitlement se réduit à une comparaison :
const aAcces = user.paidUntil && user.paidUntil > new Date();
où paidUntil est le currentPeriodEnd que vous avez écrit en recevant invoice.paid.
Qui possède quoi
C’est la frontière à avoir en tête avant d’écrire la première ligne.
| lahsab possède | Vous possédez | |
|---|---|---|
| Quoi | L’argent et le temps | L’entitlement |
| C’est-à-dire | Qui doit quoi, pour quelle période, est-ce payé, depuis quand c’est en retard. | Ce qu’un plan débloque, les quotas, les limites, les rôles. |
lahsab ne sait pas ce que débloque votre plan « Pro » : ni combien de projets il autorise, ni quelles fonctionnalités il ouvre. Exactement comme Stripe. On vous dit « payé jusqu’au 13 septembre » ; ce que vous en faites vous regarde.
Le modèle
Cinq objets, empruntés à Stripe : si vous connaissez l’un, vous connaissez l’autre.
| Objet | Rôle |
|---|---|
| Customer | Votre client. externalId = son identifiant chez vous (unique par marchand et environnement). phone est le canal de relance. |
| Product | Ce que vous vendez. « Abonnement Pro ». |
| Price | Un tarif pour un produit : montant, one_time ou recurring, et la cadence. Immuable. |
| Subscription | L’engagement d’un client sur un prix récurrent. Porte currentPeriodEnd. |
| Invoice | Ce qui est dû, pour une période, avec une échéance. C’est l’objet qui se paie. |
Souscrire, en un appel
POST /subscriptions est le point d’entrée du parcours d’achat. En un seul appel, il crée l’abonnement, émet immédiatement la première facture, et vous renvoie le lien sur lequel rediriger l’acheteur. C’est votre Stripe Checkout.
-
Résolvez le client. Cherchez-le par votre propre identifiant (
GET /customers?externalId=), créez-le s’il n’existe pas. C’est le motif de lookup / upsert. -
Créez l’abonnement. Un
POST /subscriptionsavec lecustomerIdet lepriceId. L’abonnement naîtincompleteet la première facture est émise. -
Redirigez l’acheteur vers
latestInvoice.hostedInvoiceUrl, la page de facture hébergée par lahsab, où il choisit son moyen de paiement, voit les coordonnées correspondantes et dépose sa preuve. -
Attendez
invoice.paid. C’est là, et seulement là, que l’accès s’accorde : l’abonnement passeactiveetcurrentPeriodEndavance.
# 1. Le client existe-t-il déjà ?
curl "https://api.lahsab.com/customers?externalId=user_abc123" \
-H "x-api-key: lsk_live_CLE"
# 2. Sinon, on le crée.
curl -X POST https://api.lahsab.com/customers \
-H "x-api-key: lsk_live_CLE" \
-H "content-type: application/json" \
-H "Idempotency-Key: cust-user_abc123" \
-d '{
"externalId": "user_abc123",
"name": "Yacine B.",
"phone": "0770000000",
"defaultMethod": "baridimob"
}' async function upsertCustomer(user) {
const found = await api(
`/customers?externalId=${encodeURIComponent(user.id)}`,
);
if (found.length) return found[0];
return api("/customers", {
method: "POST",
headers: { "Idempotency-Key": `cust-${user.id}` },
body: JSON.stringify({
externalId: user.id,
name: user.fullName,
phone: user.phone,
defaultMethod: "baridimob",
}),
});
} curl -X POST https://api.lahsab.com/subscriptions \
-H "x-api-key: lsk_live_CLE" \
-H "content-type: application/json" \
-H "Idempotency-Key: sub-user_abc123-pro-mensuel" \
-d '{
"customerId": "c0ffee00-1111-4222-8333-444455556666",
"priceId": "9f8e7d6c-5b4a-4321-9876-543210fedcba",
"daysUntilDue": 7
}' const res = await fetch("https://api.lahsab.com/subscriptions", {
method: "POST",
headers: {
"x-api-key": process.env.LAHSAB_API_KEY,
"content-type": "application/json",
"Idempotency-Key": `sub-${user.id}-pro-mensuel`,
},
body: JSON.stringify({
customerId: customer.id,
priceId: PRO_MENSUEL,
daysUntilDue: 7,
}),
});
const subscription = await res.json();
// Un seul appel : l'abonnement existe, la 1re facture est émise.
window.location.href = subscription.latestInvoice.hostedInvoiceUrl; <?php
$ch = curl_init("https://api.lahsab.com/subscriptions");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"x-api-key: " . getenv("LAHSAB_API_KEY"),
"content-type: application/json",
"Idempotency-Key: sub-" . $userId . "-pro-mensuel",
],
CURLOPT_POSTFIELDS => json_encode([
"customerId" => $customerId,
"priceId" => PRO_MENSUEL,
"daysUntilDue" => 7,
]),
]);
$subscription = json_decode(curl_exec($ch), true);
header("Location: " . $subscription["latestInvoice"]["hostedInvoiceUrl"]); import os, requests
res = requests.post(
"https://api.lahsab.com/subscriptions",
headers={
"x-api-key": os.environ["LAHSAB_API_KEY"],
"Idempotency-Key": f"sub-{user.id}-pro-mensuel",
},
json={
"customerId": customer.id,
"priceId": PRO_MENSUEL,
"daysUntilDue": 7,
},
)
subscription = res.json()
redirect(subscription["latestInvoice"]["hostedInvoiceUrl"]) Réponse 201, l’abonnement avec sa première facture déjà émise :
{
"id": "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d",
"environment": "live",
"status": "incomplete",
"collectionMethod": "send_invoice",
"daysUntilDue": 7,
"invoiceLeadDays": 7,
"currentPeriodStart": "2026-07-13T00:00:00.000Z",
"currentPeriodEnd": "2026-08-13T00:00:00.000Z",
"cancelAtPeriodEnd": false,
"latestInvoice": {
"id": "f1e2d3c4-b5a6-4978-8b1c-2d3e4f5a6b7c",
"number": "LSB-2026-0042",
"status": "open",
"amountDue": 2500,
"dueDate": "2026-07-20T00:00:00.000Z",
"hostedInvoiceUrl": "https://dashboard.lahsab.com/invoice/f1e2d3c4-b5a6-4978-8b1c-2d3e4f5a6b7c"
},
"createdAt": "2026-07-13T10:00:00.000Z"
}
Les états
Facture
draft est fugace : la facture est finalisée (donc numérotée) dans la foulée.
paid, void et uncollectible sont terminaux.
| Statut | Sens |
|---|---|
draft | Créée, pas encore numérotée ni payable. |
open | Finalisée, numérotée, payable. |
paid | Payée. Émet invoice.paid. |
void | Annulée par vous. |
uncollectible | La grâce est épuisée : vous ne comptez plus l’encaisser. |
Abonnement
Un abonnement entre par incomplete ou trialing, et sort par
unpaid ou canceled.
| Statut | Sens |
|---|---|
incomplete | Première facture émise, jamais payée. Aucun accès n’a jamais été accordé. |
trialing | Essai en cours. currentPeriodEnd vaut trialEnd. |
active | Payé jusqu’à currentPeriodEnd. |
past_due | L’échéance est passée et la facture de renouvellement est toujours open. |
unpaid | Grâce épuisée, facture irrécouvrable. |
canceled | Terminé. endedAt est posé. |
Numérotation
À la finalisation, chaque facture reçoit un numéro : LSB-{ANNÉE}-{séquence}.
LSB-2026-0001
LSB-2026-0002
LSB-2026-0003
La séquence est sans trou, et propre à chaque (marchand, environnement, année). Elle repart à 0001 au 1er janvier.
Impayés
lahsab ne relance pas votre client à votre place. Quand l’échéance est dépassée et que la facture reste open, il émet invoice.overdue : le payload porte le nom, le téléphone, le montant, l’échéance et le lien de paiement, de quoi composer votre propre rappel (un message, un appel) sans un appel API de plus. Ensuite, deux réglages du marchand, dans les réglages du tableau de bord, pilotent la suite :
| Réglage | Défaut | Rôle |
|---|---|---|
gracePeriodDays | 7 | Le temps laissé au client pour payer après l’échéance. |
dunningExhaustedBehavior | cancel | Ce qu’il advient de l’abonnement en fin de grâce. |
En fin de grâce, la facture devient uncollectible, lahsab émet invoice.marked_uncollectible, et l’abonnement suit le réglage :
dunningExhaustedBehavior | En fin de grâce |
|---|---|
cancel | L’abonnement passe canceled. |
mark_unpaid | Il passe unpaid, la facture devient uncollectible. |
leave_active | On ne touche à rien, vous gérez. |
Le portail client
POST /portal-sessions renvoie une URL à durée de vie courte (1 heure) où votre client retrouve son abonnement, ses factures et un bouton pour payer. C’est la page « gérer mon abonnement » que vous n’avez pas à construire.
{
"url": "https://dashboard.lahsab.com/portal/ps_7f3c2e1a9b4d4e6a8c1f2d3e",
"expiresAt": "2026-07-13T11:00:00.000Z"
}
Et ensuite ?
- Brancher
invoice.paidet vérifier les signatures : Webhooks. - Tous les endpoints, champs et erreurs : Référence API.