Aller au contenu
Bookelio / Documentation
Guides par thème

Paiements et facturation

Intégrer techniquement les abonnements à votre application

Connecter Bookelio côté serveur, gérer abonnements et droits par API, configurer le prorata et attribuer des mois gratuits.

Sur cette page

Comprendre les responsabilités

Votre application authentifie ses utilisateurs et identifie leur compte ou organisation. Bookelio conserve le catalogue commercial, les clients à facturer, les abonnements et leurs cycles de paiement. Votre serveur interroge Bookelio pour autoriser les fonctionnalités vendues. Masquer un bouton dans le navigateur ne remplace pas ce contrôle serveur.

L'organisation vendeuse possède un ou plusieurs logiciels. Chaque logiciel possède ses fonctionnalités, ses plans et ses abonnements. Le module peut donc servir à plusieurs applications et à plusieurs vendeurs, chacun dans son périmètre.

Les exemples ci-dessous sont fictifs. Remplacez les domaines et identifiants par ceux de votre installation. Pour le parcours dans les écrans, consultez Vendre des abonnements logiciels.

Préparer la connexion et les permissions

Activez le module Abonnements logiciels pour l'organisation vendeuse. Préparez un client de facturation dans cette organisation et une politique de TVA pour les offres payantes. Configurez ses moyens de paiement et son service e-mail pour les règlements et notifications.

Créez une clé API de l'organisation vendeuse avec les permissions adaptées. Conservez-la sur le serveur de votre application. Elle identifie le vendeur ; elle ne remplace pas l'authentification de vos utilisateurs et peut accéder aux abonnements de plusieurs clients du même vendeur.

UsagePermissions de la clé
Lire logiciels, plans, abonnements et fonctionnalités autoriséessoftwareBilling.read
Gérer le catalogue, résilier, accorder une offre ou programmer les siègessoftwareBilling.manage
Provisionner les fonctionnalités et les identités externessoftwareBilling.manage et customers.create
Créer un abonnementsoftwareBilling.manage et paymentRequests.create
Modifier un abonnement en fournissant planId, notamment pour une réactivationsoftwareBilling.manage et paymentRequests.create
Appliquer tout changement IMMEDIATE, même sans supplément ou pour les sièges seulssoftwareBilling.manage et paymentRequests.create
Lire ou modifier le déclenchement des factures dans les paramètres du vendeursettings.read pour lire ; settings.update pour modifier

Créer ou lire les clients et les politiques de TVA par leurs propres API demande également les permissions de ces ressources. Les exemples supposent ces éléments déjà créés.

Les routes REST ont pour préfixe /api/v1/admin/software-billing. Le transport oRPC utilise /rpc/v1 et les opérations admin.softwareBilling.*. La spécification consultable sur l'instance API est /spec/v1.json. Dans le monorepo Bookelio, le type BookelioApiV1Client décrit ce contrat ; une application externe peut utiliser directement REST, sans dépendre d'un package interne au dépôt.

Préparer les appels HTTP côté serveur

Pour les exemples, les variables ci-dessous sont des noms choisis par l'application tierce. Elles ne configurent pas automatiquement Bookelio lui-même.

SOFTWARE_BILLING_API_URL=https://api.billing.example.com
SOFTWARE_BILLING_API_KEY=REPLACE_WITH_SERVER_SIDE_KEY
SOFTWARE_BILLING_APPLICATION_ID=REPLACE_WITH_APPLICATION_ID

Voici un helper JavaScript à utiliser uniquement côté serveur, dans un environnement disposant de fetch. Il appelle les routes REST avec un corps JSON simple. Les dates reçues sont des chaînes ISO 8601 ; le client oRPC gère pour sa part la sérialisation des Date.

const billingBase = new URL(
  '/api/v1/admin/software-billing/',
  process.env.SOFTWARE_BILLING_API_URL,
);

async function billingRequest(path, { method = 'GET', body } = {}) {
  const response = await fetch(new URL(path, billingBase), {
    method,
    headers: {
      'x-api-key': process.env.SOFTWARE_BILLING_API_KEY,
      'content-type': 'application/json',
    },
    body: body === undefined ? undefined : JSON.stringify(body),
    redirect: 'error',
    signal: AbortSignal.timeout(10_000),
  });
  if (!response.ok) {
    throw new Error(`Bookelio billing request failed: HTTP ${response.status}`);
  }
  return response.json();
}

Ne placez pas la clé dans une URL, un composant navigateur ou une variable publique. L'URL de connexion est celle de l'API ; les pages de paiement sont hébergées par le dashboard du vendeur, qui peut avoir un autre domaine.

Créer le logiciel et les plans

La configuration du catalogue est une opération d'administration à effectuer une fois, pas à chaque connexion d'utilisateur. Conservez les id renvoyés par Bookelio. Les code sont des références lisibles et ne remplacent pas ces identifiants dans les appels.

Pour réutiliser un catalogue déjà créé dans le dashboard, GET /applications renvoie { applications: [...] } et GET /applications/{applicationId}/plans renvoie { plans: [...] }, sous le préfixe REST indiqué plus haut. Cette dernière liste inclut les plans inactifs : filtrez isActive avant de proposer une offre à l'achat. GET /applications/{applicationId}/subscriptions permet au vendeur de lister les abonnements avec une réponse { subscriptions: [...] }.

const application = await billingRequest('applications', {
  method: 'POST',
  body: {
    name: 'Example Workspace',
    code: 'example-workspace',
    features: [
      { key: 'projects', name: 'Projets' },
      { key: 'exports', name: 'Exports' },
    ],
  },
});

const enterprise = await billingRequest('plans', {
  method: 'POST',
  body: {
    applicationId: application.id,
    name: 'Enterprise',
    code: 'enterprise-monthly',
    interval: 'MONTHLY',
    basePriceCents: 400000,
    seatPriceCents: 0,
    includedSeats: 0,
    trialDays: 14,
    gracePeriodDays: 7,
    vatPolicyId: 'REPLACE_WITH_SELLER_VAT_POLICY_ID',
    featureKeys: ['projects', 'exports'],
  },
});

