Aller au contenu principal
Tous les articles
Technique25 min de lecture

Intégrer une API de paiement au Maroc : clés test et live, liens, sessions, webhooks signés, mise en production

Intégrer une API de paiement au Maroc : clés de test et de production, liens et sessions de paiement, webhooks signés HMAC, idempotence et carte de test.

Intégrer une API de paiement au Maroc tient en quatre briques : une clé d'API qui choisit l'environnement, une création côté serveur — lien ou session de paiement —, un webhook signé qui fait foi, et une clé d'idempotence pour ne jamais encaisser deux fois. Ce guide déroule ces briques dans l'ordre d'une intégration réelle, avec le code tel qu'il tourne contre https://api-psp.charipay.ma, en curl, Node et Python. Il se termine par la sandbox — ouverte dès l'inscription, gratuite, sans limite de durée — et par la liste exacte de ce qui change le jour du passage en production : la clé, le secret de webhook, et rien d'autre.

Les API de paiement au Maroc : ce qu'une API portée par un établissement agréé doit faire

Au Maroc, encaisser pour le compte d'un tiers est une activité régulée : l'entité qui reçoit l'argent de l'acheteur, le conserve le temps du traitement et le reverse au marchand doit être agréée par Bank Al-Maghrib. Une API de paiement n'existe donc jamais « toute seule » : derrière les endpoints, il y a un établissement, un compte sur lequel l'argent arrive, et des obligations de conformité qui ne se règlent pas par une ligne de code. ChariPay est opérée par Chari Money, établissement de paiement agréé par Bank Al-Maghrib ; la plateforme est certifiée PCI DSS niveau 1, renouvelée chaque année, et les données personnelles sont traitées dans le cadre de la loi 09-08. La page passerelle de paiement au Maroc explique ce que recouvre ce cadre et comment vérifier un agrément ; ici, nous en tirons les conséquences techniques.

Avant d'écrire une ligne, cinq points font la différence entre une API « qui répond » et une API avec laquelle on peut lancer un produit :

  • Un opérateur agréé, nommé. Vous devez pouvoir répondre à la question « quelle entité est agréée, et mes fonds passent-ils par elle ? ». Chez ChariPay, la réponse tient en un nom : Chari Money.
  • Un règlement en dirhams, sur un compte au nom de l'entreprise. L'API n'a pas de champ devise : tout est en MAD, en unités majeures avec deux décimales (149.90). Chaque paiement réussi est disponible à l'instant sur le compte de paiement du marchand — un compte de paiement tenu par Chari Money, avec un RIB au nom de l'entreprise, consultable par l'API.
  • 3-D Secure sur la carte, sans donnée de carte chez vous. Visa, Mastercard et Maroc Pay sont saisies sur la page de checkout hébergée, dans un périmètre certifié PCI DSS niveau 1. Votre serveur ne voit jamais un numéro de carte.
  • Les espèces en agence, par le même webhook. ChariPay est la seule passerelle de paiement au Maroc à encaisser aussi les espèces en agence : l'acheteur reçoit une référence — celle d'un lien de paiement à usage unique —, la règle dans une agence du réseau Chari, et vous recevez payment.succeeded comme pour une carte. Le traitement du webhook, lui, ne change pas d'une ligne.
  • Des webhooks signés, une idempotence documentée, des erreurs à code stable — et une documentation publique. La documentation API est générée depuis la spécification OpenAPI publiée par l'API, téléchargeable avec la collection Postman : vous pouvez vérifier chaque affirmation de ce guide contre le contrat lui-même.

Trois patrons d'intégration : lien, checkout hébergé, API directe

Vous n'avez pas besoin de tout intégrer. L'API propose trois portes d'entrée, et le choix dépend de la façon dont vous vendez — pas de votre niveau technique.

