Aller au contenu principal
Tous les articles
Technique16 min de lecture

API d'abonnement et de paiement récurrent : clients, souscription, webhooks et tests

API d'abonnement et de paiement récurrent : client et abonnement, un lien de paiement par échéance, webhooks, pause, reprise, annulation et tests en sandbox.

L'API d'abonnement et de paiement récurrent de ChariPay tient en deux créations et trois événements : vous créez un client (POST /v1/clients), puis un abonnement pour ce client (POST /v1/subscriptions) ; à chaque échéance, ChariPay génère un lien de paiement à usage unique et l'envoie par e-mail à votre client, qui le règle sur la page hébergée, par carte avec 3-D Secure ; chaque échéance payée vous parvient par webhook signé. Ce guide déroule chaque étape avec des appels vérifiés contre le contrat de l'API : champs requis et optionnels, premier lien de paiement, rappels, événements subscription.*, pilotage du cycle de vie, idempotence, erreurs et plan de recette en sandbox.

L'API d'abonnement et de paiement récurrent : le modèle

ChariPay est une plateforme de paiement marocaine, un PSP opéré par Chari Money, établissement de paiement agréé par Bank Al-Maghrib. Son module d'abonnements manipule trois objets :

ObjetEndpoint principalCe qu'il porte
ClientPOST /v1/clientsNom, e-mail — l'adresse où partent les liens de paiement —, téléphone
AbonnementPOST /v1/subscriptionsMontant, périodicité, dates, statut, metadata
ÉchéancesGET /v1/subscriptions/{reference}/chargesChaque échéance générée — un lien de paiement à usage unique par période, avec son status et son payUrl —, la plus récente d'abord

Le principe tient en une phrase : un abonnement ChariPay est une suite de liens de paiement. À la date de première échéance (startDate), puis à chaque période selon la périodicité, ChariPay crée un lien de paiement à usage unique pour la période et l'envoie par e-mail à votre client, sous l'objet « Nouvelle échéance » suivi de la description de l'abonnement. Votre client ouvre le lien et paie sur la page hébergée, par carte avec 3-D Secure. Tant que l'échéance n'est pas payée, un rappel part chaque jour par e-mail (« Rappel de paiement »), avec le même lien, et aucune nouvelle période n'est ouverte : les impayés ne s'accumulent pas.

Le statut d'un abonnement suit un cycle simple. Côté API, ce sont les valeurs de l'énumération status, que voici avec leur équivalent en clair :

Valeur APIEn clairSignification
INCOMPLETEPas encore actifCréé ; la première échéance n'est pas encore payée
ACTIVEActifPremière échéance payée (activatedAt renseigné) ; un lien part à chaque échéance
PAUSEDEn pauseAucun nouveau lien n'est généré jusqu'à la reprise
COMPLETEDTerminéDate de fin atteinte et dernière échéance payée
CANCELEDAnnuléAnnulé par vous, ou échéance restée impayée au-delà du délai de 14 jours

Les conventions sont celles de toute l'API : une seule base, https://api-psp.charipay.ma, en sandbox comme en production — c'est la clé qui choisit l'environnement (chari_sk_test_… ou chari_sk_live_…) ; l'en-tête X-CHARI-PAY-API-KEY sur chaque requête ; du JSON en camelCase ; des montants en MAD, en unités majeures (149.90 = 149,90 MAD) ; des champs optionnels absents, jamais null ; des énumérations en majuscules, dont votre code doit tolérer les nouvelles valeurs. Le panorama de l'API est dans API de paiement au Maroc, et l'offre côté métier sur la page abonnements et paiements récurrents.