Ce plan coûte 4 000 € HTVA par mois, indépendamment du nombre de sièges, après un essai de 14 jours. Il accorde ensuite sept jours de grâce au début de chaque cycle payant pour laisser le temps de régler. seatPriceCents: 0 désactive le supplément par siège ; aucun champ pricingMode n'est requis par l'API.

Modèle fictifbasePriceCentsseatPriceCentsincludedSeatsRésultat HTVA
Gratuit000Aucun paiement
Enterprise fixe400000004 000 € par période, quel que soit l'effectif
Base et sièges supplémentaires1000300219 € par période pour 5 sièges
Tous les sièges payants0500025 € par période pour 5 sièges

Les prix sont des entiers en centimes EUR HTVA. interval vaut MONTHLY ou YEARLY : pour une offre annuelle, fournissez le prix de l'année entière, pas un prix mensuel à multiplier. trialDays va de 0 à 365 ; zéro désactive l'essai. Un plan entièrement gratuit ne crée pas d'essai payant ni de demande de paiement, même si une durée a été saisie.

gracePeriodDays est un entier de 0 à 365, indépendant de l'essai. Il est facultatif à la création : l'omettre revient à 0 et conserve le fonctionnement sans grâce. Avec PATCH /plans/{planId}, l'omettre conserve la valeur actuelle ; envoyer 0 désactive la grâce pour les futurs cycles. Les plans existants restent sans grâce tant que cette valeur n'est pas modifiée.

Les clés de featureKeys doivent déjà exister dans le logiciel. Une politique de TVA appartenant au vendeur est obligatoire dès qu'un prix fixe ou par siège est positif. Le nombre de sièges d'un abonnement est un entier de 1 à 100 000. Cette borne technique ne transforme pas le tarif fixe en prix par siège et ne constitue pas une limite commerciale d'utilisateurs incluse dans le plan.

Identifier le client et créer son abonnement

customerId désigne le client à facturer dans l'organisation vendeuse. externalId désigne le compte de ce client dans votre application, par exemple un identifiant d'organisation stable. Stockez cette correspondance côté serveur. Évitez un nom ou une adresse e-mail susceptible de changer.

L'identité est unique par couple (applicationId, externalId). Dans le code suivant, accountId doit provenir de votre session vérifiée et de vos contrôles d'appartenance, et non d'un identifiant libre envoyé par le navigateur. Le plan et le client doivent également être sélectionnés et validés côté serveur.

const subscription = await billingRequest('subscriptions', {
  method: 'POST',
  body: {
    applicationId: process.env.SOFTWARE_BILLING_APPLICATION_ID,
    planId: selectedPlanId,
    customerId: billingCustomerId,
    externalId: accountId,
    seats: memberCount,
  },
});
// Conserver subscription.id pour les modifications et résiliations.

Une nouvelle tentative avec le même logiciel, la même référence externe, le même client et le même plan retrouve l'abonnement existant. Elle ne redémarre pas l'essai, ne refacture pas et ne modifie pas les sièges. Un client ou plan différent pour cette référence produit un conflit. Pour changer de plan, utilisez l'opération de modification ; si le client diffère, vérifiez votre correspondance d'identités, car cette opération ne remplace pas le client de facturation. Les créations de logiciels et de plans, elles, ne sont pas des opérations de création-ou-mise-à-jour ; ne les rejouez pas comme l'inscription d'un client.

Vérifier les fonctionnalités avant une action

Utilisez subscriptions.entitlements, et non le seul statut enregistré ou le nom du plan. Cette lecture vérifie la période en cours, son règlement net et son éventuel délai de grâce, et retourne les clés de fonctionnalités figées pour ce cycle.

async function canUseFeature(verifiedAccountId, featureKey) {
  const appId = encodeURIComponent(
    process.env.SOFTWARE_BILLING_APPLICATION_ID,
  );
  const externalId = encodeURIComponent(verifiedAccountId);
  try {
    const rights = await billingRequest(
      `applications/${appId}/entitlements/${externalId}`,
    );
    return rights.access === true
      && typeof rights.validUntil === 'string'
      && Date.parse(rights.validUntil) > Date.now()
      && Array.isArray(rights.featureKeys)
      && rights.featureKeys.includes(featureKey);
  } catch {
    // Renvoyer un refus temporaire ; ne pas ouvrir les fonctions payantes.
    return false;
  }
}

Exemple fictif de réponse REST :

{
  "subscriptionId": "example-subscription",
  "status": "PAST_DUE",
  "access": true,
  "featureKeys": ["projects", "exports"],
  "validUntil": "2026-09-29T10:00:00.000Z",
  "seats": 5,
  "isInGracePeriod": true,
  "graceEndsAt": "2026-09-29T10:00:00.000Z"
}

Cet exemple représente le premier cycle payant d'un essai terminé le 22 septembre à 10 h UTC : le règlement est encore attendu, mais les droits restent ouverts jusqu'au 29 septembre à 10 h UTC grâce aux sept jours configurés. PAST_DUE ne suffit donc pas à refuser une action ; utilisez access, la clé demandée et validUntil comme dans le helper.

Sans abonnement, la réponse contient subscriptionId: null, status: null, access: false, featureKeys: [], validUntil: null, seats: 0, isInGracePeriod: false et graceEndsAt: null. Si vous ajoutez un cache, isolez-le par vendeur, logiciel et référence externe, limitez sa durée et ne servez jamais un accord au-delà de validUntil. Le module ne fournit pas de webhook sortant de changement d'abonnement à l'application : prévoyez ces lectures aux points de contrôle de votre serveur.

Les lectures entitlements et getByExternalId, ainsi que syncSeats, conservent leurs contrôles de clé, de permissions et de vendeur, mais restent accessibles sans vérifier l'accès du vendeur à l'interface du module. Cela permet de consulter et régulariser les abonnements existants ; les autres opérations restent protégées par l'activation du module.

Appliquer le délai de grâce de paiement