PatronPour quiCe que vous écrivezDonnées de cartePoint d'entrée
Lien de paiementVente sans site : devis, facture, WhatsApp, comptoirUn appel serveur, ou rien (le portail suffit)Jamais chez vousPOST /v1/payment-links
Session de paiement + checkout hébergéSite e-commerce ou application avec tunnel de commandeUne création serveur, une redirection, un webhookJamais chez vousPOST /v1/payment-sessions
Checkout directVotre propre formulaire de carte, champ par champVerify → submit → 3-D Secure → returnDans votre périmètre de conformité/checkout/* avec la clé vk

Le lien de paiement est le patron le plus court : un montant, une description, et l'API renvoie une page de paiement hébergée à partager — avec QR code et affiche PDF. C'est la bonne réponse pour un devis accepté sur WhatsApp, une cotisation, un acompte, une vente au comptoir. La page Liens de paiement montre ce que voit l'acheteur ; le module Liens de paiement de la documentation liste les sept endpoints.

La session de paiement est faite pour un site marchand : une commande devient une session, vous redirigez l'acheteur vers le checkout hébergé 3-D Secure, et vous confirmez la commande à réception du webhook. La page Paiement en ligne décrit le parcours ; le module Sessions de paiement détaille les champs.

Le checkout direct n'a de sens que si vous avez une raison précise de dessiner votre propre écran de carte : il vous place dans le périmètre de conformité des données de carte, et ses endpoints ne sont pas authentifiés par votre clé d'API mais par la session et une clé de vérification à usage unique. Pour la quasi-totalité des projets, les deux premiers patrons suffisent — y compris pour une boutique Shopify ou WooCommerce, que l'on branche par un lien de paiement ou par une session créée côté serveur, sans plugin à installer.

Authentification et environnements

Chaque requête porte votre clé d'API dans l'en-tête X-CHARI-PAY-API-KEY. Il n'y a ni flux OAuth ni jeton à rafraîchir : la clé suffit, et c'est elle qui détermine l'environnement. Une clé de sandbox commence par chari_sk_test_, une clé de production par chari_sk_live_. Il n'y a qu'une seule base d'URL, https://api-psp.charipay.ma, en test comme en production : vous ne choisissez pas l'environnement dans l'URL ni dans un en-tête, vous le choisissez en choisissant la clé. C'est volontaire — il devient impossible d'envoyer par erreur une requête de test vers la production, et le passage en production ne demande aucun changement d'adresse.

Votre clé de test est à portée de main dès maintenant : Commencer en mode test. Trois champs vous sont demandés — nom, e-mail professionnel, entreprise —, puis un lien d'activation arrive par e-mail pour choisir votre mot de passe ; vous créez ensuite votre clé chari_sk_test_… depuis le portail et faites votre premier paiement de test avec la carte de test. Le mode test est gratuit, sans limite de durée, sans aucune validation à attendre ; la vérification de votre entreprise (KYB) ne conditionne que le passage en production.

Trois règles pour les clés, avant même le premier appel :

  1. 1Une clé est un secret. Elle vit sur votre serveur, dans une variable d'environnement ou un coffre. Jamais dans du code exécuté par le navigateur, jamais dans une application mobile, jamais dans un dépôt git.
  2. 2Une clé a un périmètre. Les clés sont émises par entreprise et par environnement, avec des permissions par module : n'accordez que ce que votre intégration appelle réellement. La clé complète ne s'affiche qu'une seule fois, à sa création — copiez-la à cet instant.
  3. 3**Les endpoints /checkout/* font exception.** Ils sont exécutés par le navigateur de l'acheteur et portés par la session elle-même, avec sa clé de vérification vk à usage unique. N'y envoyez jamais votre clé d'API.

Les bons réflexes — rotation, fuite, clés partagées dans une équipe — sont détaillés dans Protéger ses clés d'API.

Créer un lien de paiement

Le premier appel utile tient en trois champs : un montant en dirhams, une description affichée à l'acheteur, et un externalId dérivé de votre référence de commande, qui rend l'appel rejouable. L'en-tête Idempotency-Key protège, lui, contre un retry réseau — nous y revenons plus bas.

bash
curl -X POST 'https://api-psp.charipay.ma/v1/payment-links' \
  -H 'X-CHARI-PAY-API-KEY: chari_sk_test_...' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: order-1042-link' \
  -d '{
    "amount": 149.90,
    "description": "Commande #1042",
    "singleUse": true,
    "externalId": "order-1042",
    "customerEmail": "amine.bennani@example.com",
    "customerPhone": "+212600000000",
    "metadata": { "cartId": "c_987" }
  }'

Le même appel en Node, avec fetch et la clé lue dans l'environnement :

javascript
const response = await fetch('https://api-psp.charipay.ma/v1/payment-links', {
  method: 'POST',
  headers: {
    'X-CHARI-PAY-API-KEY': process.env.CHARI_PAY_API_KEY,
    'Content-Type': 'application/json',
    'Idempotency-Key': 'order-1042-link',
  },
  body: JSON.stringify({
    amount: 149.9,
    description: 'Commande #1042',
    singleUse: true,
    externalId: 'order-1042',
    customerEmail: 'amine.bennani@example.com',
    metadata: { cartId: 'c_987' },
  }),
});

if (!response.ok) throw new Error(await response.text());
const link = await response.json();
// link.reference — e.g. "pl_3ND8xk"; link.payUrl — the payment page to share

Et en Python, avec requests :

python
import os, requests

response = requests.post(
    'https://api-psp.charipay.ma/v1/payment-links',
    headers={
        'X-CHARI-PAY-API-KEY': os.environ['CHARI_PAY_API_KEY'],
        'Content-Type': 'application/json',
        'Idempotency-Key': 'order-1042-link',
    },
    json={
        'amount': 149.90,
        'description': 'Commande #1042',
        'singleUse': True,
        'externalId': 'order-1042',
        'customerEmail': 'amine.bennani@example.com',
        'metadata': {'cartId': 'c_987'},
    },
)
response.raise_for_status()
link = response.json()
print(link['reference'], link['payUrl'])

La réponse arrive en 201 avec la reference du lien (de la forme pl_…), son status (ACTIVE), la currency (MAD, toujours) et le payUrl — la page hébergée vers laquelle vous envoyez l'acheteur. Si vous rejouez l'appel avec le même externalId, l'API renvoie le lien existant en 200 au lieu d'en créer un second : testez le statut, les deux sont des succès.

Quatre options valent la peine d'être connues dès le premier jour. singleUse: false rend le lien réutilisable — pratique pour une affiche en vitrine ou une cotisation. expiresAt fixe une date de fin (ISO-8601 UTC, dans le futur). paymentMethod: "CASH" crée un lien à régler en espèces : l'acheteur reçoit un cashinCode à présenter en agence, et le webhook payment.succeeded vous confirme le dépôt. Enfin, acceptUrl, declineUrl et notificationUrl — toutes en https:// — personnalisent le retour et la notification pour ce lien précis. Le QR code (GET /v1/payment-links/{reference}/qr), l'affiche PDF (/poster) et l'envoi par e-mail (POST …/send) sont des endpoints du même module ; POST …/cancel rend un lien impayable si la vente ne se fait pas.

Créer une session de paiement et gérer le retour

Pour un site marchand, la session de paiement remplace le lien : elle porte votre orderId, l'acheteur, et vos URL de retour. Vous la créez côté serveur, vous redirigez vers le checkoutUrl renvoyé, et vous attendez le webhook.

bash
curl -X POST 'https://api-psp.charipay.ma/v1/payment-sessions' \
  -H 'X-CHARI-PAY-API-KEY: chari_sk_test_...' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: order-2026-0421-session' \
  -d '{
    "amount": 250.00,
    "orderId": "ORD-2026-0421",
    "externalId": "order-2026-0421",
    "config": {
      "customer": {
        "email": "amine.bennani@example.com",
        "firstName": "Amine",
        "lastName": "Bennani",
        "phone": "+212600000000"
      },
      "urls": {
        "accept": "https://votre-boutique.ma/paiement/succes",
        "decline": "https://votre-boutique.ma/paiement/echec",
        "notification": "https://votre-boutique.ma/webhooks/charipay"
      }
    },
    "metadata": { "cartId": "c_987", "source": "web" }
  }'

La réponse, en 201, contient les cinq champs que fixe le contrat : le sessionId, le sessionToken côté serveur, la clé de vérification verifyKey, l'expiresAt de la session et le checkoutUrl vers lequel rediriger l'acheteur :

json
{
  "sessionId": "ps_5Kd0Rn",
  "sessionToken": "st_…",
  "verifyKey": "vk_5Kd0Rn",
  "expiresAt": "2026-10-05T10:15:00Z",
  "checkoutUrl": "https://…/checkout/ps_5Kd0Rn"
}

Quelques précisions sur les champs. verifyKey est la clé de vérification à usage unique, passée dans le paramètre vk : elle ne vous concerne qu'en checkout direct ; avec le checkout hébergé, le checkoutUrl suffit. Le statut, le montant et l'expiration de la session se lisent par GET /v1/payment-sessions/{sessionId}. orderId est votre référence métier : affichée, renvoyée dans les webhooks, sans contrainte d'unicité. externalId est différent : unique par marchand et par environnement, il rend la création rejouable. Les quatre champs de config.customer sont obligatoires ; les URL de config.urls sont toutes optionnelles et doivent être en https:// : si accept ou decline manque, la valeur configurée pour votre compte s'applique, puis celle de la plateforme ; si notification manque, seule la valeur de votre compte s'applique. Une session est à usage unique et expire par défaut 72 heures après sa création ; expiresAt la raccourcit. config.keepAlive: true laisse l'acheteur retenter après un échec ; notifyOnFailure: true vous envoie aussi payment.failed. L'objet metadata (au plus 4 Ko) revient tel quel dans le webhook de paiement.

Le retour de l'acheteur sur votre site est l'endroit où la plupart des intégrations se trompent. La règle tient en une phrase : croire le webhook, pas la redirection. Voici l'enchaînement correct :

  1. 1Créez la session côté serveur et enregistrez le sessionId en face de votre commande, à l'état « en attente de paiement ».
  2. 2Redirigez l'acheteur vers checkoutUrl. Il y paie par carte avec 3-D Secure. Une session n'encaisse que la carte ; pour une commande à régler en espèces en agence, créez un lien de paiement à usage unique, comme dans la section sur les liens de paiement : le webhook est le même.
  3. 3À son retour sur `accept`, affichez un état d'attente — « paiement en cours de confirmation » — et pas une page de succès définitive. La redirection dit que l'acheteur est revenu ; elle ne dit pas que l'argent est arrivé. Un acheteur qui ferme son onglet après avoir payé ne reviendra jamais sur cette page, et l'argent, lui, est bien là.
  4. 4Confirmez la commande à réception de `payment.succeeded`, après vérification de la signature. C'est la seule source de vérité.
  5. 5En repli, si vous n'avez rien reçu après un délai raisonnable, interrogez GET /v1/payment-sessions/{sessionId} ou la liste des transactions — le sondage est un filet de sécurité, pas un mode de fonctionnement.

Webhooks : vérifier la signature

Un paiement n'aboutit pas au moment où vous l'appelez : l'acheteur passe par sa banque, valide 3-D Secure, revient — ou ne revient pas. Le résultat vous parvient par webhook. Vous déclarez vos URL de réception depuis le portail ou par POST /api/v1/partner/webhooks/endpoints, avec une liste explicite d'événements (enabledEvents) : n'abonnez que ce que vous traitez. L'URL doit être en HTTPS public, sur le port 443 ; localhost et les adresses privées sont refusés dès l'enregistrement — en développement, passez par un tunnel. Chaque endpoint reçoit son secret de signature, à conserver comme une clé d'API, et les secrets sont distincts entre sandbox et production.

Chaque livraison porte deux en-têtes : X-CHARI-SIGNATURE, un HMAC-SHA256 en hexadécimal minuscule calculé sur la chaîne horodatage + "." + corps brut, et X-CHARI-TIMESTAMP, l'horodatage en millisecondes depuis epoch. Voici la vérification, reprise de la documentation de l'API :

javascript
const crypto = require('crypto');

function verify(rawBody, signature, timestamp, secret) {
  // ±5-minute anti-replay window — the timestamp is in milliseconds.
  if (Math.abs(Date.now() - Number(timestamp)) > 5 * 60 * 1000) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex');

  // A malformed signature must return false — never throw (500 → retries).
  if (!/^[0-9a-f]{64}$/i.test(signature)) return false;

  // Constant-time comparison: never `===`.
  return crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(signature, 'hex'));
}

Quatre règles font toute la sécurité de cette vérification. Calculez le HMAC sur les octets bruts, avant tout parsing : un framework qui désérialise le JSON puis le re-sérialise ne produit plus les octets qui ont été signés — c'est l'erreur la plus fréquente. Rejetez un horodatage décalé de plus de ±5 minutes. Comparez en temps constant, jamais avec ===. Et pendant une rotation de secret, acceptez l'une ou l'autre des deux signatures : tant que l'ancien secret est dans sa fenêtre de grâce, chaque livraison porte X-CHARI-SIGNATURE avec l'ancien et X-CHARI-SIGNATURE-NEXT avec le nouveau.

Vient ensuite la déduplication, et c'est la distinction qui coûte le plus cher quand on se trompe : dédupliquez sur `Chari-Event-Id`, pas sur `Chari-Webhook-Id`. Un même événement logique peut être livré plusieurs fois ; chaque tentative reçoit son propre Chari-Webhook-Id, qui change donc à chaque essai, mais toutes portent le même Chari-Event-Id. La livraison est au moins une fois et peut arriver dans le désordre — par conception. Ne répondez 2xx qu'une fois la mise à jour métier et l'enregistrement de déduplication tous deux validés, et faites le travail lourd en tâche de fond : un traitement de dix secondes finit par déclencher des relectures, puis la suspension de votre endpoint.

Si votre serveur ne répond pas 2xx, nous rejouons : la première tentative part immédiatement, puis 1 min → 5 min → 30 min → 1 h → toutes les 6 h, jusqu'à 16 tentatives sur environ 72 heures ; au-delà, la livraison est marquée échouée. Le journal des livraisons du portail montre chaque tentative, la réponse de votre serveur et le corps exact de l'événement, et permet de rejouer en un clic. Après une indisponibilité plus longue, réconciliez par GET /v1/transactions plutôt que d'attendre un webhook qui ne reviendra plus.

Les événements que vous attendrez le plus : payment.succeeded, payment.failed (si demandé à la création), order.paid, refund.succeeded, refund.failed, et pour les abonnements subscription.payment_succeeded, subscription.payment_failed, subscription.canceled. Vos metadata et externalId reviennent sur chaque événement de la ressource qui les portait : c'est ce qui permet de rapprocher sans stocker nos références. Un événement de test est disponible sur chaque endpoint déclaré — envoyez-le avant le premier paiement réel. Le module Webhooks liste les vingt événements réellement émis ; Réussir son intégration des webhooks détaille les cinq erreurs classiques et le traitement complet en quinze lignes.

Idempotence

Le réseau coupe entre votre serveur et le nôtre, votre client HTTP réessaie, et vous ne savez pas si la première tentative est passée. En paiement, réessayer à l'aveugle est la meilleure façon de débiter deux fois le même client. Deux mécanismes indépendants répondent à deux pannes différentes, et ils se cumulent.

L'en-tête `Idempotency-Key` protège du retry réseau. Vous l'envoyez sur vos créations ; rejouer un appel avec la même valeur renvoie le premier résultat au lieu de créer un doublon. C'est la protection contre un timeout, une connexion coupée, une file de messages livrée deux fois. Sa barrière est bornée au compte — gardez des clés distinctes entre vos tests et votre production.

Le champ `externalId` protège de la ré-émission métier. Unique par compte et par environnement, il fait de la création une opération que vous pouvez relancer sans conserver le moindre état intermédiaire : créer une ressource avec un externalId qui existe déjà renvoie la ressource existante en 200 OK, là où une vraie création répond 201 Created. C'est la protection contre votre propre système qui ré-émet la même intention — un job relancé, une file rejouée, un double clic en back-office. Un même externalId peut exister une fois en sandbox et une fois en production sans conflit.

Les remboursements utilisent `refundReference` pour la même raison : un remboursement frais répond 202 Accepted — il s'exécute en asynchrone — et un rejeu de la même référence renvoie le remboursement existant en 200, sans débiter deux fois. Jamais 201. Dans les trois cas, dérivez la clé de votre référence de commande — jamais d'un tirage aléatoire, qui passerait à côté de tout l'intérêt. Les bons motifs de clés et ce que l'idempotence ne fait pas sont dans L'idempotence : ne facturez jamais deux fois.

Erreurs et correlationId

Toutes les erreurs de l'API marchande (/v1) partagent la même enveloppe : un code stable que votre code peut tester, un message lisible, et un correlationId à donner au support.

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

Testez le code, jamais le message : le message peut être reformulé, traduit ou précisé, le code fait partie du contrat. Le correlationId est renvoyé sur toutes les réponses, succès compris — journalisez-le systématiquement, c'est ce qui permet de retrouver une requête précise dans nos journaux. Si vous envoyez votre propre X-Request-Id, il est répété sur la réponse et devient le correlationId.

StatutCodesCe que cela veut dire, et quoi faire
400VALIDATION_ERROR, MISSING_PARAMETER, INVALID_IDEMPOTENCY_KEYUn champ ne passe pas (montant nul, URL non https://, clé d'idempotence absente ou trop longue sur une session réutilisable). Corrigez, ne rejouez pas tel quel.
401UNAUTHORIZED, INVALID_TOKENClé d'API absente ou invalide : vérifiez l'en-tête X-CHARI-PAY-API-KEY et l'environnement de la clé. Sur /checkout/*, INVALID_TOKEN signale une clé vk invalide ou déjà consommée.
403FORBIDDEN, PRODUCTION_ACCESS_NOT_ENABLEDPermission manquante sur la clé, ou clé de production sur un compte dont la production n'est pas encore activée.
404OPERATION_NOT_FOUND, ORDER_NOT_FOUND, SESSION_NOT_FOUNDLa ressource n'existe pas dans cet environnement — un identifiant sandbox ne vaut rien en production.
409IDEMPOTENCY_CONFLICT, SESSION_ALREADY_CONSUMED, SESSION_NOT_ACTIVEMême clé d'idempotence avec un corps différent, ou session déjà consommée : créez-en une nouvelle.
410SESSION_EXPIREDLa session a dépassé son expiresAt. Créez une nouvelle session.
422WALLET_NOT_ACTIVE, PAYMENT_METHOD_CONSENT_REQUIREDRequête valide, mais une règle métier la bloque.
429RATE_LIMITEDTrop de requêtes : ralentissez et relisez l'en-tête Retry-After. Les endpoints exposés au public annoncent X-RateLimit-Limit, X-RateLimit-Remaining et X-RateLimit-Reset.
502BAAS_CHARI_ERROREn sandbox, c'est presque toujours une carte autre que la carte de test. Vérifiez le PAN saisi.
5xx—De notre côté. Rejouez avec votre clé d'idempotence plutôt que de créer une nouvelle ressource.

Deux conventions complètent le tableau. Toute URL que vous déclarez — retour, notification — doit être en https://, sous peine de 400. Et tolérez les valeurs d'énumération nouvelles comme les champs optionnels absents — jamais null : c'est ainsi que l'API évolue sans vous casser. Les endpoints d'inscription et du portail ont leur propre format d'erreur, avec des codes ERR-XXXX ; l'enveloppe ci-dessus vaut pour l'API marchande, celle que votre clé appelle.

Remboursements, abonnements, compte de paiement par l'API

Une fois le premier encaissement en place, le reste de l'API se branche au rythme de votre produit. Trois modules reviennent dans presque toutes les intégrations.

Rembourser. POST /v1/refunds avec l'identifiant du paiement — operationId ou externalId —, un motif (reason) et une refundReference de votre choix ; en totalité par défaut, ou pour un montant partiel avec refundAmount. L'API répond 202 le temps du règlement, puis refund.succeeded arrive par webhook — un remboursement n'est pas instantané côté banque. Rembourser un client est gratuit, et le montant est déduit du solde disponible de votre compte de paiement.

bash
curl -X POST 'https://api-psp.charipay.ma/v1/refunds' \
  -H 'X-CHARI-PAY-API-KEY: chari_sk_test_...' \
  -H 'Content-Type: application/json' \
  -d '{
    "externalId": "order-2026-0421",
    "refundReference": "refund-order-2026-0421",
    "refundAmount": 100.00,
    "reason": "Article retourné"
  }'

Le module Remboursements décrit les trois endpoints et les statuts.

Abonnements. Créez le client (POST /v1/clients) puis l'abonnement (POST /v1/subscriptions) avec sa périodicité et son montant ; le premier paiement se fait en présence du client, sur le checkout hébergé, avec son consentement explicite à conserver son moyen de paiement. Un avis de pré-prélèvement part par e-mail avant chaque échéance. Un prélèvement qui échoue est retenté à J, J+1, J+3 et J+7, avec un lien de paiement de secours envoyé au client, puis l'abonnement est annulé — vous suivez subscription.payment_failed et subscription.canceled. En sandbox, POST /v1/subscriptions/{reference}/test-auto-pay force la prochaine échéance : vous déroulez une année d'abonnement en une minute et vérifiez votre facturation avant qu'un vrai client ne la subisse. Le module Abonnements détaille les neuf endpoints — pause, reprise, annulation, échéances.

Le compte de paiement. L'argent de chaque paiement réussi est disponible à l'instant sur votre compte de paiement ChariPay, tenu par Chari Money, et l'API vous le montre : GET /v1/wallet renvoie le solde, GET /v1/wallet/account le RIB au nom de votre entreprise, GET /v1/wallet/account/rib-document l'attestation PDF, et POST /v1/wallet/cash-ins alimente le compte avant une opération sortante. Depuis ce compte, vous virez vers un compte bancaire marocain et vous payez des factures depuis le portail ; les recharges télécom, elles, passent par l'API. Les mouvements sortants vous sont notifiés par webhook (merchant_transfer.completed, bill_payment.succeeded, topup.succeeded, et leurs échecs). Le module Wallet liste les quatre endpoints ; la page Wallet et versements décrit ce que vous pouvez faire depuis le compte, versement automatique chaque nuit compris dès qu'un compte de règlement est configuré.

Tester dans la sandbox

Vous n'attendez personne pour commencer : la sandbox est self-serve, gratuite et sans limite de durée, et le mode test s'ouvre dès l'inscription — la vérification de votre entreprise se fait en parallèle et ne conditionne que la production. Huit étapes séparent une page blanche d'un appel authentifié, et la collection Postman les déroule dans l'ordre :

  1. 1Inscrivez-vous : créez votre compte de test — nom, e-mail professionnel, entreprise.
  2. 2Activez le compte avec le lien reçu par e-mail et choisissez votre mot de passe ; le jeton d'activation n'existe que dans cet e-mail.
  3. 3Connectez-vous au portail ; selon les permissions de votre compte, une application TOTP peut vous être demandée — conservez vos codes de récupération.
  4. 4Sélectionnez votre entreprise : c'est cette réponse, pas le login, qui porte votre jeton de session.
  5. 5Passez la ré-authentification : créer une clé est une action sensible, un jeton de confirmation à courte durée de vie est exigé.
  6. 6Regardez les permissions disponibles et n'accordez que celles que votre intégration appelle.
  7. 7Créez votre clé chari_sk_test_… — elle ne s'affiche qu'une fois : mettez-la immédiatement dans un coffre ou une variable d'environnement.
  8. 8Vérifiez avec GET /v1/wallet : si le solde vous répond, vous êtes authentifié.

En sandbox, une seule carte de test est acceptée : 4918 9141 0719 5005 (à saisir sans espaces), CVV 123, n'importe quelle date d'expiration future, et 555 comme code 3-D Secure. Elle déclenche un vrai parcours — page de checkout, authentification, webhook — sans aucun mouvement d'argent. Tout autre PAN, y compris les 4242… d'autres plateformes, est rejeté en amont avec un 502 et le code BAAS_CHARI_ERROR : si vous rencontrez cette erreur en test, vérifiez d'abord la carte saisie.

Testez autre chose que le cas nominal avant de penser à la production : un paiement refusé, un lien en espèces et son cashinCode, un remboursement partiel, un abonnement dont vous forcez trois échéances, un webhook auquel votre serveur répond une erreur — pour voir la relance à l'œuvre —, et l'envoi d'un événement de test sur chaque endpoint déclaré. La sandbox reste votre environnement de recette bien après le lancement : gardez deux jeux de variables d'environnement.

Passer en production

La sandbox est ouverte immédiatement ; la production s'active sur votre compte après vérification de votre entreprise. Tant qu'elle ne l'est pas, une clé chari_sk_live_… reçoit un 403 explicite avec le code PRODUCTION_ACCESS_NOT_ENABLED — ce n'est pas un bug d'intégration, c'est une étape administrative. Les étapes, dans l'ordre : la vérification KYB (documents d'identité et registre de commerce, avec double validation humaine), l'étude de votre dossier et la proposition chiffrée — commission en pourcentage par transaction réussie et caution, définies selon vos produits, vos méthodes de paiement et vos volumes —, le règlement de la mise en service de 6 000 MAD TTC, une seule fois, puis l'activation. Le détail de la grille est sur la page Tarification. Nous ne promettons pas de délai : chaque dossier est étudié.

Le jour de la bascule, vous changez deux choses, et rien d'autre : la clé d'API — chari_sk_test_… devient chari_sk_live_… — et le secret de webhook, distinct en production. Les URL, les payloads, les codes d'erreur et la base https://api-psp.charipay.ma sont identiques. Avant de basculer, cinq vérifications valent le temps qu'elles prennent : votre traitement des webhooks vérifie la signature sur le corps brut et est idempotent ; vos créations envoient une clé d'idempotence dérivée de votre référence ; vous journalisez le correlationId de chaque appel ; vos clés vivent dans un coffre, pas dans le dépôt ; vous avez testé un remboursement et un échec de paiement. La liste complète, avec les trois oublis les plus fréquents, est dans Sandbox → production : la liste avant de basculer.

Téléchargements

Tout ce que ce guide affirme se vérifie contre le contrat. Trois ressources sont servies en accès libre, sans compte :

  • Spécification OpenAPI — le contrat lui-même, celui dont la référence en ligne est générée, sans reformulation ; à charger dans votre générateur de client, votre outil de test ou votre éditeur.
  • Collection Postman — les 62 endpoints prêts à exécuter, avec les huit requêtes d'inscription en tête : de zéro à votre première clé sans quitter Postman.
  • Pack LLM — un Markdown par module, les guides d'authentification, de webhooks et d'erreurs, la carte de test et la spécification : ce qu'un assistant de code doit lire pour intégrer l'API hors ligne.

La documentation API couvre les douze modules et les soixante-deux endpoints avec des exemples en curl, JavaScript, Python et PHP ; la page Développeurs résume les conventions et le parcours du premier appel à la production.

Questions fréquentes

Existe-t-il une API de paiement gratuite au Maroc pour tester ?

Oui. La sandbox ChariPay est gratuite, sans limite de durée et self-serve : vous vous inscrivez en ligne, activez votre compte par e-mail et créez vous-même votre clé chari_sk_test_…, sans rendez-vous ni e-mail au support. Elle tourne sur les mêmes endpoints que la production, envoie de vrais webhooks signés et accepte la carte de test 4918 9141 0719 5005. Seul le passage en production est payant.

Quelle est la base d'URL et comment choisir l'environnement ?

Il n'y a qu'une base d'URL, https://api-psp.charipay.ma, en sandbox comme en production. C'est la clé d'API qui choisit l'environnement : chari_sk_test_… cible la sandbox, chari_sk_live_… la production. Les URL, les payloads et les codes d'erreur sont identiques dans les deux cas ; le jour du passage en production, vous changez la clé et le secret de webhook, rien d'autre.

Comment vérifier un webhook ?

Recalculez un HMAC-SHA256 avec votre secret d'endpoint sur la chaîne X-CHARI-TIMESTAMP + "." + corps brut, avant tout parsing, et comparez-le en temps constant à X-CHARI-SIGNATURE. Rejetez les horodatages décalés de plus de ±5 minutes, dédupliquez sur Chari-Event-Id, et ne répondez 2xx qu'une fois le traitement enregistré. Le code complet de vérification en Node figure plus haut et dans le guide Webhooks de la documentation API.

Que se passe-t-il si mon serveur est injoignable ?

Nous rejouons la livraison : immédiatement, puis 1 min, 5 min, 30 min, 1 h, puis toutes les 6 h, jusqu'à 16 tentatives sur environ 72 heures. Chaque tentative porte un nouveau Chari-Webhook-Id mais le même Chari-Event-Id. Au-delà, la livraison est marquée échouée ; le journal du portail permet de la rejouer en un clic, et GET /v1/transactions sert à réconcilier après une panne plus longue.

Comment éviter de débiter deux fois ?

Envoyez un en-tête Idempotency-Key sur chaque création : un retry réseau avec la même valeur renvoie le premier résultat. Ajoutez un externalId dérivé de votre référence de commande : une ré-émission renvoie la ressource existante en 200 au lieu de 201. Pour un remboursement, la refundReference joue le même rôle — 202 à la création, 200 au rejeu. Et dédupliquez vos webhooks sur Chari-Event-Id.

Puis-je intégrer Shopify ou WooCommerce par l'API ?

Oui, sans plugin à installer : il n'en existe pas, et il n'en faut pas. Pour une boutique sans développeur, un lien de paiement créé depuis le portail s'envoie à la commande. Avec un développeur, votre serveur crée une session de paiement à la validation du panier, redirige vers le checkout hébergé, et confirme la commande sur payment.succeeded. Les guides Shopify et WooCommerce du blog détaillent les deux parcours.

Comment encaisser des espèces par l'API ?

Créez un lien de paiement à usage unique avec paymentMethod: "CASH", ou laissez l'acheteur choisir les espèces sur la page du lien ; une session, elle, n'encaisse que la carte. Il reçoit une référence — le cashinCode du lien — à présenter dans une agence du réseau Chari, dépose le montant, et vous recevez payment.succeeded : même webhook, même déduplication, même rapprochement que pour une carte. ChariPay est la seule passerelle de paiement au Maroc à encaisser aussi les espèces en agence.

Combien de temps pour passer en production ?

Nous ne publions pas de délai, parce que chaque dossier est étudié. Les étapes sont connues : vérification KYB avec vos documents d'identité et votre registre de commerce, étude du dossier et proposition chiffrée, règlement de la mise en service de 6 000 MAD TTC, puis activation de la production sur votre compte. Votre intégration n'attend pas : elle se construit en sandbox pendant ce temps, et bascule en changeant la clé.

Prochaine étape

Ouvrez votre compte de test — Commencer en mode test — puis faites votre premier POST /v1/payment-links et payez le lien avec la carte de test. Ouvrez ensuite la page Développeurs pour les conventions et la documentation API pour chaque endpoint. Quand payment.succeeded arrive sur votre serveur avec une signature valide, vous avez fait l'essentiel : le passage en production tient dans un changement de clé.

Écrit par Équipe ChariPay.

À lire ensuite

Technique5 min de lecture

Réussir son intégration des webhooks

Réussir son intégration des webhooks : les cinq erreurs qui reviennent dans presque toutes les intégrations, et le traitement en quinze lignes qui les évite.

Technique5 min de lecture

L'idempotence : ne facturez jamais deux fois

L'idempotence des paiements : comment l'en-tête Idempotency-Key et l'externalId évitent de débiter deux fois un client quand le réseau coupe.

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