Pour exécuter les appels qui suivent, il vous faut une clé chari_sk_test_…. Le mode test s'ouvre dès l'activation de votre compte, sans validation à attendre : Commencer en mode test — trois champs (nom, e-mail professionnel, entreprise), un lien d'activation reçu par e-mail pour choisir votre mot de passe, puis le portail, où vous créez vous-même votre clé de test après une ré-authentification ; la clé complète ne s'affiche qu'une seule fois. Payez avec la carte de test 4918 9141 0719 5005, CVV 123, n'importe quelle date d'expiration future, code 3-D Secure 555 : seule cette carte est acceptée, et aucun argent réel ne circule. La vérification de votre entreprise (KYB) se fait en parallèle et ne conditionne que le passage en production.

Étape 1 : créer le client

Un abonnement se rattache toujours à un client existant. Le client se crée une fois et se réutilise : pour un deuxième abonnement, pour un changement de formule.

bash
curl -X POST 'https://api-psp.charipay.ma/v1/clients' \
  -H 'X-CHARI-PAY-API-KEY: chari_sk_test_...' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Amine Bennani",
    "email": "amine.bennani@example.com",
    "phone": "+212600000000"
  }'

Seul name est requis par le schéma du client. Fournissez pourtant toujours email quand le client sera abonné : c'est à cette adresse que partent le lien de paiement de chaque échéance et les rappels. Le phone est optionnel et suit le format E.164. La réponse 201 contient l'id du client, un UUID : conservez-le dans votre base, à côté de votre propre identifiant client.

Le module compte cinq appels : la création, la liste (GET /v1/clients), la lecture (GET /v1/clients/{id}), la mise à jour (PUT /v1/clients/{id}), utile pour corriger une adresse e-mail avant la prochaine échéance, et la suppression (DELETE /v1/clients/{id}), refusée tant que le client a encore des abonnements actifs. Le détail des champs est dans le module « Clients » de la documentation.

Étape 2 : créer l'abonnement

L'abonnement porte la formule : montant, périodicité, dates, et ce dont vous aurez besoin pour le reconnaître dans vos webhooks. Le contrat compte neuf champs, pas un de plus :

ChampRequisFormat et rôle
clientIdOuiUUID du client créé à l'étape 1
amountOuiMontant de chaque échéance, en MAD, unités majeures
descriptionOuiLibellé repris sur chaque lien de paiement, 255 caractères au plus
frequencyOuiDAILY, WEEKLY, MONTHLY ou YEARLY
startDateOuiDate de la première échéance, au format AAAA-MM-JJ
channelsOuiCanal d'envoi des liens de paiement : ["EMAIL"]
endDateNonDate de fin ; sans elle, l'abonnement est sans terme
externalIdNonVotre identifiant d'abonnement, unique par compte et par environnement ; il rend la création rejouable
metadataNonObjet plat, 4 Ko au plus, repris sur chaque lien de paiement et dans chaque webhook de l'abonnement
bash
curl -X POST 'https://api-psp.charipay.ma/v1/subscriptions' \
  -H 'X-CHARI-PAY-API-KEY: chari_sk_test_...' \
  -H 'Content-Type: application/json' \
  -d '{
    "clientId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "amount": 149.90,
    "description": "Gold plan - monthly",
    "frequency": "MONTHLY",
    "startDate": "2026-11-01",
    "channels": ["EMAIL"],
    "externalId": "sub-cus-1001-gold",
    "metadata": { "customerId": "cus_1001", "plan": "gold" }
  }'

La réponse 201 — ou 200 si l'externalId existe déjà — décrit l'abonnement : sa reference (sub_…), qui sert à tous les appels suivants, son status — INCOMPLETE pour un abonnement qui vient d'être créé —, son origin (API, ou DASHBOARD pour un abonnement créé depuis le portail), sa prochaine échéance (nextRunDate, la date où la prochaine période s'ouvre et où son lien part par e-mail) et, quand une période est ouverte, currentCharge, le lien de paiement de la période en cours. Les dates s'entendent sur le fuseau Africa/Casablanca. Dans metadata, mettez des identifiants opaques — votre numéro de client, votre code de formule —, jamais de donnée de carte ni de donnée personnelle superflue. Le schéma complet est dans le module « Abonnements » de la documentation.