Le cycle conserve un instantané de gracePeriodDays à sa création ; graceEndsAt est calculé à partir de cette valeur et des dates immuables de la période. Pour un cycle payant, la grâce court de periodStart jusqu'à la première des deux dates : periodStart + gracePeriodDays × 24 heures ou periodEnd. L'instant de fin est exclu : à cette date, un cycle toujours impayé ne donne plus accès. La grâce concerne aussi le premier cycle payant et celui qui suit l'essai, sans modifier les dates de facturation ni créer un nouvel essai.

Pendant la grâce, le statut calculé reste PAST_DUE, access vaut true, isInGracePeriod vaut true et validUntil vaut graceEndsAt. Le paiement intégral vérifié fait passer les droits à une période réglée : isInGracePeriod: false et validUntil: periodEnd. Une grâce expirée avec paiement toujours insuffisant donne access: false, featureKeys: [] et validUntil: null, sans attendre le worker.

graceEndsAt est une métadonnée, pas une preuve d'accès : la date reste visible après règlement ou expiration. Elle vaut null pour un cycle sans grâce, gratuit ou d'essai. Seul isInGracePeriod indique que l'accès actuel utilise cette tolérance. N'ajoutez aucun délai local en cas d'échec de l'API et ne prolongez pas un cache jusqu'à cette date sans vérifier access et validUntil.

Ces champs sont ajoutés en révision compatible V1-r28. Les réponses d'une ancienne instance V1 peuvent omettre gracePeriodDays, graceEndsAt et isInGracePeriod : ne déduisez aucune grâce de cette absence et continuez à utiliser access et validUntil pour décider. L'instance vendeuse doit être mise à jour pour configurer et appliquer cette fonctionnalité.

La durée et la date sont figées pour chaque cycle : une modification du plan, une nouvelle tentative du worker ou un paiement tardif ne les déplace pas. Le worker doit d'abord créer le nouveau cycle ; avant ce passage, un ancien cycle expiré n'accorde pas de grâce anticipée. Un retard de traitement peut donc créer une brève interruption, mais pas reporter le début du délai à l'heure d'exécution du worker.

Un paiement partiel ne prolonge pas la grâce. Un remboursement réalisé ou en cours empêche la grâce ; un remboursement échoué (FAILED) ne la bloque pas à lui seul. Une résiliation effective refuse tout accès, et une résiliation programmée ne prolonge pas les droits au-delà de leur limite. La grâce ne marque aucun paiement comme réalisé, ne déclenche pas de facture à elle seule et ne permet pas l'émission de nouvelles échéances tant que le cycle reste impayé.

Présenter le règlement et suivre les statuts

const appId = encodeURIComponent(process.env.SOFTWARE_BILLING_APPLICATION_ID);
const externalId = encodeURIComponent(accountId);
const { subscription } = await billingRequest(
  `applications/${appId}/subscriptions/by-external-id/${externalId}`,
);
const currentCycle = subscription?.cycles.find(
  (cycle) => cycle.periodStart === subscription.currentPeriodStart,
);
const paymentUrl = currentCycle?.paymentUrl ?? null;

Cette lecture retourne au plus les 20 cycles les plus récents. Affichez le paymentUrl du cycle à régler, tel que renvoyé par le vendeur. Ne reconstruisez pas ce lien avec le domaine de votre application. S'il est nul pour un cycle payant, vérifiez la configuration BOOKELIO_WEB_URL du vendeur. Un plan gratuit ou un essai n'a pas de demande de paiement.

StatutConséquence pour l'intégration
TRIALINGEssai en cours ; vérifier access, les clés et validUntil
ACTIVEPériode gratuite ou réglée ; continuer à vérifier les droits calculés
PAST_DUERèglement attendu ; accès temporaire possible pendant la grâce, à vérifier via access, isInGracePeriod et validUntil
CANCELEDAbonnement résilié ; aucune fonctionnalité accordée par cet abonnement

Le worker traite les renouvellements chaque minute. À la fin d'un essai ou d'une période réglée, il crée la prochaine échéance payante. Une échéance non réglée bloque la création des suivantes. Des droits expirés peuvent être refusés avant le prochain passage du worker : n'accordez pas de délai supplémentaire sur la base d'un ancien statut.

Le client règle via les moyens de paiement du vendeur, ou le vendeur enregistre un règlement externe. Les confirmations vérifiées rapprochent le paiement ; un règlement complet déclenche par défaut la facture, ou réutilise celle déjà émise à la création de la demande selon le réglage vendeur. Une redirection réussie du navigateur ne prouve pas le paiement : relisez les droits côté serveur. Les renouvellements utilisent des demandes de paiement et non un prélèvement récurrent automatique. Les paiements partiels et remboursements peuvent retirer le droit correspondant à un cycle intégralement réglé.

Configurer le déclenchement des factures

Ce réglage appartient à l'organisation vendeuse, pas au plan ni à l'application cliente. Il est disponible dans Paramètres → Facturation → Factures des abonnements logiciels et via admin.invoicing.settings.get / .update. Les routes REST sont GET et PUT /api/v1/admin/invoicing/settings/automation : elles n'utilisent pas le préfixe software-billing du helper précédent.

softwareSubscriptionInvoiceTimingComportement
PAYMENT_COMPLETEDValeur par défaut : génération après règlement intégral vérifié
PAYMENT_REQUEST_CREATEDGénération dès la création de la demande de paiement, même impayée

Le champ est facultatif en V1-r28. Omettre le champ dans une mise à jour conserve le choix existant ; l'absence de configuration vendeur conserve PAYMENT_COMPLETED. L'opération PUT demande aussi les booléens bookings, trainings et trainingPaymentReminderInvoices : relisez les paramètres avec GET et renvoyez leurs valeurs actuelles avec le nouveau softwareSubscriptionInvoiceTiming, afin de ne pas modifier ces automatisations indépendantes. L'activation du mode à la création vérifie les services e-mail et stockage du vendeur ; un prérequis manquant empêche l'enregistrement.

Bookelio fige le choix dans chaque nouveau cycle payant. Il s'applique donc aux nouveaux abonnements, aux sorties d'essai, aux renouvellements et aux reprises créant une période, mais pas aux demandes existantes. Un cycle gratuit ou d'essai sans demande de paiement ne crée aucune facture. La génération à la création intervient après validation de la transaction d'abonnement ; les erreurs sont réessayées par le worker et ne doivent pas conduire votre application à recréer la souscription.

