Skip to main content

Contrat Billing — SDK 1.3.0 — cycle de vie, rejeu et pagination

Billing facture vos clients ; il ne conditionne pas votre accès marchand à SahelPay. Tous les SDK 1.3.0 utilisent /v1/billing/*. Orange Money Mali/XOF est le seul moyen intégré, chaque règlement exige une action du client : aucun prélèvement automatique.Un plan définit un montant et une fréquence WEEKLY, MONTHLY, QUARTERLY ou YEARLY. Un abonnement porte une identité client et des périodes ISO UTC. Une facture porte le montant à régler et son lien de paiement. Utilisez une clé de la même application et du même mode pour toutes les opérations d’un cycle.

Identité sans téléphone

La création accepte au moins une identité : customer_id, customer_external_ref, customer_email ou customer_phone. customer_name est facultatif. Le téléphone peut être absent ou null dans les réponses ; ne le remplacez jamais par un numéro inventé. Les profils sont créés par les parcours d’abonnement/portail ; l’API clients expose la liste, sans CRUD direct. external_ref est unique dans l’application.customer_external_ref identifie votre utilisateur ; external_ref identifie l’opération d’abonnement. Avec external_ref sur with-payment, fournissez customer_external_ref et omettez customer_id et customer_phone. L’import suit la même règle et exige une clé API d’application. Les profils clients restent partagés entre TEST et LIVE : utilisez des références de test distinctes.

Parcours en mode TEST

Utilisez le SDK 1.3.0. Vérifiez npm view @sahelpay/sdk version : si le registre propose 1.3.0, installez npm install @sahelpay/sdk@^1.3.0. Si npm est encore à 1.1.0, installez les sources taguées en attendant la publication :
Fournissez une clé TEST dans SAHELPAY_SECRET_KEY. Les exemples ci-dessous ne contiennent aucune vraie clé, aucun téléphone et n’envoient aucun message au client.
Pour un essai, ajoutez trial_end (date ISO UTC future) à createWithPayment. La réponse est ACTIVE, avec invoice: null et payment_link: null. La première facture sera préparée à la fin de l’essai par le worker horaire. create() accepte start_date et trial_end, mais ne produit pas de lien immédiat.Pour vérifier un paiement sans opérateur ni personne réelle, utilisez le simulateur SahelPay avec une clé TEST, mock: true, montant 4000, hosted_checkout: false. Un paiement simulé indépendant ne règle pas à lui seul votre facture Billing : ne déduisez jamais les droits d’un simple payment.success. Les fixtures du dépôt rejouent hors réseau essais, factures, signatures, doublons et réponses de reprise. Aucun test clock public n’est promis. webhook.test vérifie la connectivité, pas le cycle Billing.

Périodes payées, essais et impayés (backend #214)

Relisez le GET canonique avant d’accorder les droits : ses états sont calculés à l’heure de lecture, même lorsque le passage horaire a du retard.La réponse historique de création d’essai reste ACTIVE avec trial_end ; son GET canonique renvoie TRIALING. La création sans essai et sans paiement est INCOMPLETE. Un paiement partiel ne prolonge pas les droits. Reçu, facture PAID et période sont écrits atomiquement ; le rejeu n’accorde pas de seconde période.Le premier paiement complet et le paiement après la grâce accordent un intervalle entier depuis le règlement, sans arriérés. Dans la grâce, le renouvellement garde l’échéance et l’ancre UTC : 31 janvier → 28/29 février → 31 mars. Même règle pour le trimestre et l’année. L’import accepte une ancre historique billing_anchor_at antérieure ou égale au début de période ; les deux bornes importées restent exactes.status reste PAST_DUE après la grâce pour permettre le règlement de la même facture ; utilisez access_status et les dates pour les droits. Le passage du temps ne change pas updated_at : planifiez une relecture à la fin de période/grâce, en plus des signaux webhook. Les champs ajoutés sont facultatifs dans les types SDK pour permettre la lecture d’une ancienne version du backend.

Reprise, idempotence et pagination

Les six créations Billing (plans.create, subscriptions.create, checkout, import, plans.sendLink, invoices.create) acceptent idempotency_key : en-tête X-Idempotency-Key de 1 à 128 caractères, jamais dans le JSON. Le backend réserve l’identité avant la création et conserve la clé sans expiration. Même portée marchand/application/mode/route et même JSON (ordre des propriétés indifférent) : même ressource. Corps différent : 409 IDEMPOTENCY_KEY_REUSED ; clé invalide : 400 INVALID_IDEMPOTENCY_KEY. Gardez la clé stable lors des reprises réseau. Le rejeu renvoie l’état actuel, pas une copie de la première réponse.Sans en-tête, les garanties précédentes subsistent : external_ref déduplique checkout/import, les autres créations ne sont pas dédupliquées. Une référence de checkout exige customer_external_ref, sans ID/téléphone. Après règlement ou annulation de la facture, checkout et plans.sendLink peuvent renvoyer un lien null ; sendLink ajoute alors invoice_status. Aucun nouvel encaissement n’est créé. Renouvelez via invoice_id/payment_url du GET canonique.subscriptions.import() (Python : import_period, PHP : importPeriod) reprend une période courante avec plan_id, external_ref, customer_external_ref, current_period_start, current_period_end. trial_end, si fourni, doit être égal à la fin de période. Aucun paiement, facture ou message n’est créé par l’import.Toutes les listes acceptent take entre 1 et 200 (défaut 50, clients 20), et un cursor opaque. La pagination contient total, take, next_cursor nullable et has_more. Réutilisez le curseur avec les mêmes filtres, application et mode ; total compte tous les résultats filtrés avant le curseur. L’ordre est created_at DESC, id DESC. La page suivante n’insère pas les objets plus récents ; un curseur reste utilisable après suppression de sa ligne.
Python expose plans.list_page() pour les métadonnées ; plans.list() garde sa liste. Les nouveaux champs de pagination sont facultatifs pour lire les anciens serveurs. En PHP, les factures sont dans getData() et la pagination dans getRawData()['pagination']. Les noms de méthodes Python utilisent snake_case, PHP utilise all() pour les listes.

Webhooks : vérifier, dédupliquer, relire

Signature : X-SahelPay-Signature: t=TIMESTAMP,v1=HMAC_HEX, HMAC-SHA256 sur timestamp + '.' + corps_brut, tolérance par défaut 300 secondes. Les trois SDK vérifient la signature et rejettent les corps modifiés ou trop anciens. Les événements sont des signaux de relecture, pas une preuve suffisante de paiement. Dédupliquez sur event.id, supportez le désordre et ignorez les événements inconnus.cancel() émet subscription.cancelled avec un event.id stable. Si votre endpoint filtre les événements, abonnez-le explicitement à ce type, puis relisez le GET canonique. La relecture après l’appel et le polling restent disponibles. Les relances e-mail restent pilotées par le backend ; le SDK n’active aucun envoi.

Prévisualiser une relance, sans envoi

Équivalents : Python client.subscriptions.preview_reminder(application_id, subscription_id) ; PHP $sahelpay->subscriptions->previewReminder($applicationId, $subscriptionId). Référence de la preview.La lecture vérifie marchand, application et mode. Elle exige une clé déjà enregistrée : sinon 401, sans import/migration, création d’application ni mise à jour de la clé. Les tests utilisent une clé TEST existante. La preview ne crée aucun lien, contact, consentement ou événement et n’envoie rien.Le responsable est configuré séparément par l’opérateur dans subscription_reminders_application:<applicationId> : owner et éventuellement renewal_url. Une configuration absente/invalide/inactive ou owner: application ne déclenche aucun rappel SahelPay. Il n’existe pas de setter SDK pour l’activer.Le CTA utilise seulement une facture collectable et un lien existant sûrs dans la même portée. Sans facture, seule l’URL de renouvellement explicitement configurée, HTTPS et de même origine que le site de l’application, peut servir de repli. Une dette historique au mode indéterminé ou un SUCCESS sans reçu bloque ce repli et le rendu éligible : ne construisez pas de lien de remplacement. Les flags restent desactive et aucun transport WhatsApp n’est disponible.

Erreurs

Les messages exacts peuvent varier selon la validation. Voir les codes d’erreur et les types et versions SDK.