Étape 3 : le premier lien de paiement et les rappels

Un abonnement nouvellement créé reste INCOMPLETE tant que sa première échéance n'est pas payée. Cette première échéance est un lien de paiement comme les suivantes : votre client le reçoit par e-mail et le règle sur la page hébergée, par carte avec 3-D Secure. Votre serveur ne voit jamais de donnée de carte.

Le contrat de l'API décrit comment cette première échéance se présente :

  • Si `startDate` est aujourd'hui ou dans le passé, le premier lien est créé immédiatement et renvoyé dans currentCharge, un objet lien de paiement qui porte le payUrl destiné à l'acheteur : vous pouvez y diriger votre client dès la fin de son inscription, sans attendre qu'il ouvre l'e-mail.
  • Si `startDate` est dans le futur, aucun lien n'est encore créé : le premier part à cette date. Relisez alors l'abonnement par GET /v1/subscriptions/{reference} ; une fois la période ouverte, son currentCharge porte l'échéance en cours.

Une fois le premier paiement réussi, le statut passe à ACTIVE et le champ activatedAt indique quand. Le même scénario se répète ensuite à chaque échéance : un nouveau lien, un e-mail, un paiement sur la page hébergée, un webhook.

Quand une échéance n'est pas payée, votre client reçoit un rappel par e-mail chaque jour, avec le même lien. Aucune nouvelle période n'est ouverte entre-temps : il n'a jamais qu'une échéance à régler, pas une dette qui grossit. Le lien reste payable 14 jours après la date d'échéance — ou après son émission, si elle est postérieure ; s'il expire sans paiement, l'abonnement passe CANCELED et vous recevez subscription.canceled. La façon d'accompagner ces retards côté métier est décrite dans impayés d'abonnement et relances.

Étape 4 : écouter les événements subscription.*

Trois événements suivent la vie d'un abonnement :

ÉvénementSignificationAction type
subscription.payment_succeededUne échéance est payéeProlonger l'accès, préparer l'envoi, marquer payé
subscription.payment_failedUne tentative de paiement par carte sur le lien d'une échéance a échoué ; le lien reste payableNoter l'échec, inviter le client à réessayer sur le même lien
subscription.canceledL'abonnement a été annulé, une échéance étant restée impayée au-delà de 14 joursCouper le service selon vos conditions

Déclarez un endpoint abonné explicitement à ces trois événements :

bash
curl -X POST 'https://api-psp.charipay.ma/api/v1/partner/webhooks/endpoints' \
  -H 'X-CHARI-PAY-API-KEY: chari_sk_test_...' \
  -H 'Content-Type: application/json' \
  -d '{
    "url": "https://votre-boutique.ma/webhooks/charipay",
    "description": "Subscriptions",
    "enabledEvents": ["subscription.payment_succeeded", "subscription.payment_failed", "subscription.canceled"]
  }'

La réponse contient signingSecret (whsec_…), affiché une seule fois : conservez-le dans un coffre. Côté réception, routez sur l'en-tête Chari-Event-Type, et dédupliquez sur Chari-Event-Id, pas sur Chari-Webhook-Id — ce dernier identifie la livraison, pas l'événement.

Pour les abonnements, une exception à la règle générale compte : chaque événement subscription.* porte votre externalId d'abonnement (ExternalId), la date de la période concernée (PeriodDate) et votre metadata, renvoyé tel quel — alors que l'externalId d'une session ou d'un lien, lui, ne revient pas. Un récepteur minimal, avec la fonction verify de vérification de signature :