Pour le mode à la création, la date d'émission et l'échéance correspondent toutes deux à la date de création de la demande de paiement : la facture est exigible à l'émission, et non à graceEndsAt. Ces dates ne sont pas repoussées lors des nouvelles tentatives. La génération à la création et celle après règlement réutilisent la même facture : le paiement ultérieur ne crée pas de doublon et ne remplace pas le PDF/XML existant. Consultez la demande liée pour l'état actualisé du règlement.

L'émission d'une facture ne crée aucun paiement et ne modifie pas access. La grâce du plan et le règlement net vérifié restent indépendants du moment de facturation. Cette règle vaut aussi lorsque Bookelio est lui-même client de l'instance vendeuse via URL.

Synchroniser les sièges et changer de plan

Pour une application tierce, définissez ce qu'est un siège et calculez la quantité sur votre serveur. Bookelio n'observe pas automatiquement les utilisateurs de votre application et ne crée pas de quota de connexion à partir de ce nombre.

await billingRequest(
  `applications/${appId}/subscriptions/by-external-id/${externalId}/seats`,
  { method: 'PUT', body: { seats: 12 } },
);

await billingRequest(`subscriptions/${encodeURIComponent(subscriptionId)}`, {
  method: 'PATCH',
  body: { planId: nextPlanId, seats: 12 },
});

Sans change, ces modifications alimentent pendingSeats et pendingPlanId pour le prochain renouvellement. Elles ne recalculent ni le prix ni les droits du cycle actuel et n'effectuent pas de prorata. La quantité connue par Bookelio au début de la nouvelle période est figée. Modifiez aussi les sièges d'un forfait fixe si vous souhaitez suivre l'effectif : avec un prix par siège à zéro, son montant reste identique.

Pour modifier les tarifs, fonctionnalités ou délais de grâce d'un plan pour ses futurs cycles, utilisez PATCH /plans/{planId} avec les champs à modifier, par exemple {"basePriceCents": 450000, "gracePeriodDays": 7}. Les cycles existants conservent leurs conditions. Mettre un logiciel ou un plan à isActive: false empêche les nouveaux abonnements concernés ; cela ne résilie pas les contrats existants.

Appliquer un changement immédiat avec prorata

V1-r29 ajoute change facultatif aux deux opérations précédentes. effectiveAt: 'NEXT_PERIOD' conserve la programmation et n'accepte que proration: 'NONE'. Pour agir maintenant, envoyez effectiveAt: 'IMMEDIATE' avec une clé d'idempotence stable de 1 à 100 caractères. proration vaut NONE par défaut : choisissez explicitement le traitement financier voulu.

await billingRequest(`subscriptions/${encodeURIComponent(subscriptionId)}`, {
  method: 'PATCH',
  body: {
    planId: upgradedPlanId,
    seats: 12,
    change: {
      effectiveAt: 'IMMEDIATE',
      proration: 'CHARGE_AND_CREDIT',
      idempotencyKey: 'example-account-change-2026-09-08-001',
    },
  },
});

Pour les sièges seuls, ajoutez le même objet change au corps { seats: 12 } du PUT .../seats. Dérivez toujours le compte, la quantité et le plan autorisé côté serveur. Conservez la clé pour les retries de la même commande ; une clé nouvelle désigne une nouvelle action. Réutiliser une clé avec d'autres paramètres est un conflit. Ne générez pas une nouvelle clé à chaque tentative réseau.

prorationEffet sur la période restante
NONEApplique le plan/les sièges sans ajustement financier
CHARGE_ONLYFacture seulement les hausses ; aucune remise pour les baisses
CHARGE_AND_CREDITFacture les hausses et reporte les baisses financées éligibles comme remise commerciale

Le calcul couvre le montant fixe et les sièges, selon la fraction exacte de temps UTC restant dans la période calendaire, arrondie aux centimes. Il ne suppose pas des mois de 30 jours. À mi-période, une hausse fictive de 100 € à 160 € HTVA produit 30 € HTVA avant TVA ; les changements de sièges n'affectent pas un plan à seatPriceCents: 0.

L'abonnement doit être non résilié, sa période encore valide et toutes ses obligations financières réglées. Un impayé, même en grâce, bloque l'ajustement. La nouvelle périodicité doit être identique ; programmez les passages mensuel/annuel au renouvellement. Durant un essai ou une offre, le changement conserve la date de fin gratuite et ne génère ni montant dû ni crédit. Pour réactiver un abonnement résilié, omettez change ou utilisez NEXT_PERIOD sans prorata.

Le serveur ajoute un cycle immuable dont previousCycleId désigne le précédent. currentPeriodStart devient la date du changement, mais la fin initiale et l'ancrage de renouvellement restent inchangés. Les anciennes factures et demandes restent intactes. Un supplément à régler crée une demande avec les règles de facture et de grâce figées sur ce nouveau cycle ; les droits suivent le règlement ou cette grâce. Relisez entitlements après l'appel : le succès du changement n'est pas une preuve de paiement et ne débite aucune carte automatiquement.

creditGrantedCents retrace la remise accordée, creditAppliedCents la remise consommée et creditBalanceCents le solde restant, en centimes HTVA pour ce seul abonnement. Ce solde réduit de prochaines charges avant TVA : ce n'est ni un avoir comptable sur une facture antérieure, ni un remboursement bancaire, ni un paiement. Aucune demande négative ni paiement fictif n'est créé. La remise est plafonnée à la valeur financée ; un essai, une offre ou une hausse gratuite ne crée pas de valeur créditable. Les remboursements génériques d'un abonnement ayant des ajustements sont refusés tant qu'un parcours de réconciliation dédié n'est pas disponible.

Accorder des mois gratuits par API

À la création, ajoutez par exemple offer: { label: 'Offre de bienvenue', freeMonths: 1 } au corps du POST /subscriptions. Pour un abonnement existant, utilisez subscriptions.grantOffer :

await billingRequest(
  `subscriptions/${encodeURIComponent(subscriptionId)}/offers`,
  {
    method: 'POST',
    body: {
      label: 'Un mois offert — exemple fictif',
      freeMonths: 1,
      idempotencyKey: 'example-commercial-offer-001',
    },
  },
);

Le libellé comporte de 1 à 200 caractères, freeMonths est un entier de 1 à 36 et la clé comporte de 1 à 100 caractères. Une seule offre peut être en attente. Rejouer la même clé et les mêmes données n'ajoute pas de mois, même après application ; changer les données sous cette clé produit un conflit. Rejouer la création d'un abonnement existant ne lui ajoute pas d'offre : utilisez l'opération dédiée. Un abonnement résilié doit être réactivé avant l'attribution.

L'offre initiale fournie lors d'un retry de création doit correspondre à celle déjà enregistrée ; une offre absente ou différente dans l'abonnement existant produit un conflit. Les réponses exposent au plus les 20 offres les plus récentes, comme les 20 cycles récents : ce n'est pas un export exhaustif de l'historique.

L'offre attend le prochain cycle normalement payant, après l'essai ou la période déjà émise ; sans essai, elle peut commencer à la création. Les impayés restent bloquants et les plans entièrement gratuits la laissent en attente. Aucun document ni montant dû existant n'est modifié. Un mois signifie un mois calendaire UTC même sur un plan annuel, avec adaptation aux fins de mois ; la facturation payante reprend à la fin de l'offre avec ce nouvel ancrage.

Le cycle offert expose isComplimentary: true et offerId, reste ACTIVE et ne crée ni demande de paiement ni facture automatique. Il n'est pas un nouvel essai ; vérifiez toujours access et validUntil. offers expose le libellé, la durée, appliedAt et les dates de période lorsqu'elle a été consommée. Les nouveaux champs de sortie de prorata et d'offres sont facultatifs pour accepter des vendeurs plus anciens : leur absence ne prouve ni remise ni offre. Le vendeur doit être en V1-r29 pour appliquer ces options ; vérifiez sa révision avant de proposer ces actions.

En cas de worker retardé, l'offre ne commence jamais avant sa date d'attribution. Si toute sa fenêtre gratuite aurait déjà expiré avant le traitement, elle démarre à la reprise du worker pour ne pas être consommée entièrement dans le passé. Les dates renvoyées par Bookelio font référence.

Résilier et réactiver

await billingRequest(
  `subscriptions/${encodeURIComponent(subscriptionId)}/cancel`,
  { method: 'POST', body: { cancelAtPeriodEnd: true } },
);

true programme la fin à l'échéance : les droits déjà valides restent utilisables dans leur limite actuelle, sans prolonger une grâce qui expire avant cette date. false demande une résiliation immédiate et coupe aussi une grâce en cours ; il ne signifie pas « annuler une résiliation programmée ». Les demandes de paiement émises et l'historique financier sont conservés.

Une fois le statut CANCELED, envoyez un PATCH /subscriptions/{subscriptionId} avec un planId explicite pour réactiver. La seule synchronisation des sièges ne suffit pas. Une période déjà payée et encore valide reprend sans être refacturée ; après expiration, un nouveau cycle commence. La reprise ne donne pas de nouvel essai et refuse une ancienne échéance non réglée. Relancer POST /subscriptions retrouve l'abonnement existant et ne le réactive pas.

Une modification par subscriptions.update avant que la résiliation programmée soit effective ne retire pas cancelAtPeriodEnd. Depuis V1-r33, l'activation explicite par subscriptions.activate, décrite plus bas, applique le plan maintenant et retire cette programmation après vérification des montants déjà dus.

Connecter Bookelio à une instance de facturation par URL

Cette configuration concerne Bookelio lui-même. Sur l'instance vendeuse, créez un logiciel Bookelio et préparez vos plans. Au démarrage, Bookelio ajoute les clés manquantes de son catalogue, par exemple invoicing, trainings ou softwareBilling, sans écraser les définitions existantes. Sélectionnez ensuite explicitement les fonctionnalités incluses dans chaque plan : l'ajout d'une clé ne la rend pas automatiquement disponible dans les offres. Le module de vente d'abonnements peut ainsi lui-même être inclus dans une offre.

Sur l'API et les workers de l'instance cliente, configurez les trois variables reconnues par Bookelio :

BOOKELIO_BILLING_URL=https://api.billing.example.com
BOOKELIO_BILLING_API_KEY=REPLACE_WITH_SELLER_ORGANIZATION_KEY
BOOKELIO_BILLING_APPLICATION_ID=REPLACE_WITH_BOOKELIO_APPLICATION_ID

La connexion passe toujours par HTTP /rpc/v1, même si l'URL vise la même instance. L'adresse doit être joignable depuis l'API et les workers. Utilisez une URL API sans identifiants intégrés, paramètres de requête ou fragment. Sur l'API et les workers de l'instance vendeuse, BOOKELIO_WEB_URL désigne l'origine de son dashboard public pour les liens de règlement. La clé de connexion a besoin de softwareBilling.read, softwareBilling.manage et customers.create pour la consultation, le provisionnement et la synchronisation ; ajoutez paymentRequests.create pour les inscriptions aux plans.

Lier les organisations au démarrage

Après l'ouverture de son serveur HTTP, l'API cliente provisionne le catalogue manquant et les comptes correspondant aux organisations locales non legacy. Elle utilise l'ID d'organisation comme externalId, son nom et son nombre de membres, propriétaire inclus, avec un minimum d'un siège. Une liaison distante est unique pour le logiciel et cet identifiant. Les relances réutilisent cette liaison ; un scan sans chevauchement réessaie les échecs et découvre les nouvelles organisations chaque minute. Les organisations legacy sont exclues des comptes et de la synchronisation des sièges, mais pas du provisionnement du catalogue commun. Le démarrage n'attend pas une réponse de sa propre URL avant de commencer à écouter.

Si un abonnement distant existe déjà pour cette identité, son client est réutilisé. Sinon, les coordonnées professionnelles locales ne permettent de créer un client de facturation que si le profil nécessaire est complet et valide. Aucun pays ni identifiant fiscal n'est inventé. Un profil incomplet laisse un compte lié sans client ; les informations d'un client déjà lié ne sont pas écrasées. L'opération applications.provision elle-même ne crée aucun plan, abonnement, demande de paiement, facture ou règlement.