javascript
app.post('/webhooks/charipay', express.raw({ type: '*/*' }), async (req, res) => {
  const raw = req.body.toString('utf8');
  if (!verify(raw, req.get('X-CHARI-SIGNATURE'), req.get('X-CHARI-TIMESTAMP'), secret)) {
    return res.status(401).end();
  }
  const eventId = req.get('Chari-Event-Id');
  if (await db.events.exists(eventId)) return res.status(200).end();

  const type = req.get('Chari-Event-Type');
  const event = JSON.parse(raw);
  if (type === 'subscription.payment_succeeded') {
    await members.markPaid(event.metadata.customerId);   // your metadata, echoed verbatim
  } else if (type === 'subscription.payment_failed') {
    await members.noteFailedAttempt(event.metadata.customerId);   // the link stays payable
  } else if (type === 'subscription.canceled') {
    await members.endAccess(event.metadata.customerId);
  }
  await db.events.insert({ eventId });
  return res.status(200).end();
});

Quand c'est vous qui annulez, la réponse de POST …/cancel porte déjà le statut CANCELED : mettez votre base à jour à ce moment-là, sans attendre d'événement. En cas de doute sur l'état d'un abonnement, relisez-le par GET /v1/subscriptions/{reference}. La signature, l'anti-rejeu et les nouvelles tentatives de livraison sont détaillés dans réussir son intégration des webhooks.

Piloter le cycle de vie : pause, reprise, annulation, échéances

Les gestes du portail sur un abonnement — pause, reprise, annulation, consultation des échéances — existent aussi par l'API. Avec la création, ce sont les sept endpoints du module :

AppelEffet
GET /v1/subscriptionsListe vos abonnements, page par page
GET /v1/subscriptions/{reference}Lit un abonnement, avec son statut et le lien de la période en cours (currentCharge)
GET /v1/subscriptions/{reference}/chargesListe les échéances générées — un lien par période, avec son status et son payUrl —, la plus récente d'abord
POST /v1/subscriptions/{reference}/pauseSuspend l'abonnement : aucun nouveau lien n'est généré jusqu'à la reprise
POST /v1/subscriptions/{reference}/resumeReprend le cycle ; les périodes écoulées pendant la pause ne sont pas facturées rétroactivement
POST /v1/subscriptions/{reference}/cancelMet fin définitivement à l'abonnement ; plus aucune échéance n'est générée
bash
curl -X POST 'https://api-psp.charipay.ma/v1/subscriptions/sub_7Qa1bx/pause' \
  -H 'X-CHARI-PAY-API-KEY: chari_sk_test_...'

Trois règles de conception en découlent. Un changement de formule est un cancel suivi d'un nouveau POST /v1/subscriptions sur le même client. Il n'y a ni prorata ni période d'essai : un démarrage différé, par startDate, en tient lieu, et aucun lien ne part avant la date de première échéance. Enfin, GET …/charges est votre référence pour le rapprochement : chaque échéance y figure avec son statut et son lien, ce qui vous permet de renvoyer à un client le lien d'une échéance encore ouverte.

Idempotence et erreurs utiles

Pour une création d'abonnement, la clé de rejeu est l'externalId : unique par compte et par environnement, il fait qu'une seconde création avec la même valeur renvoie l'abonnement existant en 200 OK au lieu de 201 Created. Les deux sont des succès : testez le statut, pas seulement le corps. Dérivez l'externalId de votre objet métier — votre identifiant client et votre code de formule, par exemple — pour qu'un retry réseau ou une double soumission ne crée jamais deux abonnements. Le contrat ne déclare pas d'en-tête Idempotency-Key pour la création d'un abonnement : c'est l'externalId qui joue ce rôle.

Toutes les erreurs /v1 partagent la même enveloppe :

json
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "amount: must be greater than 0"
  },
  "correlationId": "b0c1e2d3-4f56-7890-abcd-ef0123456789"
}

Testez le code, jamais le message, et journalisez le correlationId (un X-Request-Id que vous envoyez devient le correlationId). Les codes que vous rencontrerez le plus :