Après cette opération, si un abonnement existe déjà, le scan appelle subscriptions.syncSeats sans option immédiate : les sièges sont programmés au renouvellement, sans prorata ni nouvelle obligation de paiement. Un abonnement résilié n'est pas réactivé. Cet appel conserve les automatismes existants : il peut réessayer une facture déjà attendue pour une demande de paiement existante en mode PAYMENT_REQUEST_CREATED, sans créer une nouvelle demande.

Le vendeur doit disposer de V1-r30 et du schéma additif déployé avant d'activer ce démarrage côté client. Une ancienne instance peut encore répondre aux lectures existantes, mais ne propose pas le nouveau provisionnement. La clé doit avoir les deux permissions du provisionnement, même lorsque le lot ne nécessite finalement aucun nouveau client.

L'opération est aussi disponible pour une autre application : applications.provision, soit POST /applications/{applicationId}/provision sous le préfixe REST du module. Exemple fictif sans profil de facturation, donc sans création de client :

const provisioned = await billingRequest(
  `applications/${encodeURIComponent(applicationId)}/provision`,
  {
    method: 'POST',
    body: {
      features: [{ key: 'reporting', name: 'Rapports' }],
      accounts: [{ externalId: 'example-organisation-001', name: 'Organisation exemple', seats: 5 }],
    },
  },
);

Les lots acceptent au plus 200 fonctionnalités et 100 comptes. La réponse identifie le logiciel, les clés et, pour chaque identité, accountId, customerId et subscriptionId ; les deux derniers peuvent être null. Une identité provisionnée n'est donc pas une preuve d'abonnement ou d'accès. Utilisez toujours entitlements pour autoriser les fonctionnalités. Un appel identique ne duplique ni clés ni comptes et ne remplace pas les métadonnées existantes des fonctionnalités.

Attribuer les plans et appliquer les droits distants

Dans le superadmin, la page Facturation vérifie la connexion. L'onglet abonnement d'une organisation peut associer un plan et un client de l'instance vendeuse. Le propriétaire peut aussi choisir un plan dans Paramètres → Mon abonnement Bookelio, après confirmation. Bookelio utilise l'ID de l'organisation cliente comme externalId et synchronise le nombre de membres, propriétaire inclus, avec un minimum d'un siège.

La connexion utilise les mêmes droits calculés, y compris la grâce configurée sur le plan du vendeur. Aucun paramètre de grâce supplémentaire n'est nécessaire sur l'instance cliente, même si elle utilise sa propre URL pour la facturation.

Les offres et ajustements décidés chez le vendeur utilisent aussi cette connexion. La synchronisation automatique des membres Bookelio n'envoie pas d'option immédiate : elle conserve les changements de sièges au renouvellement. La page de votre propre abonnement affiche les offres et le solde de remise en lecture seule ; les modifications commerciales se font chez le vendeur.

Pour les organisations non legacy, le fournisseur Bookelio décide de toutes les fonctionnalités, y compris celles auparavant activées par défaut ou sans tarification. Une clé absente de ses droits est désactivée. Depuis V1-r32, les plans Bookelio passent uniquement par un fournisseur externe : les anciens plans et abonnements locaux, valeurs par défaut et dérogations individuelles n'accordent plus aucun accès. Le catalogue local conserve les définitions, correspondances de clés et l'activation globale, pas les droits commerciaux. Les tables historiques ne sont pas supprimées ; le module vendeur continue à stocker ses propres plans, lus par HTTP même sur la même instance.

Le fournisseur générique BOOKELIO_FEATURE_PROVIDER_* n'est pas interrogé en parallèle de Bookelio. Une configuration Bookelio partielle ou une panne ne déclenche aucun repli. Si les trois variables BOOKELIO_BILLING_* sont absentes, le fournisseur générique peut seul fournir les droits ; sans aucun fournisseur, une organisation non legacy n'a aucune fonctionnalité. Ce fournisseur générique expose uniquement un contrat de lecture : Bookelio n'invente pas d'API d'écriture pour y créer des clés ou des organisations. Le provisionnement décrit ici concerne exclusivement une instance Bookelio configurée par BOOKELIO_BILLING_*.

Exemption explicite des organisations legacy

V1-r32 ajoute Organization.bookelioLegacyAccess, à false par défaut. Seul un superadministrateur peut marquer une organisation legacy, par exemple pour Bookelio lui-même ou un contrat de développement sur mesure. L'opération auditée superadmin.organizations.setLegacyAccess accepte strictement { organizationId, enabled } et répond { id, bookelioLegacyAccess }. Sa route REST est PUT /api/v1/superadmin/organizations/{organizationId}/legacy-access. Un owner ou une clé d'organisation ne peuvent pas l'appeler ; aucune variable générique ni dérogation par fonctionnalité n'accorde cette exemption.

Legacy donne accès à toutes les fonctionnalités globalement actives sans consulter le fournisseur, même absent, mal configuré ou en panne. Il ne contourne ni l'authentification, ni l'isolation des organisations, ni les permissions métier des membres et clés, ni une désactivation globale. Le diagnostic expose l'objet facultatif providers.legacy: { active: true } et conserve la source V1 manual. Les anciennes routes de dérogations restent disponibles pour compatibilité, mais leurs données n'accordent plus de droits.

Les lectures admin.organization.subscription.get/options exposent le booléen facultatif bookelioLegacyAccess. Pour legacy, elles renvoient connected: false, sans lecture distante, avec subscription: null pour get et aucun plan pour options. Ce résultat signifie une exemption et non une panne du fournisseur. La page Mon abonnement Bookelio l'explique sans sélecteur ; configure refuse un plan pour une organisation legacy.

Activer l'exemption ne résilie pas un abonnement déjà présent chez le vendeur, n'arrête pas ses factures futures et n'efface aucun impayé ou document. Gérez séparément et explicitement cet abonnement chez le vendeur. Désactiver legacy rétablit les contrôles externes ; la synchronisation peut reprendre sans créer automatiquement de souscription.

Avant de déployer V1-r32, faites générer, revoir et appliquer manuellement la colonne additive Organization.bookelioLegacyAccess Boolean @default(false). Préparez les droits externes ou marquez explicitement les seules organisations legacy prévues lors de la bascule. Aucun marquage automatique des anciens clients ni suppression des tables historiques ne doit accompagner cette étape : sans fournisseur, une organisation non legacy n'obtient plus les droits locaux précédents.

Choix du plan par le propriétaire via l'API cliente

V1-r31 ajoute deux opérations sur l'API de l'instance cliente, pas sur l'API du vendeur. Elles exigent une session humaine et une appartenance owner encore présente en base dans l'organisation active. Une clé API, un administrateur non propriétaire ou une autre organisation ne peuvent pas les utiliser. Le contrôle ne dépend pas du module de vente, afin de pouvoir régulariser un accès expiré. Les autres rôles autorisés conservent la consultation historique admin.organization.subscription.get.

Opération oRPCRoute RESTEffet
admin.organization.subscription.optionsGET /api/v1/admin/organization/subscription/optionsLecture seule : connected, plans actifs, seats calculés côté serveur et billingDetailsRequired
admin.organization.subscription.configurePUT /api/v1/admin/organization/subscription/planAprès confirmation : sélection avec { planId, expectedOrganizationId, activation? }, retour de l'abonnement Bookelio

expectedOrganizationId reprend l'organisation pour laquelle le propriétaire vient de confirmer. C'est uniquement une précondition : si la session a changé d'organisation entre-temps, l'API refuse avec CONFLICT avant toute écriture. La cible, l'identité externe et les sièges restent dérivés de la session et de la base ; aucun prix, client, quantité, offre ou paramètre de prorata supplémentaire n'est accepté.

La lecture des options ne crée aucun client ni abonnement. Lors de la configuration, le serveur réutilise le client de l'abonnement ou du compte distant. Le résultat vendeur subscriptions.getByExternalId expose désormais un account facultatif pour cette liaison avant abonnement ; les anciennes réponses sans ce champ restent acceptées. Sans client lié, un profil professionnel complet est transmis au provisionnement existant ; des informations insuffisantes nécessitent de compléter Paramètres → Informations de l'entreprise avant de confirmer. Aucun pays ou identifiant fiscal n'est inventé, et un client déjà lié n'est pas réécrit.

Depuis V1-r33, le dashboard envoie activation: { idempotencyKey } pour appliquer le plan immédiatement. La clé reste identique pour les nouvelles tentatives de la même confirmation. Sans ce champ facultatif, les anciens appels gardent leur comportement : création initiale, nouveau plan au renouvellement ou reprise protégée. Un premier choix respecte la gratuité et l'essai du plan ; un choix ultérieur ne recrée aucun essai.

Le serveur appelle alors admin.softwareBilling.subscriptions.activate chez le vendeur (POST /api/v1/admin/software-billing/subscriptions/activate) avec l'entrée stricte { applicationId, externalId, customerId, planId, seats, idempotencyKey }. Cette opération atomique exige softwareBilling.manage et paymentRequests.create et conserve un reçu pour chaque activation réussie, même si le plan est déjà appliqué. La clé et l'empreinte empêchent une double charge ou la réapplication d'un ancien choix après une modification ultérieure. Le prix et le traitement financier sont calculés par le vendeur, jamais choisis dans le navigateur.

La politique est CHARGE_ONLY : à périodicité identique, seul l'écart positif est facturé au prorata du temps restant, jusqu'à la fin initiale. Un changement de périodicité commence une nouvelle période maintenant et déduit la valeur HTVA restante du service dont le financement est vérifié ; l'excédent éventuel n'est ni reporté ni remboursé. Un essai ou une période offerte conserve sa fin gratuite, même si la périodicité change. Une période expirée et réglée recommence maintenant, sans rattrapage de périodes anciennes. L'activation explicite annule aussi une résiliation déjà programmée.

Un choix identique à l'instantané courant, sans changement en attente, retrouve la demande de paiement éventuelle sans autre charge. Les obligations impayées empêchent une configuration différente. Réappliquer le plan courant permet de rafraîchir des fonctionnalités figées périmées, par exemple celles d'un ancien cycle gratuit. Les cycles, paiements et factures passés restent intacts. Relisez l'abonnement et ses droits : la page propose Payer maintenant pour une demande due, mais confirmer un plan ne débite pas automatiquement une carte et ne prouve pas son règlement. Les fonctionnalités payantes suivent le paiement vérifié ou la grâce applicable. Les offres restent des décisions du vendeur.

La clé serveur vers le vendeur conserve les permissions softwareBilling.read, softwareBilling.manage, customers.create et paymentRequests.create. Elle n'est jamais envoyée au navigateur. Les appels restent HTTP /rpc/v1, même pour la même instance. Faites générer, revoir et appliquer manuellement la table additive BookelioSoftwareActivation, sa relation et sa clé unique avant de déployer le vendeur V1-r33, puis le client qui demande l'activation. Un vendeur ancien ne reçoit pas de programmation différée en remplacement. Les schémas additifs existants des cycles et comptes et la colonne legacy restent requis ; aucun reçu historique ne doit être inventé et aucune variable d'environnement supplémentaire n'est nécessaire. Sans connexion ou pour une organisation legacy, aucun plan ne peut être choisi par ce parcours.

Avant une activation propriétaire, le serveur lit le registre public /api-versions.json du vendeur, sans transmettre la clé API. Une entrée V1 de révision 33 ou supérieure est requise avant tout provisionnement ou activation. Un vendeur plus ancien peut afficher ses plans, mais le parcours propriétaire refuse l'activation avec une erreur SERVICE_UNAVAILABLE demandant sa mise à jour ; cela ne signifie pas que le plan ou le compte a disparu. Si le registre est indisponible ou invalide, l'erreur précise que la version ne peut pas être vérifiée : rétablissez ce point d'accès avant de réessayer. Les anciens appels sans activation restent inchangés.

Diagnostiquer et valider l'intégration