StatutCodeCauseQue faire
400VALIDATION_ERRORChamp manquant ou invalide (périodicité, date, canal autre que EMAIL, description de plus de 255 caractères)Corriger la requête, ne pas la rejouer telle quelle
401UNAUTHORIZEDClé absente ou invalideVérifier l'en-tête X-CHARI-PAY-API-KEY
403PRODUCTION_ACCESS_NOT_ENABLEDClé chari_sk_live_… avant l'activation de la productionRester en sandbox jusqu'à l'activation
422WALLET_NOT_ACTIVERequête valide, mais le wallet concerné n'est pas actifNe pas rejouer en boucle ; contacter l'équipe, correlationId à l'appui
429RATE_LIMITEDTrop de requêtesAttendre Retry-After, puis rejouer

Les 5xx sont de notre côté : rejouez avec votre clé d'idempotence — pour une création d'abonnement, le même externalId.

Tester les échéances en sandbox

Attendre un mois pour vérifier une deuxième échéance n'est pas un plan de test. La méthode tient en trois réglages, sans appel particulier : une startDate du jour, pour que le premier lien soit créé immédiatement ; la carte de test, pour le payer ; et la périodicité DAILY, pour qu'une deuxième échéance arrive dès le lendemain.

bash
curl -X POST 'https://api-psp.charipay.ma/v1/subscriptions' \
  -H 'X-CHARI-PAY-API-KEY: chari_sk_test_...' \
  -H 'Content-Type: application/json' \
  -d '{
    "clientId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "amount": 10.00,
    "description": "Test plan - daily",
    "frequency": "DAILY",
    "startDate": "2026-10-05",
    "channels": ["EMAIL"],
    "externalId": "test-sub-001"
  }'

Remplacez startDate par la date du jour, sur le fuseau Africa/Casablanca. La réponse porte le premier lien dans currentCharge : ouvrez son payUrl et payez avec la carte de test — 4918 9141 0719 5005, CVV 123, date d'expiration future, code 3-D Secure 555. L'abonnement passe ACTIVE, et subscription.payment_succeeded arrive sur votre endpoint de sandbox. Le lendemain, la deuxième échéance est ouverte : currentCharge porte un nouveau lien, et GET …/charges en liste deux. Les webhooks partent comme en production.

Un plan de recette complet tient en huit étapes, sur trois jours :

  1. 1Créer le client, puis l'abonnement en DAILY avec une startDate du jour : statut INCOMPLETE, currentCharge présent avec son payUrl.
  2. 2Ouvrir le payUrl et payer avec la carte de test, code 3-D Secure 555 : statut ACTIVE, activatedAt renseigné, événement subscription.payment_succeeded reçu.
  3. 3Le lendemain, relire l'abonnement : un nouveau currentCharge est ouvert et nextRunDate a avancé. Payer ce deuxième lien : un nouvel événement arrive.
  4. 4Le surlendemain, laisser la troisième échéance impayée : aucune nouvelle période ne s'ouvre, et votre base doit la montrer en attente.
  5. 5Vérifier que votre base est à jour et qu'un événement reçu deux fois n'est traité qu'une fois.
  6. 6Mettre en pause, puis reprendre : statuts PAUSED, puis ACTIVE, sans lien généré pendant la pause.
  7. 7Annuler : la réponse porte le statut CANCELED, et votre base le reflète.
  8. 8Lister les échéances (GET …/charges) et les rapprocher de votre base.

Seul le passage en CANCELED après 14 jours sans paiement demande d'attendre : laissez un abonnement de recette impayé et vérifiez votre traitement de subscription.canceled deux semaines plus tard. Le reste de la recette — carte, scénarios de paiement, ce qui ne se teste pas — est dans tester un paiement en mode test.

Questions fréquentes

Quels champs sont obligatoires pour créer un abonnement par l'API ?

Six champs : clientId (l'UUID d'un client existant), amount en MAD, description, frequency (DAILY, WEEKLY, MONTHLY ou YEARLY), startDate au format AAAA-MM-JJ, et channels, qui vaut ["EMAIL"]. Le client doit en outre avoir une adresse e-mail valide, puisque c'est là que part le lien de paiement de chaque échéance. Trois champs sont optionnels : endDate, externalId et metadata.

Pourquoi mon abonnement reste-t-il au statut INCOMPLETE ?

Parce que sa première échéance n'est pas encore payée. L'abonnement reste INCOMPLETE tant que votre client n'a pas réglé le premier lien de paiement, sur la page hébergée, par carte avec 3-D Secure. Relisez-le par GET /v1/subscriptions/{reference} : currentCharge porte l'échéance ouverte et son payUrl, que vous pouvez renvoyer à votre client ; si la date de première échéance est future, aucun lien n'est encore créé.

L'e-mail du client est-il obligatoire pour un abonnement ?

Oui, en pratique. Le schéma du client ne rend obligatoire que name, mais le lien de paiement de chaque échéance et les rappels partent par e-mail : sans adresse valide, votre client ne recevrait rien. Renseignez donc email dès POST /v1/clients, ou complétez-le plus tard avec PUT /v1/clients/{id}, avant de créer l'abonnement.

Que se passe-t-il si mon client ne paie pas une échéance ?

Il reçoit un rappel par e-mail chaque jour, avec le même lien, et aucune nouvelle période n'est ouverte tant que l'échéance n'est pas réglée. Le lien reste payable 14 jours après la date d'échéance ; s'il expire sans paiement, l'abonnement passe CANCELED et vous recevez subscription.canceled. Une tentative de paiement par carte refusée sur le lien vous parvient en subscription.payment_failed, et le lien reste payable.

Mon externalId revient-il dans les webhooks d'abonnement ?

Oui. C'est une exception à la règle générale : l'externalId d'une session ou d'un lien ne revient pas dans les événements, mais celui d'un abonnement revient dans chaque événement subscription.*, avec la date de la période (PeriodDate) et votre metadata renvoyé tel quel. Vous pouvez donc rattacher chaque échéance à votre fiche client sans table de correspondance supplémentaire.

Que se passe-t-il si je rejoue la création avec le même externalId ?

L'API ne crée pas de doublon : elle renvoie l'abonnement existant avec le statut 200 OK au lieu de 201 Created. Traitez les deux comme des succès et lisez la reference renvoyée. L'externalId est unique par compte et par environnement : la même valeur peut exister une fois en sandbox et une fois en production.

Quelles périodicités l'API d'abonnement accepte-t-elle ?

Quatre valeurs pour frequency : DAILY, WEEKLY, MONTHLY et YEARLY. Le montant est fixe pour toute la durée de l'abonnement ; pour changer de formule ou de périodicité, annulez l'abonnement puis créez-en un nouveau sur le même client. Une date de fin optionnelle (endDate) borne la durée.

Comment tester les échéances suivantes sans attendre ?

Créez en sandbox un abonnement en DAILY avec une startDate du jour : le premier lien est créé immédiatement et renvoyé dans currentCharge. Payez-le avec la carte de test, code 3-D Secure 555 : l'abonnement passe ACTIVE et subscription.payment_succeeded arrive. Le lendemain, une deuxième échéance est ouverte, avec un nouveau lien à payer de la même façon.

Prochaine étape

Exécutez les quatre étapes en sandbox avec un abonnement DAILY qui démarre le jour même, payez deux échéances avec la carte de test et vérifiez vos webhooks : Commencer en mode test. Quand votre recette est verte, préparez la bascule avec passer de la sandbox à la production : seuls la clé et le secret de webhook changent.

Écrit par Équipe ChariPay.

À lire ensuite

Prêt à essayer par vous-même ?

Créez votre compte et encaissez en mode test dès aujourd'hui : c'est gratuit, sans validation à attendre. Une question ? Notre équipe répond aux commerçants comme aux développeurs.

  • Gratuit, sans engagement
  • Mode test dès l'inscription
  • Aucune validation à attendre