SymptômeVérification
Clé refusée ou accès interditClé d'organisation valide, permissions de l'opération et module activé pour le vendeur
Fonctionnalités absentes sans fournisseurConfigurer les droits externes ou faire vérifier l'exception legacy par un superadministrateur ; aucun plan local ne sert de repli
Organisation legacy sans sélecteur de planComportement prévu ; l'exemption ne résilie pas un éventuel abonnement chez le vendeur
Choix du plan Bookelio refuséSession du propriétaire de l'organisation active ; aucune clé API ne remplace ce contrôle. Après un changement d'organisation, recharger avant de confirmer
Coordonnées demandées avant souscriptionCompléter les détails de l'entreprise ou faire vérifier le client déjà lié sur le fournisseur ; aucune donnée fictive ne doit être utilisée
Logiciel, plan ou client introuvableIdentifiants appartenant au même vendeur ; le plan doit appartenir au logiciel
Liaison au démarrage absenteVendeur en V1-r30, migration additive appliquée, URL joignable et permissions softwareBilling.manage / customers.create ; attendre le prochain scan après correction
Compte lié sans abonnement ou clientProfil de facturation incomplet ou plan non attribué ; compléter le profil ou sélectionner explicitement le client et le plan, sans attendre une facturation automatique
Conflit à la créationRéférence externe déjà utilisée ; relire l'abonnement puis modifier plutôt que recréer
Pas de droits après le retour du paiementRèglement complet réellement confirmé, période encore valide, clé de fonctionnalité incluse
PAST_DUE avec accès encore ouvertGrâce active : vérifier isInGracePeriod et validUntil, sans assimiler cet accès à un règlement
Grâce absente ou déjà terminéeValeur figée dans le cycle, début de période, graceEndsAt, résiliation ou remboursement ; changer le plan ne modifie pas l'historique
Nouveau plan ou nouvelle quantité non visibleSans option immédiate, vérifier pendingPlanId et pendingSeats ; sinon relire les droits et la nouvelle demande à régler
Changement immédiat refuséAbsence de dette, permission paymentRequests.create et clé d'idempotence cohérente ; subscriptions.update exige aussi une période valide et la même périodicité, subscriptions.activate exige un vendeur V1-r33 migré
Offre encore en attenteEssai ou période émise en cours, plan gratuit, impayé bloquant ; consulter ses dates dans offers
Aucun lien de paiementCycle gratuit/d'essai ou origine publique du vendeur manquante
Facture attendue dès la demande mais absenteChoix vendeur figé lors de la création du cycle, services de génération/livraison disponibles et worker actif ; aucun effet rétroactif
Renouvellement absentWorker actif, fin de période atteinte, absence d'échéance impayée bloquante

Sur une installation de test préparée pour ce module, vérifiez un plan gratuit, un essai, un paiement incomplet puis complet, un forfait fixe avec différents effectifs et une offre par siège. Testez une grâce désactivée puis activée, sa limite exacte, le paiement pendant et après celle-ci, un remboursement et une modification qui ne change que les futurs cycles. Vérifiez les deux déclenchements de facture, l'absence de doublon après règlement et l'absence d'effet rétroactif sur les demandes existantes. Vérifiez aussi qu'un compte ne peut jamais consulter les droits ou échéances d'un autre compte en changeant un identifiant navigateur. Terminez par un changement programmé, une résiliation et une reprise sans nouvel essai, puis par une panne simulée de la connexion.

Pour les options V1-r29, testez une hausse puis une baisse immédiate, chaque mode de prorata, l'ajout de sièges et les retries de la même clé. Vérifiez une remise consommée sur une prochaine charge et l'absence de crédit après une hausse gratuite. Testez un mois offert sur un plan annuel, après un essai et avec une facture impayée, ainsi que le refus d'une seconde offre en attente. Vérifiez les nouvelles actions avec et sans leurs permissions financières.

Pour V1-r30, testez le démarrage avec une URL distante puis celle de la même instance, deux scans successifs, une fonctionnalité déjà définie chez le vendeur et une organisation créée après démarrage. Vérifiez les profils complets et incomplets, la réutilisation d'un client existant, l'absence de nouvel abonnement ou de nouvelle obligation financière et le refus des clés sans permission. Vérifiez aussi les sièges programmés et la reprise normale des factures attendues sur des demandes existantes. Simulez une panne : aucune valeur locale ni dérogation ne doit contourner les droits du fournisseur configuré.

Pour V1-r31, vérifiez que consulter les options ne crée aucun client ou abonnement, qu'un non-propriétaire ou une clé API ne peut pas configurer le plan, et qu'un changement d'organisation avant confirmation provoque un conflit. Testez la réutilisation d'un client lié, les données manquantes, le premier plan gratuit/avec essai, un changement différé et une reprise sans nouvel essai. Le choix ne doit accepter ni prix, ni client, ni quantité, ni option commerciale fournis par le navigateur.

Pour V1-r32, vérifiez le refus sans fournisseur pour une organisation non legacy, même avec un ancien plan ou une dérogation. Testez legacy avec un fournisseur absent ou en panne, une fonctionnalité inactive et un membre sans permission métier. Vérifiez que seul le superadministrateur modifie ce statut, que l'audit est enregistré et qu'aucun appel de compte, sièges ou choix de plan distant ne part pour legacy. Retirer l'exemption doit rétablir les contrôles externes sans effacer de dette ni créer d'abonnement.

Pour V1-r33, testez l'activation immédiate d'un plan gratuit ou payant, la mise à jour des fonctionnalités du plan courant, le supplément au prorata, le changement de périodicité et la reprise. Vérifiez le paiement proposé après confirmation et rechargement, le refus d'une nouvelle configuration en présence d'une dette, les nouvelles tentatives sans doublon et les anciens appels sans activation toujours différés. Un vendeur ancien doit refuser l'activation sans appliquer silencieusement un changement au renouvellement.

Les échéances, droits et factures sont pilotés par Bookelio. L'application consommatrice utilise l'API pour les consulter et les gérer ; elle ne modifie pas directement les cycles, les statuts de paiement ou la base de données de facturation.