# INTRODUCTION

Introduction à l'intégration de l'API Intram.

Cette documentation vous accompagne dans l'intégration des solutions Intram dans vos applications web, mobiles et serveurs.

## Vue d'ensemble des APIs

Intram propose **deux interfaces** complémentaires que vous pouvez utiliser selon votre besoin :

| Interface                                                  | À utiliser pour                                                                                                     | Documentation                                 |
| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- |
| **API publique de paiement** (legacy)                      | Construire un checkout client : pages widget, SDK JavaScript, plugins WordPress/Django/PHP                          | [HTTP](/backend/http), [SDKs](/api-index)     |
| **Merchant API v1** (nouveau, recommandée pour le backend) | Piloter votre compte marchand depuis votre backend : solde, payouts, demandes de paiement, refunds, webhooks signés | [Merchant API](/merchant-api-v1/merchant-api) |

Les deux coexistent sans interférer. Choisissez la première si vous voulez encaisser un paiement client ; la seconde si vous voulez automatiser la gestion de votre compte depuis vos propres serveurs.

## Pré-requis

Avant toute intégration vous avez besoin :

1. **D'un compte Intram validé** — [inscrivez-vous](https://app.intram.org/register) puis suivez la procédure d'[activation du compte marchand](/account/account-activation).
2. **D'au moins un compte business** validé (Paramètres → Compte business).
3. **De vos clés API** — générées automatiquement pour chaque environnement (sandbox + live).

## Récupérer vos clés API

1. **Connectez-vous** à <https://app.intram.org/login> avec vos identifiants marchands.
2. Dans le menu de gauche, cliquez sur **Développeurs**.
3. Cliquez ensuite sur **API**.
4. Le tableau de bord affiche trois clés, par environnement (`SANDBOX` ou `LIVE`) :
   * **Clé API publique** (`public_key`) — identifiant non secret de votre compte
   * **Clé API privée** (`private_key`) — utilisée par les anciennes intégrations SDK
   * **Clé API secrète** (`secret_key`) — **ne sort jamais** ; utilisée pour signer les requêtes Merchant API et vérifier les webhooks

{% hint style="info" %}
Commencez toujours par développer en `SANDBOX`. Les transactions sandbox ne touchent pas l'argent réel et permettent de tester librement vos flows. Passez en `LIVE` quand votre intégration est stable.
{% endhint %}

{% hint style="danger" %}
Ne partagez jamais vos clés `private_key` et `secret_key` — pas dans un dépôt Git public, pas dans le code front-end, pas dans les logs. Si une clé est compromise, régénérez-la immédiatement depuis le dashboard.
{% endhint %}

## Par où commencer

| Vous voulez…                                                          | Direction                                                                    |
| --------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| Créer un compte                                                       | [Inscription](/account/register) → [Activation](/account/account-activation) |
| Encaisser sans site web avec un lien à partager                       | [Intram Direct](/modules/intram-direct)                                      |
| Gérer un catalogue d'offres + clients + abonnements                   | [Intram Business](/modules/intram-business)                                  |
| Encaisser des paiements depuis votre site (page checkout)             | [SDKs frontend / backend](/api-index) ou [HTTP direct](/backend/http)        |
| Piloter votre compte (payouts, balance, refunds) depuis votre backend | [Merchant API v1 — Quickstart](/merchant-api-v1/quickstart)                  |
| Encaisser des paiements depuis votre app mobile                       | [Flutter](/mobile/flutter)                                                   |
| Comprendre les frais appliqués                                        | [Frais](/payment/fees)                                                       |
| Connaître les devises et moyens de paiement supportés                 | [Devises supportées](/payment/supported-devices)                             |


# INTRAM

Intram accélère la digitalisation des entreprises en Afrique avec des solutions de vente, d'encaissement et de paiement multicanal.

Nous accélérons la digitalisation des entreprises en Afrique à travers des solutions digitales pour vendre, recevoir des paiements, émettre des paiements et assurer une meilleure gestion.

Notre objectif : vous offrir une solution **multicanale** qui facilite l'émission et la réception de paiements sur site internet, application mobile ou backend.

## Ce que vous pouvez faire avec Intram

* **Vendre en ligne** — proposez votre catalogue par un lien personnalisé ou intégrez un checkout sur votre site.
* **Encaisser via Mobile Money et carte bancaire** — un seul intégration couvre MTN, Moov, SBIN et les cartes Visa/Mastercard.
* **Automatiser vos paiements de masse** — payez en 1 clic des milliers de bénéficiaires sur leurs comptes Mobile Money ou bancaires.
* **Programmer des paiements récurrents** — pour vos abonnements, salaires, loyers, redevances scolaires…
* **Suivre votre activité** — dashboard avec transactions, statuts, balances, journaux, webhooks, refunds.

## Notre engagement

Vous êtes notre priorité. Nous restons continuellement à votre écoute et présents pour vous accompagner dans votre croissance. La qualité de nos relations est au cœur de notre fonctionnement.

[**Essayez INTRAM maintenant →**](https://app.intram.org/register)

## Prochaines étapes pour intégrer

| Vous voulez…                     | Direction                                                |
| -------------------------------- | -------------------------------------------------------- |
| Créer un compte                  | [Inscription](/account/register)                         |
| Comprendre les APIs disponibles  | [Introduction](/)                                        |
| Démarrer une intégration backend | [Merchant API — Quickstart](/merchant-api-v1/quickstart) |
| Intégrer un checkout client      | [SDK / clients](/api-index)                              |


# Inscription

Créer un compte utilisateur Intram pour commencer l'intégration.

La création d'un compte est gratuite et prend moins de 2 minutes. Une fois inscrit, vous accéderez au dashboard pour générer vos clés API et activer votre compte business.

![Page d'inscription d'Intram](https://2777829266-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MGIEqBMMwAoeIigQGKA-887967055%2Fuploads%2Fgit-blob-48bc27eb1b1114861738f5b67bd90441cf38bff2%2FCapture%20d%E2%80%99%C3%A9cran%202022-06-18%20%C3%A0%2007.41.37.png?alt=media)

## Étapes

1. Rendez-vous sur <https://app.intram.org/register>.
2. Remplissez le formulaire avec les informations demandées (nom, email, mot de passe).
3. Lisez attentivement les conditions d'utilisation — plusieurs règles s'appliquent à tout marchand enregistré sur la plateforme.
4. Validez votre inscription.
5. Connectez-vous à votre compte sur <https://app.intram.org> avec l'email et le mot de passe choisis.

## Et après ?

Votre compte utilisateur est créé, mais vous ne pouvez **pas encore encaisser de paiements**. Pour activer la production, il faut :

1. [Activer votre compte business](/account/account-activation) — fournir les pièces justificatives demandées.
2. Récupérer vos [clés API](https://docs.intram.org/account/pages/aT9Gj8yGmX4lS8BSJirp#récupérer-vos-clés-api) depuis le menu **Développeurs**.
3. Intégrer l'API : [SDK et clients](/api-index) ou [Merchant API v1](/merchant-api-v1/merchant-api).

{% hint style="danger" %}
Tant que votre compte business n'est pas validé, vos clés `LIVE` ne fonctionneront pas. Vous pouvez en revanche utiliser librement les clés `SANDBOX` pour développer et tester.
{% endhint %}

## Problème de connexion ?

* Vérifiez l'email de confirmation reçu après l'inscription (regardez aussi vos spams).
* Mot de passe oublié : utilisez le lien "Mot de passe oublié" sur [la page de login](https://app.intram.org/login).
* Si rien ne fonctionne, contactez le support depuis le formulaire en pied de page du site.


# Activation du compte machand

Activer votre compte marchand Intram selon votre type (particulier, auto-entrepreneur, entreprise, association) — documents requis et limites applicables.

L'activation transforme votre compte utilisateur en **compte marchand validé**, condition indispensable pour utiliser les clés `LIVE` et encaisser de vrais paiements. Vos clés `SANDBOX` fonctionnent dès l'inscription, sans validation.

## Choisir votre type de compte

Intram propose **4 types de comptes** correspondant à votre statut légal. Chaque type donne accès à des modules différents et applique des limites de transactions distinctes.

| Type                  | Pour qui                                 | Limites hebdomadaires                       | Modules accessibles                                                                                                                                         |
| --------------------- | ---------------------------------------- | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Particulier**       | Personne physique sans activité déclarée | 10 transactions / 500 000 XOF par semaine   | [Intram Direct](/modules/intram-direct), [Intram Business](/modules/intram-business)                                                                        |
| **Auto-entrepreneur** | Activité individuelle déclarée           | 50 transactions / 5 000 000 XOF par semaine | [Intram Direct](/modules/intram-direct), [Intram Business](/modules/intram-business)                                                                        |
| **Entreprise**        | Société immatriculée (RCCM, SARL, SA…)   | Illimité                                    | Tous les modules ([Direct](/modules/intram-direct), [Business](/modules/intram-business), [Scholar](/modules/intram-scholar), [Immo](/modules/intram-immo)) |
| **Association**       | Association loi 1901 ou équivalent local | Illimité                                    | Tous les modules ([Direct](/modules/intram-direct), [Business](/modules/intram-business), [Scholar](/modules/intram-scholar), [Immo](/modules/intram-immo)) |

{% hint style="info" %}
Si vous démarrez petit, le type **Particulier** vous permet de tester l'encaissement réel rapidement sans paperasse lourde. Vous pourrez monter en gamme dès que votre activité grossit — voir [Évolution du type de compte](#évolution-du-type-de-compte).
{% endhint %}

## Documents requis par type

| Type              |     Pièce d'identité    | Selfie | Justificatif d'activité             |
| ----------------- | :---------------------: | :----: | ----------------------------------- |
| Particulier       | ✅ CNI, passeport ou CIP |    ✅   | —                                   |
| Auto-entrepreneur | ✅ CNI, passeport ou CIP |    ✅   | —                                   |
| Entreprise        | ✅ CNI, passeport ou CIP |    ✅   | ✅ Registre du commerce + numéro     |
| Association       | ✅ CNI, passeport ou CIP |    ✅   | ✅ Récépissé / statuts d'association |

Pour les entreprises et associations, un **numéro fiscal** peut également être demandé selon la juridiction.

## Étapes d'activation

### 1. Choisir le type de compte

Dans **Paramètres → Compte business**, sélectionnez le type qui correspond à votre statut légal.

### 2. Renseigner les informations

* Particulier / Auto-entrepreneur : nom complet, date de naissance, téléphone, adresse
* Entreprise / Association : raison sociale, numéro de registre, numéro fiscal, adresse du siège, contact principal

### 3. Téléverser les pièces justificatives

Selon votre type, fournissez les documents listés [ci-dessus](#documents-requis-par-type).

{% hint style="warning" %}
Vérifiez que vos documents sont **lisibles, non rognés, au format accepté** (PDF, JPG, PNG) et que la taille respecte la limite affichée dans le formulaire. Un document non conforme entraîne un refus et oblige à recommencer cette étape.
{% endhint %}

### 4. Soumettre pour vérification

Une fois tous les champs remplis et les documents téléversés, cliquez sur **Soumettre**. Le statut KYC passe à `pending`.

### 5. Attendre la validation

Notre équipe vérifie votre dossier sous **1 à 3 jours ouvrés** :

* ✅ **Validé** — vous recevez un email de confirmation, votre compte passe en `verified`, vos clés `LIVE` deviennent utilisables, les limites correspondant à votre type sont appliquées.
* ❌ **Refusé** — vous recevez un email avec la raison précise. Le statut KYC passe à `rejected`. Vous pouvez resoumettre directement depuis le dashboard après correction.

## Pendant la validation

Vous pouvez intégralement développer votre intégration en attendant :

* Utilisez les clés `SANDBOX` (Menu **Développeurs → API → mode SANDBOX**)
* Toutes les fonctionnalités sont disponibles en sandbox sans restriction de type ni de plafond
* Aucune somme réelle n'est manipulée — les wallets sandbox sont isolés

## Comprendre les limites hebdomadaires

Les limites s'appliquent **uniquement en mode live** et se calculent sur une **fenêtre glissante de 7 jours**.

* **`weekly_count`** — nombre maximum de transactions encaissées sur 7 jours
* **`weekly_amount`** — somme maximum des montants encaissés sur 7 jours, exprimée dans la devise principale (XOF par défaut)

Quand l'une des deux limites est atteinte, les nouvelles transactions sont rejetées jusqu'à ce que la fenêtre glissante se libère.

| Type              | Compteur transactions |     Plafond montant     |
| ----------------- | :-------------------: | :---------------------: |
| Particulier       |      10 / semaine     |  500 000 XOF / semaine  |
| Auto-entrepreneur |      50 / semaine     | 5 000 000 XOF / semaine |
| Entreprise        |        Illimité       |         Illimité        |
| Association       |        Illimité       |         Illimité        |

{% hint style="info" %}
Si vos limites sont trop basses pour votre activité, demandez un **upgrade de type** (voir ci-dessous) ou contactez le support pour un **dépassement ponctuel** négocié.
{% endhint %}

## Évolution du type de compte

Vous pouvez faire évoluer votre compte vers un type supérieur sans recréer un compte. Les transitions autorisées :

| Depuis            | Vers              | Documents supplémentaires à fournir  |
| ----------------- | ----------------- | ------------------------------------ |
| Particulier       | Auto-entrepreneur | (aucun supplémentaire)               |
| Particulier       | Entreprise        | Registre du commerce + numéro fiscal |
| Particulier       | Association       | Récépissé / statuts d'association    |
| Auto-entrepreneur | Entreprise        | Registre du commerce + numéro fiscal |
| Auto-entrepreneur | Association       | Récépissé / statuts d'association    |
| Entreprise        | Association       | Récépissé / statuts d'association    |
| Association       | Entreprise        | Registre du commerce + numéro fiscal |

La **rétrogradation** (par exemple Entreprise → Particulier) n'est **pas proposée** depuis l'interface. Si vous avez besoin de changer dans ce sens, contactez le support.

### Comment demander un upgrade

1. **Paramètres → Compte business → Évoluer mon compte**
2. Sélectionnez le type cible
3. Remplissez les nouveaux champs requis (nouvelle raison sociale, nouveau numéro, etc.)
4. Téléversez les nouveaux justificatifs
5. Soumettez — le statut passe à `pending_upgrade` ; vos limites actuelles restent appliquées en attendant la validation
6. Une fois validé : votre type bascule, les nouvelles limites s'appliquent, et un historique de changement est enregistré

## Statuts KYC

Le champ `kyc_status` visible dans votre dashboard suit ce cycle :

| Statut          | Sens                                                 | Action attendue                      |
| --------------- | ---------------------------------------------------- | ------------------------------------ |
| (non renseigné) | Compte business créé, KYC pas encore soumis          | Compléter le formulaire d'activation |
| `pending`       | Dossier soumis, en cours d'examen                    | Patienter (1 à 3 jours ouvrés)       |
| `verified`      | Dossier accepté — vous pouvez utiliser les clés LIVE | —                                    |
| `rejected`      | Dossier refusé — voir la raison dans l'email reçu    | Corriger et resoumettre              |

## Voir aussi

* [Inscription](/account/register) — créer le compte utilisateur initial
* [Récupérer vos clés API](https://docs.intram.org/account/pages/aT9Gj8yGmX4lS8BSJirp#récupérer-vos-clés-api)
* [Devises et moyens de paiement supportés](/payment/supported-devices)
* [Frais](/payment/fees)


# Devises supportées

Devises (ISO 4217) et moyens de paiement actuellement pris en charge par Intram pour vos transactions.

{% hint style="info" %}
Cette section évolue continuellement à mesure que nous ajoutons de nouveaux pays et opérateurs.
{% endhint %}

## Devises

| Code ISO 4217 | Devise            | Zone                                                                             | Statut |
| :-----------: | ----------------- | -------------------------------------------------------------------------------- | :----: |
|     `XOF`     | Franc CFA (BCEAO) | UEMOA (Bénin, Burkina, Côte d'Ivoire, Mali, Niger, Sénégal, Togo, Guinée-Bissau) |    ✅   |

D'autres devises arriveront prochainement. Pour anticiper, structurez votre intégration pour passer la devise en paramètre (`currency: "XOF"`) plutôt que de la coder en dur.

## Moyens de paiement par pays

| Pays       | Mobile Money      | Code interne            |
| ---------- | ----------------- | ----------------------- |
| Bénin (BJ) | MTN Mobile Money  | `MTN_BENIN_229`         |
| Bénin (BJ) | Moov Africa Money | `MOOV_AFRICA_BENIN_229` |
| Bénin (BJ) | Sirius Bank       | `SBIN_BENIN_229`        |

Les cartes bancaires (Visa / Mastercard) sont également supportées via le gateway Stripe pour les transactions en `XOF` converties.

## Activer un moyen de paiement sur votre compte

1. Connectez-vous à <https://app.intram.org>
2. Menu **Paramètres → Moyens de paiement**
3. Activez les moyens souhaités — ils deviendront proposables aux clients dans vos transactions
4. Les taux appliqués apparaissent dans **Paramètres → Frais** ([documentation](/payment/fees))

## Voir aussi

* [Frais](/payment/fees) — calcul des commissions par moyen de paiement
* [Merchant API — Payouts](/merchant-api-v1/payouts) — pour reverser vers ces moyens depuis votre backend
* [Merchant API — Payment requests](/merchant-api-v1/payment-requests) — pour demander un paiement client via ces moyens


# Frais

Comment les frais Intram sont calculés selon le moyen de paiement, le pays, la catégorie marchand et le mode (sandbox / live).

Les frais Intram s'appliquent à chaque transaction et varient selon plusieurs critères. Cette page explique **le modèle de calcul** ; les **taux actuels** sont visibles dans votre tableau de bord (Menu **Développeurs → API** ou **Paramètres → Frais**), car ils peuvent être personnalisés par marchand.

## Modèle de calcul

Pour chaque transaction, le montant facturé au client est :

```
Montant facturé = Montant transaction + Frais
```

où **Frais = Montant × Taux%**, le taux dépendant du moyen de paiement et de votre profil.

Les frais sont ensuite répartis entre :

| Composante            | Description                                                               |
| --------------------- | ------------------------------------------------------------------------- |
| **Frais opérateur**   | Reversés au fournisseur du moyen de paiement (MTN, MOOV, banque, Stripe…) |
| **Commission Intram** | Conservés par Intram pour le service de plateforme                        |

**Commission Intram = Frais − Frais opérateur**

## Sources des taux (ordre de priorité)

| Priorité | Source                                             | Quand                                                                |
| :------: | -------------------------------------------------- | -------------------------------------------------------------------- |
|     1    | Taux personnalisé par marchand × moyen de paiement | Taux négocié sur-mesure                                              |
|     2    | Taux par défaut du moyen                           | Taux public standard                                                 |
|     3    | `0`                                                | Si aucun taux n'est défini (cas exceptionnel — contactez le support) |

Autrement dit : un taux négocié pour votre compte écrase toujours le taux public du moyen.

## Add-ons spécifiques

Selon le type de paiement, des frais supplémentaires peuvent s'appliquer :

| Cas                                                   | Mécanique                                               |
| ----------------------------------------------------- | ------------------------------------------------------- |
| Paiement initié via un **Payment Link** Intram Direct | Surcharge plateforme déduite du net versé au marchand   |
| Marchand de catégorie **club**                        | Surcharge catégorielle déduite du net versé au marchand |
| Refund mobile money                                   | Frais provider éventuels débités du wallet marchand     |

Ces add-ons sont définis au niveau plateforme ; consultez le support ou votre dashboard pour les valeurs à jour.

## Exemple chiffré (illustratif)

Soit une transaction MTN Bénin de **10 000 XOF** avec un taux de `2 %` et une part opérateur de `1,5 %` :

```
Montant transaction    = 10 000
Frais (2%)             =    200
Frais opérateur (1,5%) =    153
Commission Intram      =     47
Montant facturé client = 10 200
Net versé au marchand  =  9 800
```

Le marchand est crédité de `9 800` au moment du settlement. Le provider reçoit `153` ; Intram conserve `47`.

{% hint style="warning" %}
**Les chiffres ci-dessus sont un exemple — pas un barème réel.** Pour vos taux réels, ouvrez **Paramètres → Frais** dans le dashboard.
{% endhint %}

## Voir vos taux applicables

1. Connectez-vous sur <https://app.intram.org>
2. Menu **Paramètres → Frais** (ou **Développeurs → API → Frais** selon votre version d'UI)
3. Le tableau liste chaque moyen de paiement activé sur votre compte, avec le taux **effectivement appliqué** (taux personnalisé prioritaire, sinon taux par défaut)
4. Les changements de taux décidés par l'équipe Intram sont notifiés par email et visibles ici en temps réel

## Activer un moyen de paiement pour voir ses frais

Si aucun moyen n'apparaît dans votre tableau :

1. Menu **Paramètres → Moyens de paiement**
2. Activez les moyens souhaités (MTN, MOOV, SBIN, Visa/Mastercard, virement…)
3. Une fois activés, ils apparaissent dans **Paramètres → Frais** avec leurs taux

## Frais en mode sandbox

Les frais sont calculés à l'identique en sandbox (même taux, même répartition), mais comme rien ne touche les wallets de production, c'est uniquement utile pour vérifier vos arrondis et l'affichage côté client.

## Voir aussi

* [Devises supportées](/payment/supported-devices)
* [Merchant API — Solde wallet](/merchant-api-v1/balance) — pour lire vos balances après application des frais
* [Refunds](/merchant-api-v1/refunds) — comment les frais se comportent lors d'un remboursement


# Intram Direct

Intram Direct — créez et partagez des liens de paiement personnalisés pour encaisser sans site web ni intégration complexe.

**Intram Direct** est la solution la plus simple pour commencer à encaisser : vous créez un **lien de paiement** depuis le dashboard, vous le partagez à vos clients (SMS, email, WhatsApp, réseaux sociaux, QR code), et l'argent crédite votre wallet dès qu'ils paient.

Aucun site web, aucun développement requis. Disponible pour **tous les types de comptes** (Particulier, Auto-entrepreneur, Entreprise, Association).

## Cas d'usage typiques

* **Indépendant / freelance** — facturer un client ponctuel sans avoir à installer un système de paiement complet
* **Petit commerce** — partager un lien fixe pour un produit unique (formation, abonnement annuel, ticket d'événement)
* **Collecte de fonds** — récolter des dons pour une association ou un projet
* **Vente sur les réseaux sociaux** — coller le lien dans une bio Instagram, WhatsApp Business ou une story
* **Facturation manuelle** — envoyer un lien personnalisé à un client après accord verbal

## Comment ça marche

1. **Vous créez un lien** depuis le dashboard avec : libellé, prix, description (optionnelle), image (optionnelle), type de paiement
2. **Vous obtenez** une URL courte (`https://gateway.intram.org/<référence>`) et un **QR code** prêts à partager
3. **Votre client ouvre le lien**, voit le détail, choisit son moyen de paiement et règle
4. **L'argent crédite votre wallet** dès la confirmation du provider
5. **Vous êtes notifié** par email + dashboard, et — si vous avez configuré un [webhook](/merchant-api-v1/webhooks) — par notification signée

## Créer un lien depuis le dashboard

1. Connectez-vous sur <https://app.intram.org>
2. Menu **Intram Direct → Liens de paiement → Nouveau lien**
3. Remplissez :
   * **Libellé** — nom interne pour vous y retrouver
   * **Prix** — montant fixe à encaisser
   * **Utilité / Description** — ce que voit le client sur la page de paiement
   * **Image** — optionnelle, affichée sur la page (logo produit, photo, etc.)
   * **URL de redirection** — page de votre site où renvoyer le client après paiement (succès)
   * **Type de paiement** — moyens autorisés (Mobile Money, Cartes…)
4. Validez — le lien est généré, accompagné d'un QR code téléchargeable

## Gérer vos liens

Depuis **Intram Direct → Liens de paiement** vous pouvez :

* **Activer / désactiver** un lien sans le supprimer (utile pour les ventes flash)
* **Archiver** un lien obsolète sans perdre l'historique des transactions
* **Voir les statistiques** par lien : nombre de paiements, montant collecté, taux de succès
* **Exporter** la liste des paiements en CSV pour votre comptabilité

## Suivre vos transactions

Menu **Intram Direct → Statistiques** :

* **Revenu total** sur la période choisie
* **Taux de succès** (transactions réussies / tentatives)
* **Liens actifs** vs archivés
* **Filtrage** par lien, par statut, par plage de dates
* **Détail transaction par transaction** avec recherche

Vous pouvez aussi consulter chaque transaction individuelle via [`GET /merchant/transactions/:reference`](/merchant-api-v1/transactions) depuis votre backend.

## Frais

Une **surcharge plateforme** s'applique aux transactions encaissées via Intram Direct (en plus des frais standards du moyen de paiement choisi par le client). Elle est déduite du montant net versé au marchand.

Voir [Frais](/payment/fees) pour le détail du calcul et vos taux applicables.

## Intégration API (optionnelle)

Si vous voulez **automatiser la création de liens** ou les exposer dans votre propre interface, deux options :

### Option 1 — Créer une transaction one-shot depuis votre backend

Plutôt qu'un lien réutilisable, créez une [demande de paiement à usage unique](/merchant-api-v1/payment-requests) via la Merchant API :

```bash
POST /v1/payment-requests
{
  "invoice": {
    "amount": 12000,
    "currency": "XOF",
    "description": "Commande #4521",
    "customer": { "email": "client@example.com" }
  },
  "return_urls": {
    "success": "https://shop.example.com/ok",
    "cancel":  "https://shop.example.com/no"
  }
}
```

Vous obtenez un `gateway_url` à présenter / envoyer au client. Idéal pour les paniers de boutique en ligne.

### Option 2 — Recevoir les notifications de paiement de vos liens existants

Configurez un [webhook](/merchant-api-v1/webhooks) avec l'event `payment_request.paid` (et `payment_request.failed`) pour recevoir en temps réel le résultat de chaque paiement réalisé via vos liens Intram Direct.

## Limites

* Soumises aux [limites hebdomadaires de votre type de compte](/account/account-activation#comprendre-les-limites-hebdomadaires).
* Un même lien peut servir un nombre **illimité** de paiements (sauf si vous le désactivez).
* La devise par défaut est `XOF` ; voir [Devises supportées](/payment/supported-devices) pour les autres.

## Voir aussi

* [Intram Business](/modules/intram-business) — pour gérer un vrai catalogue d'offres et des clients récurrents
* [Frais](/payment/fees) — comprendre le coût d'une transaction
* [Merchant API — Payment requests](/merchant-api-v1/payment-requests) — l'équivalent API d'un lien à usage unique
* [Webhooks signés](/merchant-api-v1/webhooks) — recevoir les notifications de paiement


# Intram Business

Intram Business — catalogue d'offres, gestion clients, abonnements et installments pour piloter votre activité commerciale depuis Intram.

**Intram Business** transforme Intram en mini-CRM commercial. Vous gérez votre **catalogue d'offres** (produits + plans d'abonnement), un **fichier client** dédié, et l'**historique des paiements** liés à chaque client. C'est l'outil pour aller au-delà du paiement ponctuel et structurer votre activité récurrente.

Disponible pour **tous les types de comptes** (Particulier, Auto-entrepreneur, Entreprise, Association).

## Cas d'usage typiques

* **Coach / formateur** — vendre des prestations à l'unité (`product`) ou des packages d'accompagnement à paiements échelonnés (`installments`)
* **École privée hors module Scholar** — facturer des frais de scolarité mensuels (`monthly`)
* **Salle de sport / club** — abonnements mensuels ou annuels avec renouvellement automatique
* **Box / abonnement à la box** — livraisons récurrentes (`weekly`, `monthly`)
* **Logiciel / SaaS** — facturation d'abonnements à votre service
* **Prestations sur devis** — encaissement en plusieurs versements négociés avec le client

## Concepts clés

### Offre (Offering)

Une entrée de votre **catalogue**. Deux types :

| Type          | Description                                   | Exemple                             |
| ------------- | --------------------------------------------- | ----------------------------------- |
| **`product`** | Vente à l'unité, paiement immédiat            | "Pack 10 séances de coaching"       |
| **`plan`**    | Vente récurrente avec calendrier de paiements | "Abonnement mensuel salle de sport" |

### Calendrier (pour les plans)

| `schedule_type` | Comportement                                      |
| --------------- | ------------------------------------------------- |
| `immediate`     | Paiement unique immédiat                          |
| `daily`         | Prélèvement quotidien                             |
| `weekly`        | Prélèvement hebdomadaire                          |
| `monthly`       | Prélèvement mensuel                               |
| `yearly`        | Prélèvement annuel                                |
| `installments`  | Découpage en N échéances (négocié au cas par cas) |

Pour `installments`, vous précisez le nombre d'échéances (minimum 2) — utile pour les ventes en 3, 4 ou 6 fois sans frais.

### Client (MarchandClient)

Une fiche dans votre **carnet client** local. Distincte du compte utilisateur Intram du payeur : un même client peut avoir une fiche chez plusieurs marchands.

Chaque fiche regroupe :

* Coordonnées (nom, email, téléphone)
* Catégorie client (configurable depuis le dashboard)
* Historique de toutes les transactions le concernant
* Solde dû éventuel (ventes non soldées)

### Transaction client (MarchandClientTransaction)

Chaque vente / paiement enregistré est rattaché à un client. Le tableau de bord Business vous montre par client :

* Total dépensé
* Dernière transaction
* Solde restant dû
* Échéances à venir

## Comment ça marche

```
1. Vous créez votre catalogue d'offres depuis le dashboard
   ├── Produits (vente unique)
   └── Plans (abonnement / installments)

2. Vous ajoutez vos clients (fichier local)
   └── Ou ils sont créés automatiquement lors de la première vente

3. Vous facturez :
   ├── via un lien de paiement adossé à l'offre (encaissement online)
   └── via un paiement manuel enregistré (espèces, virement, autre)

4. Le système suit :
   ├── Calendrier des prochaines échéances
   ├── Statuts (à venir / payé / en retard)
   ├── Notifications client (email + SMS)
   └── Solde par client + global
```

## Créer une offre depuis le dashboard

1. Menu **Intram Business → Catalogue → Nouvelle offre**
2. Choisissez le type : **Produit** ou **Plan**
3. Remplissez :
   * **Nom** + description
   * **Prix** (par unité ou par échéance pour les plans)
   * **Image** optionnelle
   * **Catégorie** (organisez votre catalogue)
   * Pour les plans : **type de calendrier** + nombre d'échéances si `installments`
4. Validez — l'offre devient `active` et peut être vendue immédiatement

Les offres peuvent être **archivées** (gardent l'historique des ventes passées sans pouvoir être revendues).

## Gérer vos clients

Menu **Intram Business → Clients** :

* **Liste** des clients avec recherche par nom / email / téléphone
* **Fiche client** : coordonnées, historique des transactions, solde
* **Catégories** : créez vos propres tags (Premium, Particulier, Pro…) pour segmenter
* **Import / export CSV** pour synchroniser avec votre outil habituel

## Suivre vos transactions Business

Menu **Intram Business → Transactions** :

* Liste paginée des paiements (online + enregistrés manuellement)
* Filtre par client, par offre, par statut, par plage de dates
* Détail par transaction : montant, frais, méthode, client, offre liée
* Échéances à venir pour les plans actifs

## Paiements en ligne vs manuels

Intram Business distingue deux origines de paiement :

| Origine               | Comment                                                                                  |         Frais        |
| --------------------- | ---------------------------------------------------------------------------------------- | :------------------: |
| **Paiement en ligne** | Le client paie via un lien généré (Mobile Money, carte)                                  |   ✅ Frais standards  |
| **Paiement manuel**   | Vous enregistrez vous-même un règlement reçu hors plateforme (espèces, virement, chèque) | ❌ Aucun frais Intram |

Les paiements manuels servent à **garder une vue unifiée** de votre compta, sans surcharge.

## Frais

Pour les paiements **en ligne** transitant par Intram Business, les frais suivent le [modèle standard](/payment/fees). Les paiements manuels enregistrés ne génèrent aucun frais.

## Intégration API (optionnelle)

Si vous voulez piloter votre catalogue ou recevoir des notifications, deux briques :

### Recevoir les notifications de paiement

Configurez un [webhook](/merchant-api-v1/webhooks) abonné à `payment_request.paid` — chaque encaissement Business déclenchera un appel signé vers votre URL.

### Lister vos transactions Business depuis votre backend

[`GET /merchant/transactions?limit=100`](/merchant-api-v1/transactions) renvoie l'ensemble des transactions de votre compte. Les transactions issues d'Intram Business sont identifiables par le champ `webhook_data` que vous aurez transmis à la création.

### Rembourser une transaction Business

Utilisez [`POST /merchant/refunds`](/merchant-api-v1/refunds) avec la `transaction_reference`. Le refund met à jour automatiquement l'historique du client dans Intram Business.

## Limites

* Soumises aux [limites hebdomadaires de votre type de compte](/account/account-activation#comprendre-les-limites-hebdomadaires).
* Nombre d'offres dans le catalogue : illimité.
* Nombre de clients dans le fichier : illimité.
* Plans à `installments` : minimum 2 échéances, pas de maximum théorique.

## Voir aussi

* [Intram Direct](/modules/intram-direct) — pour les encaissements ponctuels sans gestion client
* [Activation du compte marchand](/account/account-activation) — disponibilité par type de compte
* [Frais](/payment/fees)
* [Merchant API — Payment requests](/merchant-api-v1/payment-requests) — pour créer un lien de paiement programmatiquement
* [Webhooks signés](/merchant-api-v1/webhooks) — pour suivre les paiements en temps réel


# Intram Scholar

Intram Scholar — gestion complète des frais de scolarité, élèves, parents, classes et années académiques, avec encaissement en ligne et hors-ligne.

**Intram Scholar** est la solution complète pour les **établissements scolaires** : écoles privées, instituts, centres de formation. Vous y gérez vos années académiques, classes, élèves, parents, frais et leurs échéances, paiements en ligne et hors-ligne, factures et rappels automatisés — le tout depuis un dashboard dédié.

{% hint style="info" %}
**Disponible uniquement** pour les types de compte **Entreprise** et **Association**. Les types Particulier et Auto-entrepreneur n'ont pas accès à ce module. Voir [Activation du compte marchand](/account/account-activation).
{% endhint %}

## Cas d'usage typiques

* **École primaire / collège / lycée privé** — facturation annuelle découpée en trimestres ou mensualités
* **Université / institut supérieur** — frais d'inscription + frais de scolarité par semestre
* **Centre de formation** — encaissement par session avec possibilité de paiement échelonné
* **École spécialisée** (musique, langue, sport…) — abonnements + frais d'inscription récurrents

## Concepts clés

### École

L'entité de base. Un marchand peut posséder **plusieurs écoles** depuis le même compte (utile pour les groupes scolaires). Chaque école a son propre nom, adresse, contact, logo et paramètres.

### Année académique

Définit le calendrier scolaire (ex : 2024-2025). Tout est organisé autour des années — les classes, frais et inscriptions sont rattachés à une année académique précise. Cela permet de **clore une année** et passer à la suivante sans perdre l'historique.

### Classe

Un regroupement d'élèves au sein d'une année académique. Chaque classe a son nom (6ème A, CM2, L1 Informatique…) et est rattachée à une année. Les frais sont configurés par classe.

### Frais (Fee)

Un montant à percevoir associé à un libellé (scolarité, inscription, cantine, transport, uniforme…). Chaque frais peut être :

* **Forfaitaire** — un montant fixe payable en une fois
* **Échéancé** — découpé en plusieurs versements (`instalments`) avec pourcentage et date butoir pour chaque échéance

Exemple : frais de scolarité 600 000 XOF découpé en 3 tranches : 40 % (octobre), 30 % (janvier), 30 % (avril).

### Élève (Student)

Une fiche par élève contenant son identité, sa classe, son statut, ses frais assignés et l'historique de ses paiements.

### Parent

Une fiche par responsable (parent, tuteur) reliée à un ou plusieurs élèves. Les rappels et notifications de paiement sont envoyés aux parents.

### Réductions (ReductionFee)

Remise appliquée à un élève sur un frais donné (ex : -50% sur la scolarité pour les boursiers, -10% pour le second enfant d'une fratrie).

## Comment ça marche

```
1. Vous créez votre école et configurez l'année académique en cours
2. Vous définissez vos classes et les frais associés (forfaitaires ou échéancés)
3. Vous inscrivez vos élèves dans leurs classes
   ├── Import depuis Excel / CSV pour les inscriptions en masse
   └── Ou saisie individuelle
4. Vous associez les frais à chaque élève (et appliquez des réductions si besoin)
5. Le système génère automatiquement les échéances et factures
6. Les parents reçoivent les notifications (email + SMS) avec un lien pour payer
7. Vous suivez les paiements en temps réel
   ├── Paiements en ligne (Mobile Money, carte) — créditent automatiquement
   └── Paiements manuels (espèces, virement) — vous les enregistrez vous-même
8. Rappels automatiques pour les échéances en retard
```

## Configurer une école

1. Menu **Intram Scholar → Écoles → Nouvelle école**
2. Renseignez : nom, sigle, adresse, contact, logo, description
3. Configurez les paramètres : devise, modes de paiement autorisés, modèle de facture
4. Créez l'**année académique** en cours (date début + date fin)
5. Créez vos **classes** et configurez les **frais** par classe

## Gérer les élèves

Menu **Intram Scholar → Élèves** :

* **Liste paginée** avec recherche par nom, classe, statut de paiement
* **Inscription individuelle** ou **import en masse** depuis un fichier Excel/CSV (le template d'import est téléchargeable)
* **Fiche élève** : informations, parents liés, frais affectés, historique des paiements, échéances à venir, solde restant dû
* **Mutation** : déplacer un élève d'une classe à une autre en conservant son historique

## Suivre les paiements

Menu **Intram Scholar → Paiements** ou **Statistiques** :

* **Vue d'ensemble** par école : encaissé sur la période, taux de recouvrement, top classes
* **Vue par classe** : qui a payé, qui doit, montant collecté vs attendu
* **Vue par élève** : timeline des règlements, prochaines échéances, retards
* **Export comptable** : CSV par école, par classe, par période

## Rappels et notifications automatiques

Le module envoie automatiquement :

* **Notifications de nouvelle facture** dès qu'une échéance est due
* **Rappels J-7, J-3, J-0** avant chaque échéance
* **Rappels de retard** à intervalle configurable après la date butoir
* **Confirmations de paiement** lors de chaque encaissement

Les canaux disponibles : **email**, **SMS**, **notification push** (si l'app mobile est utilisée par le parent). Configurables par école dans **Intram Scholar → Paramètres → Notifications**.

## Paiements en ligne vs manuels

| Origine               | Comment                                                                        |         Frais        |
| --------------------- | ------------------------------------------------------------------------------ | :------------------: |
| **Paiement en ligne** | Le parent paie via le lien reçu (Mobile Money, carte)                          |   ✅ Frais standards  |
| **Paiement manuel**   | Vous enregistrez un règlement reçu hors plateforme (espèces, virement, chèque) | ❌ Aucun frais Intram |

Les paiements manuels conservent une **trace identique** dans l'historique de l'élève — vous avez une vue unifiée pour votre comptabilité.

## Factures

Chaque échéance payée génère une **facture PDF** téléchargeable depuis la fiche élève. Le modèle peut être personnalisé avec votre logo et vos mentions légales depuis **Intram Scholar → Paramètres → Modèle de facture**.

## Frais

Les paiements **en ligne** suivent le [modèle standard](/payment/fees). Les paiements manuels enregistrés ne génèrent aucun frais Intram.

## Intégration API (optionnelle)

Si vous voulez synchroniser Intram Scholar avec un SI existant (logiciel de gestion scolaire, ERP), deux briques :

### Recevoir les notifications de paiement

Configurez un [webhook](/merchant-api-v1/webhooks) sur `payment_request.paid` — chaque encaissement scolarité déclenchera un appel signé vers votre URL. Vous pouvez ensuite mettre à jour votre système avec la référence de la transaction et celle de l'élève (transmise dans `webhook_data`).

### Récupérer les transactions Scholar depuis votre backend

[`GET /merchant/transactions`](/merchant-api-v1/transactions) renvoie toutes vos transactions. Les transactions issues de Scholar sont identifiables via le champ `webhook_data` que vous avez configuré.

## Limites

* Disponibilité : **Entreprise** et **Association** uniquement (cf. [Types de comptes](/account/account-activation)).
* Soumises aux limites hebdomadaires de votre type de compte (les comptes Entreprise/Association ont des limites **illimitées** par défaut).
* Nombre d'écoles, années, classes, élèves, frais : **illimité**.
* Import en masse : pas de limite stricte, mais privilégiez des lots de < 500 lignes pour de meilleures performances.

## Voir aussi

* [Intram Immo](/modules/intram-immo) — pour la gestion immobilière (loyers, charges, baux)
* [Intram Business](/modules/intram-business) — alternative légère sans gestion académique structurée
* [Activation du compte marchand](/account/account-activation) — disponibilité par type de compte
* [Frais](/payment/fees)
* [Merchant API — Webhooks](/merchant-api-v1/webhooks) — synchroniser avec un SI externe


# Intram Immo

Intram Immo — gestion complète de votre parc immobilier locatif : immeubles, logements, locataires, baux, loyers et charges récurrentes.

**Intram Immo** est la solution dédiée aux **propriétaires, agences immobilières et gestionnaires de biens locatifs**. Vous y gérez vos immeubles, vos logements (chambres, studios, appartements, villas, boutiques…), vos locataires et leurs baux, et vous encaissez automatiquement loyers et charges — en ligne ou hors-ligne — avec rappels, factures et état des lieux intégrés.

{% hint style="info" %}
**Disponible uniquement** pour les types de compte **Entreprise** et **Association**. Les types Particulier et Auto-entrepreneur n'ont pas accès à ce module. Voir [Activation du compte marchand](/account/account-activation).
{% endhint %}

## Cas d'usage typiques

* **Propriétaire bailleur** — gestion directe de quelques biens (studios étudiants, appartements meublés)
* **Agence immobilière** — gestion locative pour le compte de propriétaires multiples
* **Résidence étudiante / coliving** — gestion d'un immeuble avec multiples chambres ou colocations
* **Gestion mixte habitation + commerce** — immeubles avec rez-de-chaussée commercial et étages résidentiels
* **Foncière / société immobilière** — pilotage centralisé d'un portefeuille d'actifs

## Concepts clés

### Immeuble (Building)

L'unité géographique de votre patrimoine. Trois types :

* **Immeuble** — bâtiment avec plusieurs logements
* **Appartement** — bien individuel
* **Résidence étudiante** — immeuble dédié à la location étudiante (chambres, studios)

Chaque immeuble a son adresse, ses documents (titre de propriété, plans), et un délai de paiement par défaut configurable.

### Logement (Room)

Une unité louable au sein d'un immeuble. Types disponibles : **chambre**, **studio**, **appartement**, **maison**, **villa**, **boutique**, **colocation**, **T2 / T3 / T4 / T5**.

Chaque logement précise s'il est **meublé**, et si **eau / électricité / ordures** sont incluses dans le loyer. Statut : `libre` ou `occupé`.

### Locataire (Tenant)

Une fiche par locataire avec identité, contact, justificatifs et historique. Un locataire peut avoir **plusieurs baux** (déménagement interne, location de plusieurs biens chez vous).

### Bail (Lease)

Le contrat qui lie un locataire à un logement. C'est le cœur du module — il contient :

* **Dates** : début, fin, renouvellement automatique optionnel
* **Loyer** : montant et fréquence (mensuel, trimestriel, semestriel, annuel)
* **Avance** : nombre de mois d'avance demandés (et statut payé / en attente)
* **Caution** : montant retenu (et statut détenue / partiellement remboursée / remboursée)
* **Garant** : optionnel, avec coordonnées et lien de parenté
* **Règles d'occupation** : habitation ou commerce, sous-location, animaux, responsabilité des charges
* **Préavis** : durée côté locataire / côté propriétaire
* **Contact local** : gardien, agent, propriétaire référent
* **Contrat PDF** signé électroniquement par OTP (email ou SMS)
* **État des lieux** d'entrée et de sortie (notes + photos)
* **Documents** : pièces jointes (max 10 par bail), partageables ou non avec le locataire

Statut du bail : `actif`, `résilié`, `renouvelé`.

### Charge récurrente (RecurringCharge)

Frais périodiques liés à un logement ou un bail (eau, électricité, gardiennage, syndic…). Vous définissez le montant et la périodicité — le système génère les factures automatiquement.

### Réduction (Discount) et Pénalité (PenaltyConfig)

* **Réduction** — remise appliquée à un locataire (geste commercial, ancienneté, fidélité)
* **Pénalité** — règle de majoration appliquée automatiquement en cas de retard de paiement, configurable par immeuble

### Configuration des rappels (ReminderConfig)

Définit quand et comment relancer un locataire avant et après l'échéance (J-7, J-3, J+1, J+15…), par email, SMS ou notification push.

## Comment ça marche

```
1. Vous créez votre immeuble (adresse, type, documents)
2. Vous ajoutez vos logements avec leurs caractéristiques (type, surface, charges incluses)
3. Vous créez la fiche locataire (identité, contact, justificatifs)
4. Vous générez le bail
   ├── Dates, loyer, fréquence
   ├── Avance + caution
   ├── Règles d'occupation
   └── Garant optionnel
5. Le contrat PDF est généré, signé par vous, puis envoyé au locataire pour signature OTP
6. Vous réalisez l'état des lieux d'entrée (notes + photos)
7. Le système génère automatiquement les échéances de loyer + charges
8. Les locataires reçoivent les notifications avec lien de paiement
9. Vous suivez les paiements en temps réel
   ├── Paiements en ligne (Mobile Money, carte) — créditent automatiquement
   └── Paiements manuels (espèces, virement) — vous les enregistrez vous-même
10. Rappels automatiques + pénalités appliquées en cas de retard
11. À la résiliation : état des lieux de sortie, déductions, remboursement caution
```

## Configurer un immeuble

1. Menu **Intram Immo → Immeubles → Nouvel immeuble**
2. Renseignez : nom, type (immeuble / appartement / résidence étudiante), adresse, description
3. Téléversez les **documents** (titre de propriété, plans, autorisations)
4. Configurez le **délai de paiement par défaut** (nombre de jours après émission d'une facture avant qu'elle soit considérée en retard)
5. Ajoutez vos **logements** un à un (ou par lot)

## Configurer un logement

Pour chaque logement :

* **Type** — chambre, studio, T2…
* **Surface** et **étage** (optionnel)
* **Meublé** ou non
* **Charges incluses** — cochez si eau, électricité, ordures sont comprises dans le loyer
* **Loyer indicatif** (peut être ajusté à la signature du bail)
* **Photos** (optionnel)

## Créer un bail

Menu **Intram Immo → Baux → Nouveau bail** :

1. **Locataire** — sélectionner un locataire existant ou créer une fiche
2. **Logement** — choisir parmi vos logements libres
3. **Dates** — début, fin, renouvellement automatique ?
4. **Loyer** — montant et fréquence (mensuel par défaut)
5. **Avance** — nombre de mois et statut initial (payé / en attente)
6. **Caution** — nombre de mois retenus
7. **Règles** — usage (habitation / commerce), sous-location, animaux, charges
8. **Préavis** — délais en jours
9. **Garant** — si exigé, coordonnées complètes
10. **Génération du contrat PDF** — vous le signez, le locataire reçoit un lien OTP pour signer

## État des lieux

* **À l'entrée** : ajoutez des photos pièce par pièce + une note descriptive
* **À la sortie** : refaites le tour, identifiez les dégradations éventuelles, chiffrez les déductions sur la caution

Les états des lieux sont **horodatés** et **signés numériquement** par les deux parties.

## Suivre les paiements

Menu **Intram Immo → Paiements** ou **Statistiques** :

* **Vue d'ensemble par immeuble** : taux d'occupation, encaissé sur la période, impayés
* **Vue par logement** : locataire en place, prochaine échéance, retards
* **Vue par locataire** : historique des règlements, baux en cours, solde
* **Export comptable** : CSV par immeuble, par période

## Rappels et notifications

Le module envoie automatiquement :

* **Avis d'échéance** quelques jours avant la date butoir (J-7, J-3 — configurables)
* **Rappels de retard** à intervalle paramétrable (J+1, J+7, J+15…)
* **Application de pénalités** en cas de retard prolongé (selon la configuration de l'immeuble)
* **Confirmations de paiement** à chaque encaissement
* **Notifications de fin de bail** approchant pour les baux non auto-renouvelables

Canaux : **email**, **SMS**, **notification push**. Configurables par immeuble dans **Intram Immo → Paramètres → Notifications**.

## Paiements en ligne vs manuels

| Origine               | Comment                                                                        |         Frais        |
| --------------------- | ------------------------------------------------------------------------------ | :------------------: |
| **Paiement en ligne** | Le locataire paie via le lien reçu (Mobile Money, carte)                       |   ✅ Frais standards  |
| **Paiement manuel**   | Vous enregistrez un règlement reçu hors plateforme (espèces, virement, chèque) | ❌ Aucun frais Intram |

Les paiements manuels conservent une **trace identique** dans l'historique du bail.

## Résiliation d'un bail

Depuis la fiche bail → **Résilier** :

1. Indiquez la **raison** : demande du locataire, non-paiement, manquement, accord amiable, fin de contrat
2. Renseignez la **date de préavis** et la **date de départ**
3. Réalisez l'**état des lieux de sortie** (photos + notes)
4. Saisissez les **déductions éventuelles** (dégradations avec libellé et montant)
5. Le système calcule automatiquement le **remboursement caution** (caution − déductions)
6. Vous validez — le statut bail passe à `résilié`, le logement repasse `libre`

## Factures

Chaque échéance payée (loyer ou charge) génère une **facture PDF** téléchargeable depuis la fiche locataire ou la fiche bail. Modèle personnalisable avec votre logo et vos mentions légales depuis **Intram Immo → Paramètres → Modèle de facture**.

## Frais

Les paiements **en ligne** suivent le [modèle standard](/payment/fees). Les paiements manuels enregistrés ne génèrent aucun frais Intram.

## Intégration API (optionnelle)

Si vous voulez synchroniser Intram Immo avec un SI existant (logiciel de gestion locative, ERP, comptabilité) :

### Recevoir les notifications de paiement

Configurez un [webhook](/merchant-api-v1/webhooks) sur `payment_request.paid` — chaque encaissement loyer ou charge déclenchera un appel signé vers votre URL. Vous pouvez transmettre la référence du bail et celle du logement via le champ `webhook_data`.

### Récupérer les transactions Immo depuis votre backend

[`GET /merchant/transactions`](/merchant-api-v1/transactions) renvoie toutes vos transactions. Les transactions issues d'Immo sont identifiables via le champ `webhook_data` que vous avez configuré.

## Limites

* Disponibilité : **Entreprise** et **Association** uniquement (cf. [Types de comptes](/account/account-activation)).
* Soumises aux limites hebdomadaires de votre type de compte (les comptes Entreprise/Association ont des limites **illimitées** par défaut).
* Nombre d'immeubles, logements, locataires, baux : **illimité**.
* **Documents par bail** : **10 maximum** (titre de propriété, contrat d'assurance, états des lieux, etc.).
* **Photos par état des lieux** : pas de limite stricte, mais privilégiez des images compressées (< 2 Mo chacune).

## Voir aussi

* [Intram Scholar](/modules/intram-scholar) — pour la gestion des frais de scolarité
* [Intram Business](/modules/intram-business) — alternative légère pour de la facturation simple
* [Activation du compte marchand](/account/account-activation) — disponibilité par type de compte
* [Frais](/payment/fees)
* [Merchant API — Webhooks](/merchant-api-v1/webhooks) — synchroniser avec un SI externe


# Overview

Vue d'ensemble de l'API Merchant Intram v1 — solde, payouts, demandes de paiement, refunds, webhooks signés.

L'**API Merchant** est l'interface server-to-server pour piloter votre compte Intram depuis votre backend : lire le solde, déclencher des payouts, demander un paiement à un client, rembourser une transaction, recevoir des notifications signées.

Elle remplace progressivement l'ancienne API `/api/v1/payments/*` pour les usages backend ; les routes existantes restent en service inchangées (rétrocompatibilité totale).

## Caractéristiques

* **Asynchrone par défaut** — les mutations renvoient `202 Accepted` avec un `operation_id` que vous pouvez suivre par polling ou webhook signé.
* **Signée** — chaque requête porte un HMAC-SHA256 de son contenu (anti-tampering + anti-replay 5 min).
* **Idempotente** — chaque POST exige un `Idempotency-Key`; les rejeux dans les 24 h renvoient la réponse d'origine sans réexécuter.
* **Sandbox / Live isolés** — les clés sandbox touchent un wallet de test, les clés live touchent l'argent réel et exigent une whitelist IP.
* **Webhooks signés** — chaque event est livré avec un en-tête `X-Intram-Signature` et retenté selon un backoff exponentiel.

## Base URL

| Environnement        | URL                                              |
| -------------------- | ------------------------------------------------ |
| Production / Live    | `https://api.intram.org/v1`                      |
| Production / Sandbox | `https://api.intram.org/v1` (clé `pk_sandbox_…`) |

Le mode (sandbox vs live) est déterminé par la **clé** présentée — pas par l'URL.

## Endpoints en un coup d'œil

| Méthode           | Path                       | Description                                        | Type        |
| ----------------- | -------------------------- | -------------------------------------------------- | ----------- |
| `GET`             | `/balance`                 | Solde wallet courant                               | Synchrone   |
| `POST`            | `/payouts`                 | Reversement vers Mobile Money ou compte bancaire   | Async (202) |
| `POST`            | `/payment-requests`        | Demande de paiement client (renvoie l'URL gateway) | Async (202) |
| `POST`            | `/refunds`                 | Rembourse une transaction                          | Async (202) |
| `GET`             | `/transactions/:reference` | Statut d'une transaction                           | Synchrone   |
| `GET`             | `/operations/:id`          | Statut d'une opération asynchrone                  | Synchrone   |
| `GET/POST/DELETE` | `/webhooks`                | Gérer les souscriptions webhook                    | Synchrone   |

## Par où commencer

1. Lisez l'[Authentification](/merchant-api-v1/authentication) — c'est la marche la plus haute.
2. Gardez 5 minutes pour [Sandbox vs Live](/merchant-api-v1/sandbox-vs-live) — c'est ce qui décide si votre requête passe ou pas en production.
3. Suivez le [Quickstart](/merchant-api-v1/quickstart) — un appel `curl` signé à `/balance` en moins de 2 minutes.
4. Activez les [Webhooks](/merchant-api-v1/webhooks) — c'est comme ça que vous saurez quand un payout, un remboursement ou un paiement aboutit.

{% hint style="info" %}
Une collection Postman prête à l'emploi (avec script de signature automatique) est disponible : `paycfa-webservice/docs/merchant-api.postman_collection.json` + l'environnement sandbox associé.
{% endhint %}


# Quickstart

Faire ton premier appel signé à l'API Merchant en moins de 5 minutes.

L'objectif : lire le solde de ton wallet sandbox avec un appel `curl` signé. Si ce premier ping marche, ton intégration est sur de bons rails.

## Prérequis

* Compte Intram avec accès au dashboard
* Clés sandbox (Menu **Développeurs → API → mode SANDBOX**)
* `curl` + `openssl` installés (ou utilise notre [collection Postman](https://github.com/intram/paycfa-webservice/blob/main/docs/merchant-api.postman_collection.json))

## 1. Récupère tes clés sandbox

```
X-Api-Key      = pk_sandbox_a1b2c3d4...
secret_key     = sk_sandbox_e5f6g7h8...   (ne sort jamais en clair)
```

## 2. Signe et appelle `/balance`

{% code title="quickstart.sh" %}

```bash
#!/usr/bin/env bash
set -euo pipefail

PUB="pk_sandbox_a1b2c3d4..."
SEC="sk_sandbox_e5f6g7h8..."
HOST="https://api.intram.org/v1"

# Path interne (après rewrite Nginx) — c'est lui qui sert à la signature
SIGN_PATH="/api/v1/merchant/balance"

# Timestamp ISO 8601 UTC
TS=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")

# Payload : timestamp\nMETHOD\nPATH\nQUERY\nBODY
PAYLOAD="$TS
GET
$SIGN_PATH

"

# HMAC-SHA256 hex
SIG="sha256=$(printf %s "$PAYLOAD" | openssl dgst -sha256 -hmac "$SEC" -binary | xxd -p -c 256)"

# Appel
curl -i "$HOST/balance" \
  -H "X-Api-Key: $PUB" \
  -H "X-Timestamp: $TS" \
  -H "X-Signature: $SIG"
```

{% endcode %}

Lance-le :

```bash
chmod +x quickstart.sh && ./quickstart.sh
```

Réponse attendue :

```json
HTTP/2 200
{
  "error": false,
  "http_status": 200,
  "data": {
    "currency": "XOF",
    "payment_method_balances": [
      { "payment_method": "MTN Bénin", "available": 10000, "pending": 0 }
    ]
  }
}
```

## 3. Déclenche un payout sandbox

```bash
PAYLOAD_BODY='{
  "amount": 5000,
  "currency": "XOF",
  "destination": {
    "type": "mobile_money",
    "country_code": "BJ",
    "provider_code": "MTN_BENIN_229",
    "msisdn": "22961234567"
  },
  "reference": "TEST-PO-1"
}'

SIGN_PATH="/api/v1/merchant/payouts"
TS=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
PAYLOAD="$TS
POST
$SIGN_PATH

$PAYLOAD_BODY"
SIG="sha256=$(printf %s "$PAYLOAD" | openssl dgst -sha256 -hmac "$SEC" -binary | xxd -p -c 256)"

curl -i "$HOST/payouts" \
  -H "X-Api-Key: $PUB" \
  -H "X-Timestamp: $TS" \
  -H "X-Signature: $SIG" \
  -H "Idempotency-Key: test-po-1-attempt-1" \
  -H "Content-Type: application/json" \
  -d "$PAYLOAD_BODY"
```

Réponse :

```json
HTTP/2 202
{
  "error": false,
  "http_status": 202,
  "data": {
    "operation_id": "op_2f4a...",
    "type": "payout",
    "status": "queued",
    "env": "sandbox",
    "date": "2026-05-20T10:30:00.000Z",
    "_links": { "self": "/api/v1/merchant/operations/op_2f4a..." }
  }
}
```

## 4. Suivre l'opération

```bash
SIGN_PATH="/api/v1/merchant/operations/op_2f4a..."
TS=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
PAYLOAD="$TS
GET
$SIGN_PATH

"
SIG="sha256=$(printf %s "$PAYLOAD" | openssl dgst -sha256 -hmac "$SEC" -binary | xxd -p -c 256)"

curl -s "$HOST/operations/op_2f4a..." \
  -H "X-Api-Key: $PUB" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" | jq
```

## 5. Étapes suivantes

| Si tu veux…                      | Direction                                                       |
| -------------------------------- | --------------------------------------------------------------- |
| Apprendre la signature en détail | [Authentification & signature](/merchant-api-v1/authentication) |
| Comprendre sandbox vs live       | [Sandbox vs Live](/merchant-api-v1/sandbox-vs-live)             |
| Recevoir les updates en push     | [Webhooks signés](/merchant-api-v1/webhooks)                    |
| Voir tous les endpoints          | [Référence](/merchant-api-v1/balance)                           |
| Comprendre les rejeux sûrs       | [Idempotency](/merchant-api-v1/idempotency)                     |

{% hint style="success" %}
**Passer en live** : récupère tes clés `pk_live_…`, configure ta whitelist IP dans le dashboard (Menu Développeurs → IPs autorisées), puis remplace les clés. Aucun autre changement de code.
{% endhint %}


# Authentification & signature

Headers, calcul du HMAC-SHA256 et exemples Node.js / PHP / cURL pour signer une requête vers l'API Merchant.

Chaque requête vers l'API Merchant porte trois headers obligatoires : ta clé publique, un timestamp, et une signature HMAC du contenu calculée avec ta clé secrète.

## Récupérer tes clés

* Connecte-toi sur <https://app.intram.org/login>
* Menu **Développeurs → API**
* Choisis le mode `SANDBOX` (clés `pk_sandbox_…`) ou `LIVE` (clés `pk_live_…`)
* Tu obtiens 3 clés par environnement :

| Clé          | Usage                                                                                        |
| ------------ | -------------------------------------------------------------------------------------------- |
| Clé publique | Envoyée dans le header `X-Api-Key` — identifie ton compte                                    |
| Clé secrète  | **Ne sort jamais** — sert à calculer la signature HMAC                                       |
| Clé privée   | Réservée à l'ancienne API (`/api/v1/payments/request`) ; pas utilisée par le Merchant API v1 |

{% hint style="danger" %}
La clé secrète ne doit **jamais** apparaître dans une requête HTTP, dans le code front-end, ni dans un repo Git. Elle ne sert qu'à signer côté serveur.
{% endhint %}

## Headers obligatoires

```http
X-Api-Key:     pk_live_a1b2c3d4…
X-Timestamp:   2026-05-20T10:30:00.000Z
X-Signature:   sha256=<hex>
```

Pour les mutations (POST / PUT / PATCH), ajoute :

```http
Idempotency-Key: <UUID v4, 8 à 128 caractères [A-Za-z0-9_-]>
Content-Type:    application/json
```

* `X-Timestamp` doit être en ISO 8601 UTC. Toute requête de plus de **5 minutes** par rapport à l'heure serveur est rejetée (anti-replay).
* `Idempotency-Key` doit être unique par requête métier. Voir [Idempotency](/merchant-api-v1/idempotency).

## Construire la chaîne à signer

La signature couvre **5 champs séparés par `\n`** :

```
<timestamp>\n<METHOD>\n<path>\n<sorted_query>\n<raw_body>
```

| Champ          | Règle                                                                                        |
| -------------- | -------------------------------------------------------------------------------------------- |
| `timestamp`    | La valeur exacte du header `X-Timestamp`                                                     |
| `METHOD`       | En MAJUSCULES : `GET`, `POST`, `DELETE`…                                                     |
| `path`         | Le chemin **après** le rewrite Nginx, donc `/api/v1/merchant/...` (voir note ci-dessous)     |
| `sorted_query` | Paires `clé=valeur` triées alphabétiquement, jointes par `&`. Chaîne vide si pas de query    |
| `raw_body`     | Le body JSON **tel qu'envoyé** (pas de re-sérialisation). Chaîne vide si GET / HEAD / DELETE |

Puis HMAC-SHA256 de cette chaîne avec ta `secret_key`, encodé en hex, préfixé par `sha256=`.

{% hint style="warning" %}
**Important — le path signé n'est pas celui de l'URL publique.** Si tu appelles `https://api.intram.org/v1/payouts`, Nginx réécrit vers `/api/v1/merchant/payouts` avant de toucher le serveur. C'est **ce path-là** qui sert à la signature. Hardcode `/api/v1/merchant/<endpoint>` dans ton SDK.
{% endhint %}

## Exemple Node.js complet

{% code title="signed-request.js" %}

```javascript
const crypto = require('crypto');
const axios = require('axios');

const PUBLIC_KEY = process.env.INTRAM_PUBLIC_KEY;
const SECRET_KEY = process.env.INTRAM_SECRET_KEY;

async function signedPost(endpoint, body, idempotencyKey) {
  // path à signer (path interne, pas l'URL publique)
  const path = `/api/v1/merchant${endpoint}`;
  const rawBody = JSON.stringify(body);
  const timestamp = new Date().toISOString();

  const signingPayload = [
    timestamp,
    'POST',
    path,
    '',          // pas de query
    rawBody,
  ].join('\n');

  const signature = 'sha256=' + crypto
    .createHmac('sha256', SECRET_KEY)
    .update(signingPayload)
    .digest('hex');

  return axios.post(`https://api.intram.org/v1${endpoint}`, rawBody, {
    headers: {
      'Content-Type':    'application/json',
      'X-Api-Key':       PUBLIC_KEY,
      'X-Timestamp':     timestamp,
      'X-Signature':     signature,
      'Idempotency-Key': idempotencyKey,
    },
  });
}

// Utilisation
const { data } = await signedPost('/payouts', {
  amount: 25000,
  currency: 'XOF',
  destination: {
    type: 'mobile_money',
    provider_code: 'MTN_BENIN_229',
    msisdn: '22961234567',
  },
}, 'po-2026-0001-attempt-1');

console.log(data.data.operation_id);
```

{% endcode %}

## Exemple PHP

{% code title="signed-request.php" %}

```php
<?php
function signedPost($endpoint, array $body, string $idempotencyKey) {
    $publicKey = getenv('INTRAM_PUBLIC_KEY');
    $secretKey = getenv('INTRAM_SECRET_KEY');

    $path      = "/api/v1/merchant{$endpoint}";
    $rawBody   = json_encode($body, JSON_UNESCAPED_SLASHES);
    $timestamp = gmdate('Y-m-d\TH:i:s.000\Z');

    $signingPayload = implode("\n", [$timestamp, 'POST', $path, '', $rawBody]);
    $signature = 'sha256=' . hash_hmac('sha256', $signingPayload, $secretKey);

    $ch = curl_init("https://api.intram.org/v1{$endpoint}");
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => $rawBody,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => [
            'Content-Type: application/json',
            "X-Api-Key: {$publicKey}",
            "X-Timestamp: {$timestamp}",
            "X-Signature: {$signature}",
            "Idempotency-Key: {$idempotencyKey}",
        ],
    ]);
    return json_decode(curl_exec($ch), true);
}
```

{% endcode %}

## Exemple cURL

{% code title="balance.sh" %}

```bash
PUB="pk_sandbox_…"
SEC="sk_sandbox_…"
TS=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
PATH_="/api/v1/merchant/balance"

# Pour GET : body vide, query vide
PAYLOAD="$TS
GET
$PATH_

"

SIG="sha256=$(printf %s "$PAYLOAD" | openssl dgst -sha256 -hmac "$SEC" -binary | xxd -p -c 256)"

curl -i https://api.intram.org/v1/balance \
  -H "X-Api-Key: $PUB" \
  -H "X-Timestamp: $TS" \
  -H "X-Signature: $SIG"
```

{% endcode %}

## Codes d'erreur d'authentification

| Code HTTP | `code` JSON          | Cause                                                                                         |
| :-------: | -------------------- | --------------------------------------------------------------------------------------------- |
|    401    | `missing_api_key`    | Header `X-Api-Key` absent                                                                     |
|    401    | `invalid_api_key`    | Clé inconnue ou révoquée                                                                      |
|    401    | `signature_invalid`  | HMAC mauvais, timestamp hors fenêtre, ou header manquant                                      |
|    403    | `merchant_inactive`  | Compte marchand non validé                                                                    |
|    403    | `ip_allowlist_empty` | Clé live mais aucune IP whitelistée (cf. [Sandbox vs Live](/merchant-api-v1/sandbox-vs-live)) |
|    403    | `ip_not_allowed`     | L'IP appelante n'est pas dans la whitelist live                                               |

{% hint style="success" %}
Si tu utilises notre [collection Postman](https://github.com/intram/paycfa-webservice/blob/main/docs/merchant-api.postman_collection.json), tout ça est calculé automatiquement avant chaque envoi — tu n'as qu'à renseigner `apiKey` et `secretKey` dans l'environnement.
{% endhint %}


# Sandbox vs Live + IP allowlist

Différences entre les modes sandbox et live, et configuration de la whitelist d'IPs obligatoire en mode live.

Le mode (sandbox vs live) est **déterminé côté serveur** par la clé que tu présentes — pas par un paramètre de requête. Une clé `pk_sandbox_…` tape un wallet de test ; une clé `pk_live_…` tape l'argent réel.

## Tableau comparatif

| Critère                                 | Sandbox (`pk_sandbox_…`)                   | Live (`pk_live_…`)                       |
| --------------------------------------- | ------------------------------------------ | ---------------------------------------- |
| Wallet touché                           | Wallet sandbox (jouet, isolé)              | Wallet de production (argent réel)       |
| Providers externes (MTN, MOOV, Stripe…) | Endpoints sandbox                          | Endpoints production                     |
| Signature HMAC                          | Requise                                    | Requise                                  |
| Idempotency-Key                         | Requise sur POST/PUT/PATCH                 | Requise sur POST/PUT/PATCH               |
| **Whitelist IP**                        | **Désactivée** — appel depuis n'importe où | **Obligatoire** — sinon 403 systématique |
| Limites par opération                   | Permissives                                | Standards                                |
| Données stockées                        | Effacables sans impact                     | Auditées, comptables                     |

## La whitelist IP en mode live

C'est le filet de sécurité qui empêche qu'une clé live compromise soit utilisée depuis un serveur inconnu.

**Comment ça marche :**

1. Chaque marchand maintient une liste d'IPs ou de CIDR autorisés (max 20 entrées).
2. À chaque requête en mode live, l'API résout l'IP source (via `X-Real-IP` fourni par Nginx ou socket direct) et la compare à la liste.
3. Si l'IP correspond à une entrée exacte ou tombe dans un CIDR → la requête passe à l'étape suivante (vérification signature).
4. Sinon → 403, requête rejetée avant même de lire le body. La tentative est loggée (audit).

**Important — fail-closed** : une liste vide ne signifie pas "accepter tout". Elle signifie "rejeter tout". C'est volontaire : une clé live mal configurée ne peut jamais servir de trafic depuis l'Internet ouvert.

## Configurer la whitelist depuis le dashboard

* Menu **Développeurs → IPs autorisées**
* Récupère l'IP publique de ton serveur backend depuis ce serveur :

```bash
curl -4 ifconfig.me
```

{% hint style="warning" %}
**L'IP de ton navigateur n'est presque jamais la bonne** — c'est l'IP du serveur d'où ton backend va appeler l'API. Sauf si tu testes en local depuis la même machine que ton backend.
{% endhint %}

* Clique "Ajouter un serveur"
* Renseigne l'IP exacte (`203.0.113.4`) ou un bloc CIDR (`203.0.113.0/29` pour un cluster de 8 IPs derrière le même NAT)
* Sauvegarde — le changement est immédiat

## Formats acceptés

| Format      | Exemple          | Quand l'utiliser                                           |
| ----------- | ---------------- | ---------------------------------------------------------- |
| IPv4 exacte | `203.0.113.4`    | Un serveur unique                                          |
| IPv6 exacte | `2001:db8::1`    | Un serveur unique IPv6                                     |
| CIDR IPv4   | `203.0.113.0/29` | Cluster derrière un NAT, pool de sortie d'un load balancer |
| CIDR IPv6   | `2001:db8::/32`  | Bloc IPv6 attribué par ton fournisseur                     |

## Cas d'usage typiques

| Topologie                                 | Recommandation                                                        |
| ----------------------------------------- | --------------------------------------------------------------------- |
| 1 serveur backend, 1 IP fixe              | Ajoute l'IP exacte                                                    |
| Plusieurs serveurs derrière un NAT unique | Ajoute l'IP du NAT (une seule entrée)                                 |
| Cluster Kubernetes avec egress fixe       | Ajoute l'IP/CIDR de l'egress gateway                                  |
| Auto-scaling sans IP fixe                 | Mets en place un NAT egress avec IP statique, puis whitelist celle-ci |
| Plusieurs régions cloud                   | Ajoute chaque IP de sortie (max 20 entrées au total)                  |

{% hint style="danger" %}
Ne whitelistez pas `0.0.0.0/0` (ouvrir à tout) — l'API rejettera de toute façon une entrée aussi large, et c'est l'inverse du but recherché.
{% endhint %}

## Erreurs possibles

```json
// Liste vide sur clé live
HTTP 403
{
  "error": true,
  "code": "ip_allowlist_empty",
  "message": "Live API keys require at least one allowed IP. Configure it in your merchant dashboard before going live."
}

// IP appelante absente de la liste
HTTP 403
{
  "error": true,
  "code": "ip_not_allowed",
  "message": "This source IP is not in the merchant allowlist",
  "client_ip": "203.0.113.99"
}
```

## Tester en sandbox sans whitelist

1. Crée une clé sandbox depuis le dashboard (mode `SANDBOX`)
2. Utilise cette clé dans Postman / cURL / ton intégration locale — aucune restriction IP
3. Quand tout fonctionne, génère une clé live et configure la whitelist **avant** ton premier appel live


# Idempotency-Key

Pourquoi et comment utiliser l'Idempotency-Key pour rejouer une requête en toute sécurité.

Toute requête mutante (POST / PUT / PATCH) vers le Merchant API doit porter un header `Idempotency-Key`. C'est ce qui vous permet de **retenter** une requête sans risquer un double payout ou un double refund — même si la première tentative a coupé en cours de route.

## Format de la clé

```http
Idempotency-Key: po-2026-0001-attempt-1
```

* Longueur : **8 à 128 caractères**
* Charset : `[A-Za-z0-9_-]` (alphanum + underscore + tiret)
* Unique pour une opération métier — typiquement un UUID v4 ou ton ID interne suffixé

```javascript
// Exemple Node.js
const idem = crypto.randomUUID();   // 36 caractères, conforme
```

## Comment ça marche côté serveur

1. À réception, l'API calcule une empreinte SHA-256 de `(method + path + body)` et l'associe à la clé d'idempotence dans Redis avec un TTL de **24 h**.
2. Si la clé n'existe pas → la requête est traitée normalement. La réponse (status + body) est mémorisée.
3. Si la clé existe **avec la même empreinte** → la réponse mémorisée est renvoyée immédiatement, avec un header `Idempotent-Replay: true`. **Le traitement ne se rejoue pas.**
4. Si la clé existe **avec une empreinte différente** → 409 Conflict (`idempotency_conflict`). Tu as réutilisé une clé pour une opération différente, c'est interdit.

## Quand tu DOIS rejouer

| Cas                                  | Action                                                                    |
| ------------------------------------ | ------------------------------------------------------------------------- |
| Timeout côté client (pas de réponse) | Rejoue avec **la même** `Idempotency-Key`                                 |
| 5xx serveur                          | Rejoue avec la même clé, après un petit délai                             |
| Connexion réseau coupée              | Rejoue avec la même clé                                                   |
| 4xx (sauf 409 conflict)              | **Ne rejoue pas** — corrige la requête, puis utilise une **nouvelle** clé |

## Quand tu DOIS changer de clé

* Quand le contenu métier change (montant différent, destinataire différent…)
* Quand tu rejoues volontairement la même opération une seconde fois (par exemple un payout récurrent hebdomadaire)

## Erreurs

```json
// Clé manquante sur une mutation
HTTP 400
{
  "error": true,
  "code": "missing_idempotency_key",
  "message": "Idempotency-Key header is required for mutating requests"
}

// Format invalide
HTTP 400
{
  "error": true,
  "code": "invalid_idempotency_key",
  "message": "Idempotency-Key must be 8-128 alphanumeric / - / _ characters"
}

// Réutilisation avec un autre body
HTTP 409
{
  "error": true,
  "code": "idempotency_conflict",
  "message": "Idempotency-Key already used with a different request body"
}
```

## Bonne pratique : préfixer par l'opération métier

| Mauvais           | Bon                         |
| ----------------- | --------------------------- |
| `abc123`          | `payout-2026-05-20-batch-1` |
| `42`              | `refund-tx-AB12CD34EF-full` |
| `uuid-v4-anonyme` | `payment-req-order-4521`    |

Préfixer par l'opération métier facilite le debug : si tu vois une 409, tu sais immédiatement quel flow a généré le doublon.

## Cas d'usage : rejeu sûr après timeout

```javascript
async function safePayout(payload) {
  // 1. Génère la clé UNE SEULE FOIS
  const idem = `payout-${payload.reference}-${Date.now()}`;

  // 2. Boucle de retry avec la MÊME clé
  for (let attempt = 1; attempt <= 3; attempt++) {
    try {
      return await signedPost('/payouts', payload, idem);
    } catch (err) {
      // Si c'est un 5xx ou réseau, rejoue. Sinon, propage.
      if (err.response && err.response.status >= 500) {
        await sleep(2 ** attempt * 1000);
        continue;
      }
      throw err;
    }
  }
}
```

Si la première tentative a en fait abouti (mais que la connexion a coupé avant que tu ne reçoives la réponse), la seconde verra `Idempotent-Replay: true` et te rendra le résultat d'origine — pas de double débit.


# Enveloppe de réponse & erreurs

Format standard des réponses, catalogue des codes d'erreur, gestion des opérations asynchrones.

Toutes les réponses du Merchant API suivent une enveloppe JSON standard.

## Succès synchrone

```json
HTTP 200
{
  "error": false,
  "data": { /* contenu spécifique à l'endpoint */ }
}
```

## Succès asynchrone (POST mutants)

```json
HTTP 202 Accepted
{
  "error": false,
  "http_status": 202,
  "data": {
    "operation_id": "op_2f4a…",
    "type": "payout",
    "status": "queued",
    "env": "live",
    "date": "2026-05-20T10:30:00.000Z",
    "_links": { "self": "/api/v1/merchant/operations/op_2f4a…" }
  }
}
```

L'opération est en file d'attente. Suis son avancement via webhook (recommandé) ou polling sur `GET /operations/:id`.

## Erreur

```json
HTTP 4xx | 5xx
{
  "error": true,
  "http_status": 400,
  "code":  "validation_error",
  "message": "Invalid payout request",
  "details": [ /* optionnel — détails par champ */ ]
}
```

| Champ         | Description                                                                             |
| ------------- | --------------------------------------------------------------------------------------- |
| `error`       | Toujours `true` en cas d'erreur, `false` en cas de succès                               |
| `http_status` | Écho du code HTTP de la réponse                                                         |
| `code`        | Identifiant machine-readable stable de l'erreur (à utiliser dans le code)               |
| `message`     | Message human-readable, peut évoluer entre versions                                     |
| `details`     | Optionnel — tableau de violations de validation par champ, ou objet provider-spécifique |

{% hint style="warning" %}
Toujours **dispatcher sur `code`**, jamais sur `message`. Le message peut être traduit ou reformulé sans préavis ; le code est garanti stable dans une version majeure.
{% endhint %}

## Catalogue des codes

### Authentification (401 / 403)

| Code                 | HTTP | Action                                                                            |
| -------------------- | :--: | --------------------------------------------------------------------------------- |
| `missing_api_key`    |  401 | Ajoute le header `X-Api-Key`                                                      |
| `invalid_api_key`    |  401 | Vérifie la clé — peut-être révoquée ou mauvais env                                |
| `signature_invalid`  |  401 | Recalcule la signature — voir [Authentification](/merchant-api-v1/authentication) |
| `merchant_inactive`  |  403 | Le compte n'est pas validé — contacte le support                                  |
| `ip_allowlist_empty` |  403 | Configure au moins une IP autorisée dans le dashboard                             |
| `ip_not_allowed`     |  403 | L'IP appelante n'est pas dans la whitelist live                                   |

### Idempotency (400 / 409)

| Code                      | HTTP | Action                                                                  |
| ------------------------- | :--: | ----------------------------------------------------------------------- |
| `missing_idempotency_key` |  400 | Ajoute le header `Idempotency-Key`                                      |
| `invalid_idempotency_key` |  400 | Reformule la clé (8-128 chars `[A-Za-z0-9_-]`)                          |
| `idempotency_conflict`    |  409 | Tu réutilises une clé avec un body différent — utilise une nouvelle clé |

### Validation (400)

| Code               | HTTP | Action                                              |
| ------------------ | :--: | --------------------------------------------------- |
| `validation_error` |  400 | Voir `details[]` pour la liste des champs invalides |
| `invalid_body`     |  400 | Le body n'est pas du JSON valide                    |
| `unknown_currency` |  400 | La devise demandée n'est pas supportée              |
| `amount_too_small` |  400 | Sous le minimum autorisé pour la devise             |
| `amount_too_large` |  400 | Au-dessus du maximum autorisé pour la devise        |

### Ressources (404 / 409)

| Code                    | HTTP | Action                                                                              |
| ----------------------- | :--: | ----------------------------------------------------------------------------------- |
| `transaction_not_found` |  404 | Le `transaction_reference` n'existe pas ou n'appartient pas à ce marchand           |
| `operation_not_found`   |  404 | L'`operation_id` n'existe pas ou n'appartient pas à ce marchand                     |
| `not_refundable`        |  400 | La transaction n'est pas dans un statut remboursable (déjà refunded, échouée, etc.) |

### Métier (réponses 200 avec opération `failed`)

Quand le worker traite une opération asynchrone et qu'elle échoue, la réponse `GET /operations/:id` contient un champ `error` avec un de ces codes :

| Code                         | Quand                                                           |
| ---------------------------- | --------------------------------------------------------------- |
| `insufficient_balance`       | Solde marchand insuffisant pour le payout                       |
| `merchant_wallet_missing`    | Aucun wallet provisionné pour ce marchand (cas rare)            |
| `provider_rejected`          | Le provider MoMo / banque a refusé l'opération                  |
| `provider_unavailable`       | Aucun handler pour le couple country/operator demandé           |
| `provider_error`             | Erreur réseau ou exception inattendue du provider               |
| `missing_provider_code`      | `destination.provider_code` absent pour un payout mobile\_money |
| `amount_exceeds_transaction` | Tentative de refund supérieure au montant original              |

### Infrastructure (5xx)

| Code                | HTTP | Action                                                           |
| ------------------- | :--: | ---------------------------------------------------------------- |
| `internal_error`    |  500 | Réessaie avec la **même** Idempotency-Key après un délai         |
| `auth_error`        |  500 | Erreur côté serveur sur la vérif de la clé — contacte le support |
| `idempotency_error` |  500 | Redis indisponible — réessaie après quelques secondes            |

## Différencier erreur synchrone vs erreur asynchrone

```javascript
const { data } = await signedPost('/payouts', payload, idem);

// Synchrone — la requête elle-même a-t-elle été acceptée ?
if (data.error) {
  console.error('Rejet immédiat:', data.code, data.message);
  return;
}

// Acceptée — surveiller l'opération via webhook ou polling
const opId = data.data.operation_id;

// Polling alternatif (déconseillé — préfère les webhooks)
const op = await signedGet(`/operations/${opId}`);
if (op.data.status === 'failed') {
  console.error('Échec métier:', op.data.error.code, op.data.error.message);
}
```

## Statuts d'opération

| `status`     | Sens                                                   |
| ------------ | ------------------------------------------------------ |
| `queued`     | En file d'attente, pas encore traitée                  |
| `processing` | En cours de traitement par un worker                   |
| `succeeded`  | Terminée avec succès — voir `result`                   |
| `failed`     | Échouée — voir `error.code` et `error.message`         |
| `expired`    | Pas encore utilisé (réservé pour les opérations à TTL) |


# Recevoir les webhooks signés

Recevoir, vérifier et traiter les webhooks signés émis par Intram pour les opérations asynchrones.

Quand une opération asynchrone du Merchant API change d'état (payout terminé, paiement reçu, remboursement settled…), Intram fait un `POST` HTTP signé vers l'URL que vous avez configurée. C'est le moyen recommandé de suivre l'état des opérations — bien plus efficace que du polling.

## Configurer un endpoint webhook

Via l'API (recommandé pour automatiser) :

```bash
curl -X POST https://api.intram.org/v1/webhooks \
  -H "X-Api-Key: $PUB" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" \
  -H "Idempotency-Key: webhook-setup-1" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://my-app.example.com/intram/webhook",
    "event": "*"
  }'
```

Le champ `event` accepte :

| Pattern            | Comportement                                                         |
| ------------------ | -------------------------------------------------------------------- |
| `payout.completed` | Match exact d'un seul event                                          |
| `payout.*`         | Tous les events `payout.completed`, `payout.failed`, `payout.queued` |
| `*` ou `all`       | Tous les events                                                      |

{% hint style="warning" %}
La réponse de création contient `secret` — c'est ta **clé HMAC pour vérifier les webhooks entrants**. Elle n'est **affichée qu'une seule fois**, stocke-la dans un coffre-fort (Vault, AWS Secrets Manager, etc.). Si tu la perds, recrée le webhook.
{% endhint %}

## Headers d'un webhook entrant

Chaque livraison porte :

```http
Content-Type:        application/json
X-Intram-Signature:  sha256=<hex>
X-Intram-Timestamp:  2026-05-20T10:31:14.000Z
X-Intram-Event:      payout.completed
X-Intram-Delivery:   whd_550e8400e29b41d4a716446655440000
X-Intram-Attempt:    1
User-Agent:          Intram-Webhooks/1.0
```

* `X-Intram-Signature` — HMAC-SHA256 de `timestamp.raw_body` avec le secret du webhook
* `X-Intram-Timestamp` — ISO 8601. Rejette si > 5 minutes
* `X-Intram-Delivery` — identifiant unique de la tentative. Utilise-le pour dédupliquer côté toi en cas de double livraison
* `X-Intram-Attempt` — numéro de tentative (1 à 5)

## Body d'un webhook

```json
{
  "event": "payout.completed",
  "operation_id": "op_2f4a…",
  "occurred_at": "2026-05-20T10:31:14.000Z",
  "data": {
    "reference": "PO-2026-0001",
    "amount": 25000,
    "currency": "XOF",
    "status": "completed",
    "destination": { "type": "mobile_money", "msisdn": "22961234567" }
  }
}
```

## Vérifier la signature

{% code title="webhook-verifier.js (Node.js / Express)" %}

```javascript
const crypto = require('crypto');
const express = require('express');

const WEBHOOK_SECRET = process.env.INTRAM_WEBHOOK_SECRET;

const app = express();

// IMPORTANT : récupérer le RAW body, pas le JSON parsé
app.post('/intram/webhook',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const signature = req.headers['x-intram-signature'];
    const timestamp = req.headers['x-intram-timestamp'];
    const delivery  = req.headers['x-intram-delivery'];

    // 1. Anti-replay : refuse si > 5 minutes
    if (Math.abs(Date.now() - Date.parse(timestamp)) > 5 * 60_000) {
      return res.status(401).send('Timestamp out of window');
    }

    // 2. Recalcule la signature attendue
    const expected = 'sha256=' + crypto
      .createHmac('sha256', WEBHOOK_SECRET)
      .update(`${timestamp}.${req.body}`)
      .digest('hex');

    // 3. Comparaison timing-safe
    const a = Buffer.from(signature || '');
    const b = Buffer.from(expected);
    if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
      return res.status(401).send('Invalid signature');
    }

    // 4. Dédup par X-Intram-Delivery (au cas où un retry passe en double)
    if (alreadySeen(delivery)) return res.status(200).send('ok');
    markSeen(delivery);

    // 5. Parse et traite
    const event = JSON.parse(req.body.toString('utf8'));
    handleEvent(event);

    // 6. Toujours répondre 2xx pour acquitter
    res.status(200).send('ok');
  }
);
```

{% endcode %}

{% hint style="danger" %}
**Si tu utilises `express.json()` globalement**, ton middleware aura déjà mangé le raw body — la signature ne matchera jamais. Il faut soit appliquer `express.raw()` spécifiquement sur la route webhook, soit conserver le raw body via une option de bodyParser.
{% endhint %}

## Politique de retry

|   Tentative  | Délai après la précédente              |
| :----------: | -------------------------------------- |
|       1      | immédiat                               |
|       2      | 1 min                                  |
|       3      | 5 min                                  |
|       4      | 30 min                                 |
|       5      | 2 h                                    |
|    (puis)    | 12 h                                   |
| Au-delà de 5 | livraison marquée `exhausted`, abandon |

* Une réponse **2xx** acquitte la livraison — pas de retry.
* Toute autre réponse (4xx, 5xx, timeout > 15 s, connexion refusée) déclenche un retry suivant le calendrier ci-dessus.
* Les retries portent un `X-Intram-Attempt` incrémenté mais le **même** `X-Intram-Delivery` — c'est ce qui te permet de dédupliquer.

## Catalogue des events

| Event                     | Émis quand                                                                |
| ------------------------- | ------------------------------------------------------------------------- |
| `payout.completed`        | Le payout mobile money a été confirmé par le provider                     |
| `payout.queued`           | Le payout bancaire a été accepté pour traitement back-office              |
| `payout.failed`           | Le payout a été rejeté (solde, provider, validation)                      |
| `payment_request.created` | La demande de paiement est prête, `gateway_url` disponible                |
| `payment_request.paid`    | Le client a payé avec succès                                              |
| `payment_request.pending` | Le client a initié mais le provider n'a pas confirmé                      |
| `payment_request.failed`  | Le flow paiement a échoué côté client ou provider                         |
| `refund.completed`        | Le refund a été settled (mobile money succeeded ou Stripe refund created) |
| `refund.pending`          | Le refund est accepté par le provider mais pas encore settled             |
| `refund.failed`           | Le refund a été rejeté                                                    |
| `test.ping`               | Envoi de test via `POST /webhooks/:subscription_id/test`                  |

## Bonnes pratiques

1. **Réponds rapidement** (sous 5 secondes). Si ton handler doit faire du travail lourd, queue-le puis renvoie 200 tout de suite.
2. **Idempotence** côté toi : un event peut arriver plusieurs fois (rare mais possible). Dédup sur `X-Intram-Delivery`.
3. **Endpoint HTTPS uniquement** — la création d'un webhook refuse les URLs `http://`.
4. **Logge les `X-Intram-Attempt > 1`** — un retry signifie que ton premier renvoi a échoué, c'est un signal de monitoring.
5. **Endpoint de test** : utilise `POST /webhooks/:id/test` pour vérifier que ton handler fonctionne sans déclencher une vraie opération.


# Solde du wallet

Lire le solde du wallet marchand par moyen de paiement.

Renvoie le solde du wallet, ventilé par moyen de paiement, pour l'environnement de la clé présentée (sandbox vs live).

## Requête

```http
GET /v1/balance
X-Api-Key:    pk_live_…
X-Timestamp:  2026-05-20T10:30:00.000Z
X-Signature:  sha256=…
```

Aucun body. Aucun paramètre de query.

## Réponse `200 OK`

```json
{
  "error": false,
  "http_status": 200,
  "data": {
    "currency": "XOF",
    "payment_method_balances": [
      {
        "payment_method": "MTN Bénin",
        "available": 800000,
        "pending": 0
      },
      {
        "payment_method": "Moov Bénin",
        "available": 440000,
        "pending": 35000
      }
    ]
  }
}
```

## Champs

| Champ                                      | Type           | Description                                                           |
| ------------------------------------------ | -------------- | --------------------------------------------------------------------- |
| `http_status`                              | number         | Écho du code HTTP de la réponse                                       |
| `currency`                                 | string \| null | Code alphabétique de la devise principale du wallet (`XOF`, `EUR`, …) |
| `payment_method_balances[]`                | array          | Décomposition par moyen de paiement                                   |
| `payment_method_balances[].payment_method` | string         | Nom du moyen de paiement (`MTN Bénin`, `Moov Bénin`, `Carte Visa`, …) |
| `payment_method_balances[].available`      | number         | Montant disponible pour ce moyen de paiement                          |
| `payment_method_balances[].pending`        | number         | Montant en attente de settlement pour ce moyen de paiement            |

## Cas limites

* **Aucun wallet provisionné** : la réponse est `200` avec `currency: null` et `payment_method_balances: []`.
* **Erreur interne** : `500` avec `code: "internal_error"`.

## Synchrone — pas d'opération créée

Contrairement aux mutations, cet endpoint n'ajoute pas de ligne dans la file des opérations et n'exige pas d'`Idempotency-Key`.

## Voir aussi

* [Statut d'une opération](/merchant-api-v1/operations) — pour suivre les payouts / refunds en cours
* [Sandbox vs Live](/merchant-api-v1/sandbox-vs-live) — le solde renvoyé dépend de la clé utilisée


# Payouts (reversements)

Reverser des fonds depuis le wallet marchand vers un compte Mobile Money ou un compte bancaire.

Initie un payout (reversement). Renvoie immédiatement `202` avec un `operation_id` ; le résultat final arrive par [webhook](/merchant-api-v1/webhooks) ou peut être pollé via `GET /operations/:id`.

## Comportement

| Destination    | Flow                                                                                                                                        |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `mobile_money` | Le wallet est débité, le provider est appelé. Si succès → webhook `payout.completed`. Si rejet → wallet recrédité, webhook `payout.failed`. |
| `bank_account` | Le wallet est débité, un Reversement est créé en `queued_for_settlement`, traité ensuite par le back-office. Webhook `payout.queued`.       |

## Requête

```http
POST /v1/payouts
X-Api-Key: …
X-Timestamp: …
X-Signature: …
Idempotency-Key: po-2026-0001-attempt-1
Content-Type: application/json
```

### Body — Mobile Money

```json
{
  "amount": 25000,
  "currency": "XOF",
  "destination": {
    "type": "mobile_money",
    "country_code": "BJ",
    "provider_code": "MTN_BENIN_229",
    "msisdn": "22961234567",
    "account_name": "Ada",
    "account_surname": "Lovelace"
  },
  "reference": "PO-2026-0001",
  "note": "Settlement hebdomadaire",
  "metadata": { "batch_id": "B-42" }
}
```

### Body — Bank wire

```json
{
  "amount": 500000,
  "currency": "XOF",
  "destination": {
    "type": "bank_account",
    "swift": "ECOCBJBJ",
    "account_number": "BJ06...",
    "account_name": "Ada Lovelace"
  },
  "reference": "PO-2026-WIRE-001"
}
```

## Validation des champs

| Champ                         |  Obligatoire  | Règles                                                          |
| ----------------------------- | :-----------: | --------------------------------------------------------------- |
| `amount`                      |       ✓       | Number > 0                                                      |
| `currency`                    |   optionnel   | Code ISO 3 lettres (`XOF`, `EUR`…)                              |
| `destination.type`            |       ✓       | `mobile_money` ou `bank_account`                                |
| `destination.country_code`    | mobile\_money | 2 lettres ISO, défaut `BJ`                                      |
| `destination.provider_code`   | mobile\_money | Ex : `MTN_BENIN_229`, `MOOV_AFRICA_BENIN_229`, `SBIN_BENIN_229` |
| `destination.msisdn`          | mobile\_money | Numéro international sans `+`                                   |
| `destination.account_number`  | bank\_account | Format bancaire local ou IBAN                                   |
| `destination.swift`           | bank\_account | Code SWIFT/BIC                                                  |
| `destination.account_name`    |   optionnel   | Prénom du bénéficiaire                                          |
| `destination.account_surname` |   optionnel   | Nom du bénéficiaire                                             |
| `reference`                   |   optionnel   | Référence interne, max 100 chars                                |
| `note`                        |   optionnel   | Texte libre, max 500 chars                                      |
| `metadata`                    |   optionnel   | Object libre transmis dans le webhook                           |

## Réponse `202 Accepted`

```json
{
  "error": false,
  "http_status": 202,
  "data": {
    "operation_id": "op_2f4a8b1c…",
    "type": "payout",
    "status": "queued",
    "env": "live",
    "payload": { /* le body que vous avez envoyé */ },
    "date": "2026-05-20T10:30:00.000Z",
    "_links": { "self": "/api/v1/merchant/operations/op_2f4a8b1c…" }
  }
}
```

## Résultat final (via webhook ou polling)

Quand l'opération est terminée, `op.result` ressemble à :

```json
{
  "reference": "PO-2026-0001",
  "amount": 25000,
  "currency": "XOF",
  "destination": { "type": "mobile_money", "msisdn": "22961234567" },
  "status": "completed",
  "provider_response": {
    "responsecode": "00",
    "responsemsg": "Success",
    "serviceref": "MTN987654321"
  }
}
```

## Événements webhook émis

| Événement          | Quand                                                  |
| ------------------ | ------------------------------------------------------ |
| `payout.completed` | Mobile money settled avec succès                       |
| `payout.queued`    | Bank wire accepté pour traitement back-office          |
| `payout.failed`    | Provider a rejeté, solde insuffisant, ou erreur réseau |

## Erreurs

| Code                    |     HTTP     | Cas                                                   |
| ----------------------- | :----------: | ----------------------------------------------------- |
| `validation_error`      |      400     | `amount`, `destination.type`, etc. invalides          |
| `insufficient_balance`  | 200 (worker) | Solde marchand < montant demandé                      |
| `provider_rejected`     | 200 (worker) | Le provider MoMo a refusé — wallet recrédité          |
| `provider_unavailable`  | 200 (worker) | Aucun handler pour ce `country_code`/`provider_code`  |
| `missing_provider_code` | 200 (worker) | `destination.provider_code` absent pour mobile\_money |

## Voir aussi

* [Statut d'une opération](/merchant-api-v1/operations)
* [Webhooks](/merchant-api-v1/webhooks) pour la livraison push du résultat
* [Idempotency](/merchant-api-v1/idempotency) — comment retenter sûrement un payout


# Demandes de paiement

Demander un paiement à un client. Renvoie une URL de checkout (gateway\_url) à partager au client.

Crée une demande de paiement adressée à un client. Le worker initialise une Transaction + une Invoice, puis renvoie l'URL du gateway de paiement Intram à présenter au client.

## Flow complet

```
1. POST /payment-requests         → 202 + operation_id
2. Worker crée Transaction PENDING
3. Webhook payment_request.created → result.gateway_url disponible
4. Tu redirigeas le client vers gateway_url (ou tu lui envoies par email/SMS)
5. Le client paie via le gateway
6. Webhook payment_request.paid|pending|failed selon le résultat
```

## Requête

```http
POST /v1/payment-requests
X-Api-Key: …
X-Timestamp: …
X-Signature: …
Idempotency-Key: pr-order-4521-attempt-1
Content-Type: application/json
```

### Body

```json
{
  "invoice": {
    "amount": 12000,
    "currency": "XOF",
    "description": "Commande #4521",
    "customer": {
      "email": "client@example.com",
      "first_name": "Ada",
      "last_name": "Lovelace",
      "phone": "22961234567"
    }
  },
  "payment_method": {
    "code": "MTN_BENIN_229",
    "country_code": "BJ",
    "msisdn": "22961234567"
  },
  "return_urls": {
    "success": "https://shop.example.com/ok",
    "cancel":  "https://shop.example.com/no"
  },
  "webhook_data": { "order_id": "4521" }
}
```

### Validation

| Champ                    | Obligatoire | Règles                                            |
| ------------------------ | :---------: | ------------------------------------------------- |
| `invoice.amount`         |      ✓      | Number > 0, dans les limites min/max de la devise |
| `invoice.currency`       |      ✓      | Code ISO 3 lettres                                |
| `invoice.description`    |  optionnel  | Max 500 chars                                     |
| `invoice.customer`       |      ✓      | Object                                            |
| `invoice.customer.email` |  optionnel  | Email valide                                      |
| `invoice.customer.phone` |  optionnel  | String                                            |
| `payment_method`         |  optionnel  | Suggère un moyen de paiement par défaut au client |
| `return_urls.success`    |  optionnel  | URL où rediriger après succès                     |
| `return_urls.cancel`     |  optionnel  | URL où rediriger après annulation                 |
| `webhook_data`           |  optionnel  | Object libre transmis dans les webhooks           |

## Réponse `202 Accepted`

```json
{
  "error": false,
  "http_status": 202,
  "data": {
    "operation_id": "op_4d2a…",
    "type": "payment_request",
    "status": "queued",
    "env": "live",
    "date": "2026-05-20T10:30:00.000Z"
  }
}
```

## Résultat final (op.result après `payment_request.created`)

```json
{
  "transaction_reference": "AB12CD34EF",
  "status": "pending",
  "amount": 12000,
  "currency": "XOF",
  "gateway_url":  "https://gateway.intram.org/AB12CD34EF",
  "payment_url":  "https://gateway.intram.org/AB12CD34EF",
  "receipt_url":  "https://gateway.intram.org/AB12CD34EF",
  "qr_code": "data:image/png;base64,iVBOR…"
}
```

| Champ         | Description                                       |
| ------------- | ------------------------------------------------- |
| `gateway_url` | URL canonique du checkout — à présenter au client |
| `payment_url` | Alias de `gateway_url` (compat)                   |
| `receipt_url` | URL de la facture imprimable une fois settled     |
| `qr_code`     | Data-URL PNG du QR pointant vers `gateway_url`    |

## Événements webhook

| Événement                 | Quand                                                       |
| ------------------------- | ----------------------------------------------------------- |
| `payment_request.created` | Worker a créé la transaction → `gateway_url` disponible     |
| `payment_request.paid`    | Le client a payé avec succès                                |
| `payment_request.pending` | Le client a initié mais le provider n'a pas encore confirmé |
| `payment_request.failed`  | Échec côté client / provider / timeout                      |

## Cas d'usage typiques

### E-commerce — checkout immédiat

```javascript
// 1. Ton client clique "Payer"
const { data: op } = await intram.paymentRequests.create({...});

// 2. Tu attends le webhook payment_request.created (ou polles ~1s)
const final = await waitForOperation(op.data.operation_id);

// 3. Tu rediriges
res.redirect(final.result.gateway_url);
```

### Facture par email

```javascript
const final = await waitForOperation(op.data.operation_id);
await sendEmail(customerEmail, {
  subject: 'Votre facture',
  body: `Payer ici : ${final.result.gateway_url}`,
});
```

### QR à scanner en boutique

```javascript
const final = await waitForOperation(op.data.operation_id);
display(final.result.qr_code);   // affichage sur écran caisse / impression
```

## Erreurs

| Code                                    | Cas                               |
| --------------------------------------- | --------------------------------- |
| `validation_error`                      | Body invalide                     |
| `unknown_currency`                      | Devise non supportée              |
| `amount_too_small` / `amount_too_large` | Hors des limites min/max          |
| `missing_invoice`                       | Le body n'a pas de bloc `invoice` |

## Voir aussi

* [Statut transaction](/merchant-api-v1/transactions) — lire le statut d'une transaction créée par cet endpoint
* [Webhooks](/merchant-api-v1/webhooks) — recevoir le résultat final en push


# Refunds

Rembourser tout ou partie d'une transaction.

Lance un refund sur une transaction existante du marchand. Le worker route automatiquement vers le provider d'origine (MTN/MOOV/SBIN pour mobile money, Stripe pour bank/card).

## Comportement

| Type d'origine       | Action                                                                                                                  |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| Mobile money         | Appel `requestRefund` du provider correspondant. Si succès → débit du wallet marchand + transaction marquée `REFUNDED`. |
| Bank / Card (Stripe) | Si transaction `PENDING` → `paymentIntents.cancel`. Si `SUCCESS` → `refunds.create`.                                    |

## Requête

```http
POST /v1/refunds
X-Api-Key: …
X-Timestamp: …
X-Signature: …
Idempotency-Key: refund-AB12CD34EF-full
Content-Type: application/json
```

### Body

```json
{
  "transaction_reference": "AB12CD34EF",
  "amount": 12000,
  "reason": "Demande client",
  "metadata": { "ticket_id": "T-9001" }
}
```

| Champ                   | Obligatoire | Règles                                                                                            |
| ----------------------- | :---------: | ------------------------------------------------------------------------------------------------- |
| `transaction_reference` |      ✓      | Référence publique de la transaction                                                              |
| `amount`                |  optionnel  | Défaut = montant total. Doit être ≤ montant original. Refund partiel selon le support du provider |
| `reason`                |  optionnel  | Max 500 chars                                                                                     |
| `metadata`              |  optionnel  | Object libre                                                                                      |

## Pré-validation synchrone

Avant d'enregistrer l'opération, l'endpoint vérifie que la transaction existe et appartient au marchand. Si non : **404 immédiat**, pas d'opération créée.

```json
HTTP 404
{
  "error": true,
  "http_status": 404,
  "code": "transaction_not_found",
  "message": "Transaction not found for this merchant"
}
```

## Réponse `202 Accepted`

```json
{
  "error": false,
  "http_status": 202,
  "data": {
    "operation_id": "op_6f7c…",
    "type": "refund",
    "status": "queued",
    "env": "live",
    "date": "2026-05-20T10:30:00.000Z"
  }
}
```

## Résultat final

```json
{
  "status": "completed",
  "refund_reference": "9DEFGH1234",
  "amount": 12000,
  "transaction_reference": "AB12CD34EF",
  "provider_response": { "responsecode": "00", "responsemsg": "Success" }
}
```

`status` ∈ `completed` (settled), `pending` (mobile money attend la confirmation), ou implicite `failed` via le champ `error` au niveau opération.

## Événements webhook

| Événement          | Quand                                            |
| ------------------ | ------------------------------------------------ |
| `refund.completed` | Settled (provider success ou Stripe refund créé) |
| `refund.pending`   | Mobile money en attente de confirmation provider |
| `refund.failed`    | Rejeté, non-refundable, ou erreur réseau         |

## Effets de bord

* La transaction d'origine passe en `_status: REFUNDED` (et `_refunded_at` est rempli).
* Le wallet marchand est débité du montant du refund (en plus des frais éventuels du provider).
* Le refund apparaît dans `GET /transactions/:reference` sous le champ `refunds[]`.

## Erreurs

| Code                         | Cas                                                            |
| ---------------------------- | -------------------------------------------------------------- |
| `validation_error`           | Body invalide                                                  |
| `transaction_not_found`      | Référence inconnue ou pas à ce marchand (404 sync)             |
| `transaction_not_settled`    | La transaction n'a pas encore de moyen de paiement assigné     |
| `not_refundable`             | Statut hors `SUCCESS`/`PENDING` (déjà refunded, échouée, etc.) |
| `amount_exceeds_transaction` | `amount` > montant original                                    |
| `insufficient_balance`       | Wallet marchand insuffisant pour couvrir le refund             |
| `stripe_unavailable`         | SDK Stripe non configuré côté serveur (cas bank)               |
| `unsupported_payment_method` | Le moyen de paiement n'est ni Mobile ni Banque                 |
| `provider_rejected`          | Provider a refusé le refund                                    |

## Voir aussi

* [Statut transaction](/merchant-api-v1/transactions) pour voir l'historique des refunds appliqués


# Statut transactions

Lire le statut d'une transaction du marchand.

## `GET /transactions/:reference`

Renvoie les détails d'une transaction du marchand authentifié.

### Requête

```http
GET /v1/transactions/AB12CD34EF
X-Api-Key: …
X-Timestamp: …
X-Signature: …
```

`:reference` accepte :

* Le `_reference` public (10 chars alphanumériques)
* L'identifiant interne Mongo (24 hex)

### Réponse `200 OK`

```json
{
  "error": false,
  "http_status": 200,
  "data": {
    "reference": "AB12CD34EF",
    "status": "SUCCESS",
    "type": "CREDIT",
    "amount": 12000,
    "fees": 240,
    "net_amount": 11760,
    "currency": "XOF",
    "payment_method": "MTN Bénin",
    "customer": {
      "email": "client@example.com",
      "name": "Ada Lovelace"
    },
    "source": {
      "number": "22961234567"
    },
    "date": "2026-05-20T10:30:00.000Z",
    "refunds": [
      {
        "reference": "9DEFGH1234",
        "status": "SUCCESS",
        "reason": "Demande client",
        "date": "2026-05-20T11:45:00.000Z"
      }
    ]
  }
}
```

### Champs

| Champ                 | Type              | Description                                                           |
| --------------------- | ----------------- | --------------------------------------------------------------------- |
| `http_status`         | number            | Écho du code HTTP de la réponse                                       |
| `reference`           | string            | Référence publique de la transaction                                  |
| `status`              | string            | Statut courant (voir tableau ci-dessous)                              |
| `type`                | string            | `CREDIT`, `DEBIT` ou `REFUND`                                         |
| `amount`              | number            | Montant brut                                                          |
| `fees`                | number            | Frais appliqués                                                       |
| `net_amount`          | number            | Montant net après frais                                               |
| `currency`            | string \| null    | Code alphabétique de la devise (`XOF`, `EUR`, …)                      |
| `payment_method`      | string \| null    | Nom du moyen de paiement (`MTN Bénin`, `Moov Bénin`, `Carte Visa`, …) |
| `customer`            | object            | Coordonnées du client (`email`, `name`) — objet vide si non renseigné |
| `source.number`       | string \| null    | Numéro source de l'opération (mobile money), s'il est connu           |
| `date`                | string (ISO 8601) | Date de création de la transaction                                    |
| `refunds[]`           | array             | Liste des remboursements liés (vide si aucun)                         |
| `refunds[].reference` | string            | Référence publique du remboursement                                   |
| `refunds[].status`    | string            | Statut du remboursement                                               |
| `refunds[].reason`    | string \| null    | Motif du remboursement                                                |
| `refunds[].date`      | string (ISO 8601) | Date du remboursement                                                 |

### Statuts possibles

| `status`   | Sens                                            |
| ---------- | ----------------------------------------------- |
| `PENDING`  | Créée, en attente du paiement client / provider |
| `SUCCESS`  | Settled                                         |
| `ERROR`    | Échouée                                         |
| `REFUNDED` | Remboursée (totalement)                         |

### Erreurs

| Code                    | HTTP | Cas                                     |
| ----------------------- | :--: | --------------------------------------- |
| `transaction_not_found` |  404 | Référence inconnue ou pas à ce marchand |

***

## `GET /transactions`

Liste paginée des transactions du marchand, triée par création décroissante.

### Query params

| Param    | Type     | Description                                                       |
| -------- | -------- | ----------------------------------------------------------------- |
| `limit`  | int      | 1-100, défaut 20                                                  |
| `status` | string   | Filtre exact : `PENDING`, `SUCCESS`, `ERROR`, `REFUNDED`          |
| `type`   | string   | Filtre exact : `CREDIT`, `DEBIT`, `REFUND`                        |
| `before` | ISO 8601 | Curseur — renvoie les éléments créés strictement avant cette date |

### Réponse

```json
{
  "error": false,
  "http_status": 200,
  "data": [ /* tableau de transactions, format identique au get unitaire */ ],
  "paging": {
    "limit": 20,
    "next_before": "2026-05-19T12:34:56.000Z"
  }
}
```

`next_before` est `null` quand il n'y a plus de page suivante. Sinon, passez-le tel quel en `before` au prochain appel.

### Pagination type

```javascript
let cursor = null;
const all = [];
do {
  const url = `/transactions?limit=100${cursor ? `&before=${encodeURIComponent(cursor)}` : ''}`;
  const r = await signedGet(url);
  all.push(...r.data);
  cursor = r.paging.next_before;
} while (cursor);
```

## Voir aussi

* [Refunds](/merchant-api-v1/refunds) pour rembourser une transaction
* [Payment requests](/merchant-api-v1/payment-requests) pour créer une nouvelle transaction


# Opérations asynchrones

Suivre l'avancement et le résultat d'une opération asynchrone.

Toutes les mutations asynchrones (`/payouts`, `/payment-requests`, `/refunds`) renvoient un `operation_id` que vous pouvez suivre via ces endpoints.

{% hint style="info" %}
**Préfère les webhooks** au polling. Le polling est fait pour le fallback (en dev local, en debug, ou quand ton endpoint webhook est temporairement indisponible).
{% endhint %}

## `GET /operations/:id`

### Requête

```http
GET /v1/operations/op_2f4a8b1c...
X-Api-Key: …
X-Timestamp: …
X-Signature: …
```

### Réponse `200 OK`

```json
{
  "error": false,
  "http_status": 200,
  "data": {
    "operation_id": "op_2f4a8b1c...",
    "type": "payout",
    "status": "succeeded",
    "env": "live",
    "payload": { /* body envoyé à l'origine */ },
    "result": {
      "reference": "PO-2026-0001",
      "amount": 25000,
      "currency": "XOF",
      "status": "completed"
    },
    "error": null,
    "attempts": 1,
    "date": "2026-05-20T10:30:00.000Z",
    "updated_date": "2026-05-20T10:30:14.000Z",
    "completed_date": "2026-05-20T10:30:14.000Z",
    "_links": { "self": "/api/v1/merchant/operations/op_2f4a8b1c..." }
  }
}
```

### Statuts

| `status`     | Sens                                           |
| ------------ | ---------------------------------------------- |
| `queued`     | En file d'attente, pas encore traitée          |
| `processing` | Worker actif                                   |
| `succeeded`  | Terminée — voir `result`                       |
| `failed`     | Échouée — voir `error.code` et `error.message` |
| `expired`    | (réservé pour TTL)                             |

### Quand `status === "failed"`

```json
{
  "status": "failed",
  "result": null,
  "error": {
    "code": "insufficient_balance",
    "message": "Insufficient merchant balance",
    "details": null
  }
}
```

Les codes d'erreur métier sont listés dans le [catalogue d'erreurs](https://docs.intram.org/merchant-api-v1/pages/Q2kc6u9rwRUelazEXKyg#métier-réponses-200-avec-opération-failed).

***

## `GET /operations`

Liste paginée des opérations du marchand.

### Query params

| Param    | Description                                              |
| -------- | -------------------------------------------------------- |
| `limit`  | 1-100, défaut 20                                         |
| `type`   | `payout`, `payment_request`, `refund`                    |
| `status` | `queued`, `processing`, `succeeded`, `failed`, `expired` |
| `before` | ISO 8601 — curseur de pagination                         |

### Exemple : tous les payouts échoués des 7 derniers jours

```bash
# Le filtrage par date passe par `before` (curseur). Pour une fenêtre,
# itère depuis maintenant et coupe quand date < now - 7j.
GET /v1/operations?type=payout&status=failed&limit=100
```

### Réponse

```json
{
  "error": false,
  "http_status": 200,
  "data": [ /* tableau d'opérations */ ],
  "paging": { "limit": 20, "next_before": "2026-05-19T12:34:56.000Z" }
}
```

## Pattern de polling

Si tu ne peux pas mettre en place de webhook (ex: dev local), poll avec un backoff :

```javascript
async function waitForOperation(opId, { maxMs = 60000, intervalMs = 1000 } = {}) {
  const deadline = Date.now() + maxMs;
  while (Date.now() < deadline) {
    const op = await signedGet(`/operations/${opId}`);
    if (op.data.status === 'succeeded' || op.data.status === 'failed') return op.data;
    await new Promise(r => setTimeout(r, intervalMs));
    intervalMs = Math.min(intervalMs * 1.5, 10000);
  }
  throw new Error(`Timeout waiting for ${opId}`);
}
```

## Voir aussi

* [Webhooks](/merchant-api-v1/webhooks) — l'alternative push, recommandée
* [Catalogue d'erreurs](/merchant-api-v1/errors)


# Souscriptions webhook

Gérer les souscriptions webhook depuis l'API.

Les souscriptions définissent où et pour quels events tu reçois les notifications signées. Voir [Recevoir les webhooks](/merchant-api-v1/webhooks) pour le format des livraisons.

## `GET /webhooks`

Liste les souscriptions du marchand.

```http
GET /v1/webhooks
X-Api-Key: …
X-Timestamp: …
X-Signature: …
```

```json
{
  "error": false,
  "http_status": 200,
  "data": [
    {
      "subscription_id": "whs_b4c3a2…",
      "url": "https://my-app.example.com/intram/webhook",
      "event": "*",
      "date": "2026-05-20T08:00:00.000Z"
    }
  ]
}
```

| Champ             | Type              | Description                                                                                                                      |
| ----------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `http_status`     | number            | Écho du code HTTP de la réponse                                                                                                  |
| `subscription_id` | string            | Identifiant public de la souscription (à passer à `DELETE /webhooks/:subscription_id` ou `POST /webhooks/:subscription_id/test`) |
| `url`             | string            | URL HTTPS qui reçoit les livraisons                                                                                              |
| `event`           | string            | Pattern d'événements écouté (`*`, `payout.*`, `payout.completed`, …)                                                             |
| `date`            | string (ISO 8601) | Date de création de la souscription                                                                                              |

***

## `POST /webhooks`

Crée une nouvelle souscription. **La réponse contient le `secret` complet — affiché une seule fois.**

```http
POST /v1/webhooks
X-Api-Key: …
Idempotency-Key: webhook-create-1
Content-Type: application/json
```

### Body

```json
{
  "url": "https://my-app.example.com/intram/webhook",
  "event": "payout.*"
}
```

| Champ   | Obligatoire | Règles                                                                                      |
| ------- | :---------: | ------------------------------------------------------------------------------------------- |
| `url`   |      ✓      | Doit être en `https://`                                                                     |
| `event` |      ✓      | Pattern : exact (`payout.completed`), prefix wildcard (`payout.*`), catch-all (`*` / `all`) |

### Réponse `201 Created`

```json
{
  "error": false,
  "http_status": 201,
  "data": {
    "subscription_id": "whs_b4c3a2…",
    "url": "https://my-app.example.com/intram/webhook",
    "event": "payout.*",
    "secret": "whsec_b4c3a2…",
    "date": "2026-05-20T10:30:00.000Z"
  },
  "message": "Webhook created. Store the secret now — it will not be shown again."
}
```

{% hint style="danger" %}
Le `secret` n'est retourné qu'à la création. Stocke-le immédiatement dans un coffre-fort. Si tu le perds, supprime la souscription et recrée-la.
{% endhint %}

***

## `DELETE /webhooks/:subscription_id`

Supprime une souscription. Les livraisons en cours ne sont pas annulées (elles iront jusqu'à `exhausted` si elles échouent), mais aucune nouvelle livraison ne sera créée pour cette URL.

```http
DELETE /v1/webhooks/whs_b4c3a2…
X-Api-Key: …
X-Timestamp: …
X-Signature: …
```

```json
{
  "error": false,
  "http_status": 200,
  "message": "Webhook deleted"
}
```

***

## `POST /webhooks/:subscription_id/test`

Enqueue une livraison de test (`event: test.ping`) vers l'URL configurée. Utile pour vérifier que ton handler est joignable et que ta vérification de signature fonctionne sans déclencher de vraie opération.

```http
POST /v1/webhooks/whs_b4c3a2…/test
X-Api-Key: …
Idempotency-Key: webhook-test-1
Content-Type: application/json
```

Body : `{}` (vide).

Réponse :

```json
HTTP 202
{
  "error": false,
  "http_status": 202,
  "message": "Test delivery enqueued"
}
```

Payload livré (chez toi) :

```json
{
  "event": "test.ping",
  "operation_id": "op_test",
  "occurred_at": "2026-05-20T10:30:00.000Z",
  "data": { "test": true, "sent_at": "2026-05-20T10:30:00.000Z" }
}
```

## Patterns d'événements

| Pattern            | Match                                                |
| ------------------ | ---------------------------------------------------- |
| `payout.completed` | Exactement `payout.completed`                        |
| `payout.*`         | `payout.completed`, `payout.failed`, `payout.queued` |
| `*` ou `all`       | Tous les events                                      |

Tu peux créer plusieurs souscriptions sur la même URL avec des patterns différents — chaque souscription est livrée indépendamment.

## Voir aussi

* [Recevoir les webhooks signés](/merchant-api-v1/webhooks)
* [Catalogue des événements](/merchant-api-v1/webhooks#catalogue-des-events)


# Changelog

Historique des changements de l'API Merchant.

## v1.0 — 2026-05-20 (initial)

Première version publique du Merchant API.

### Ajouts

* `GET /balance` — solde wallet sync
* `POST /payouts` — reversements mobile money + bank wire (async)
* `POST /payment-requests` — demandes de paiement avec `gateway_url` (async)
* `POST /refunds` — refunds mobile money + Stripe (async)
* `GET /transactions/:reference` + `GET /transactions` — statut + historique
* `GET /operations/:id` + `GET /operations` — suivi des opérations asynchrones
* `GET/POST/DELETE /webhooks` + `POST /webhooks/:subscription_id/test` — gestion des souscriptions
* Webhooks sortants signés HMAC-SHA256 avec retry exponentiel (1m / 5m / 30m / 2h / 12h)
* Whitelist IP obligatoire en mode live (gérée depuis le dashboard)
* Idempotency-Key obligatoire sur toutes les mutations (TTL 24h)

### Compatibilité

Aucune modification des routes existantes `/api/v1/payments/*`, `/refund/*`, `/recharges/*`, `/widget/*`, `/webhook/*`. Le Merchant API v1 vit sous `/api/v1/merchant/*` (URL publique `api.intram.org/v1/*` après rewrite Nginx).

## Politique de versioning

* **Patch / additions non-breaking** : nouveaux endpoints, nouveaux champs en réponse, nouveaux events webhook → pas de bump de version, annoncé ici.
* **Breaking changes** : changement de signature, suppression de champ, changement de comportement → nouveau path `/v2/`, l'ancien reste maintenu **au minimum 12 mois** après l'annonce, période pendant laquelle les deux coexistent.
* Les `code` JSON d'erreur sont **stables** à l'intérieur d'une version majeure. Les `message` peuvent évoluer (traductions, reformulations).


# API legacy

Catalogue des SDK et clients pour l'API publique de paiement Intram. Permettent de présenter un checkout client dans votre application.

Cette section regroupe les clients officiels pour intégrer l'**API publique de paiement** — celle qui sert à présenter un checkout à votre client, en l'envoyant sur une page de paiement ou via un widget embarqué.

{% hint style="info" %}
Si votre besoin est de **piloter votre compte marchand depuis votre backend** (lire le solde, déclencher un payout, transférer vers un client, demander un paiement, rembourser…), utilisez plutôt la [**Merchant API v1**](/merchant-api-v1/merchant-api) — elle est asynchrone, signée HMAC, idempotente et mieux outillée pour le backend.
{% endhint %}

## Pré-requis

Avant d'utiliser un de ces clients, vous devez avoir suivi les étapes de la section [**Introduction**](/) :

1. Avoir un compte marchand Intram validé
2. Avoir récupéré vos clés API (`public_key`, `private_key`, `secret_key`) depuis **Développeurs → API**
3. Choisir l'environnement (`sandbox` pour le développement, `live` pour la production)

## Clients disponibles

Choisissez le SDK adapté à votre stack technique :

| Stack                              | Statut | Documentation                                     |
| ---------------------------------- | :----: | ------------------------------------------------- |
| **PHP**                            |    ✅   | [PHP Server SDK](/backend/php-server-sdk)         |
| **WordPress / WooCommerce**        |    ✅   | [WooCommerce Plugin](/backend/woocommerce-plugin) |
| **Django (Python)**                |    ✅   | [Django-Intram](/backend/django-intram)           |
| **Node.js**                        |    ✅   | [Node.js API Client](/backend/nodejs-api-client)  |
| **Flutter (mobile)**               |    ✅   | [Flutter](/mobile/flutter)                        |
| **HTTP / JSON direct**             |    ✅   | [HTTP](/backend/http)                             |
| **JavaScript (widget navigateur)** |    ✅   | [JavaScript](/fontend/javascript)                 |

Tous ces clients consomment le même endpoint public `https://webservices.intram.org:4002/api/v1/`.

## Si votre stack n'est pas listée

Utilisez directement [l'API HTTP/JSON](/backend/http) — tous les SDK ci-dessus en sont des wrappers. Vous pouvez vous appuyer sur n'importe quel client HTTP standard (`requests` Python, `Guzzle` PHP, `axios` Node…) pour faire vos requêtes.

## Voir aussi

* [Frais](/payment/fees) — comprendre le calcul des commissions appliquées
* [Devises supportées](/payment/supported-devices)
* [Merchant API v1](/merchant-api-v1/merchant-api) — l'alternative backend recommandée pour les nouvelles intégrations


# JAVASCRIPT

Le SDK JavaScript est conçu pour intégrer la solution de paiement dans votre application web sans recharger la page.

{% hint style="info" %}
Le SDK Javascript ne permet pas le traitement des données de facturation. Vous devez utilisé un SDK BACKEND pour générer un lien de facturation qui vous servira ensuite après une requête HTTP.
{% endhint %}

Toute utilisation du SDK JavaScript requiert un compte marchand valide et des données dans votre application de gestion Intram. Si vous n'en disposez pas encore, [créez-en un](https://app.intram.org/register) dès maintenant. L'intégration du SDK JavaScript se fait en important le script qui suit avant la fermeture de la balise fermante`</body>`

```markup
<script src="https://cdn.intram.org/sdk-javascript.js"></script>
```

Une fois le SDK importé dans votre application web vous pouvez l'utiliser de cette manière :

```javascript
intramOpenWidget.init({
            public_key:'9974dacefe24494f68332ae23d0', //your public api key
            amount:1000,
            sandbox:false,
            currency:'xof',
            callback_url:'https://my-shop.com/check-paiement-status/',
            company:{ 
                name:'my-shop',
                color:'green',
                logo_url:'https://my-site.com/logo.jpg'
            },
        }).then((data)=>{
            console.log(data,'****** responses')
        })
```

{% hint style="success" %}
EXEMPLE AVEC AJAX
{% endhint %}

```javascript
 $.ajax(
 {
       url: 'url',
       method: "POST",
       data: data,
       success: function (data) 
         {
             intramOpenWidget.init({
            public_key:'9974dacefe24494f68332ae23d01', //your public api key
            amount:data.company.amount, //replace your product price 
            sandbox:false, // choose the right public key : sandbox=test or live (see your intram account)
            callback_url:data.callback_url,
            currency:'xof', //choose the currency
            company:{ 
                name:'data.company.name', //your company namr
                color:data.company.color, // payment gate template color
                logo_url:data.company.logo // your company logo
            },
        }).then((data)=>{
            //console.log(data,'****** responses')
        })
          }
});
```

{% hint style="success" %}
Numero de test

**MTN** : 0162000000

**MOOV** : 0194000000
{% endhint %}


# HTTP

Intégration HTTP directe avec l'ancienne API Intram (mode public). Pour les nouvelles intégrations backend, voir la section MERCHANT API V1.

{% hint style="warning" %}
**Nouvelle intégration ?** Cette page documente l'API historique conçue pour les pages de paiement publiques (widget, gateway). Pour piloter votre compte marchand depuis votre **backend** (lire le solde, faire des payouts, transférer, demander un paiement, rembourser, recevoir des webhooks signés), utilisez la [**Merchant API v1**](/merchant-api-v1/merchant-api) — elle est asynchrone, signée HMAC, idempotente et mieux outillée.
{% endhint %}

### Prérequis

Pour utiliser cette API, vous aurez besoin de :

* Vos clés d'API : **public\_key**, **private\_key**, **secret\_key** (Menu Développeurs → API du dashboard)
* `curl` (ou un client HTTP équivalent) pour les tests en ligne de commande

### Authentification

Toutes les requêtes doivent inclure les trois headers d'authentification suivants :

```
X-API-KEY: votre_public_key
X-PRIVATE-KEY: votre_private_key
X-SECRET-KEY: votre_secret_key
```

Les clés sont validées sur la table `keys` et liées à votre compte marchand. Aucun autre identifiant n'est nécessaire dans les headers (l'identifiant marchand est dérivé automatiquement à partir de la combinaison des 3 clés).

### Endpoints

#### Base URL

* `https://webservices.intram.org:4002/api/v1/`

#### 1. Initier un paiement

* **URL** : `/payments/request`
* **Méthode** : `POST`
* **Exemple de commande** :

```bash
curl -X POST \
  'https://webservices.intram.org:4002/api/v1/payments/request' \
  -H 'X-API-KEY: votre_public_key' \
  -H 'X-PRIVATE-KEY: votre_private_key' \
  -H 'X-SECRET-KEY: votre_secret_key' \
  -H 'Content-Type: application/json' \
  -d '{
    "invoice": {
      "currency": "XOF",
      "items": [],
      "taxes": [],
      "amount": 1000,
      "description": "Paiement test",
      "custom_datas": {}
    },
    "store": {
      "name": "Mon Magasin",
      "postal_adress": "Adresse du magasin",
      "logo_url": "https://monmagasin.com/logo.png",
      "web_site_url": "https://monmagasin.com",
      "phone": "+22500000000",
      "template": "default"
    },
    "actions": {
      "cancel_url": "https://monmagasin.com/cancel",
      "return_url": "https://monmagasin.com/return",
      "callback_url": "https://monmagasin.com/callback"
    }
  }'
```

{% hint style="info" %}
Les clés d'API se passent **uniquement en headers**. Ne les mettez jamais dans le body — c'est du code mort qui sera ignoré et qui expose vos secrets dans les logs.
{% endhint %}

#### 2. Vérifier le statut d'une transaction

* **URL** : `/transactions/confirm/{transaction_id}`
* **Méthode** : `GET`
* **Exemple de commande** :

```bash
curl -X GET \
  'https://webservices.intram.org:4002/api/v1/transactions/confirm/TRANSACTION_ID' \
  -H 'X-API-KEY: votre_public_key' \
  -H 'X-PRIVATE-KEY: votre_private_key' \
  -H 'X-SECRET-KEY: votre_secret_key' \
  -H 'Content-Type: application/json'
```

### Exemples d'utilisation

#### Exemple minimal d'initiation de paiement

```bash
curl -X POST https://webservices.intram.org:4002/api/v1/payments/request \
-H "X-API-KEY: votre_public_key" \
-H "X-PRIVATE-KEY: votre_private_key" \
-H "X-SECRET-KEY: votre_secret_key" \
-H "Content-Type: application/json" \
-d '{
  "invoice": {
    "currency": "XOF",
    "amount": 1000
  },
  "store": {
    "name": "Test Store"
  },
  "actions": {
    "cancel_url": "http://example.com/cancel",
    "return_url": "http://example.com/return",
    "callback_url": "http://example.com/callback"
  }
}'
```

### Gestion des erreurs

L'API retourne des codes HTTP standards :

* `200` : Succès
* `400` : Erreur dans la requête
* `401` : Erreur d'authentification
* `500` : Erreur serveur

En cas d'erreur, un message JSON est retourné avec le détail de l'erreur :

```json
{
  "error": true,
  "status": "ERROR",
  "message": "Invalid API keys"
}
```

### Notes importantes

1. **Sécurité**
   * Ne partagez jamais vos clés d'API — surtout pas dans le body d'une requête, pas dans un repo Git, pas dans le code front-end.
   * Utilisez HTTPS pour toutes les requêtes.
   * Validez toujours les callbacks côté serveur en re-vérifiant l'état de la transaction avec `/transactions/confirm/{id}`.
2. **Environnement de test**
   * Utilisez des clés sandbox (Menu Développeurs → API → mode `SANDBOX`) pour vos tests.
   * Les transactions sandbox ne touchent pas les wallets de production.

### Voir aussi

* [**Merchant API v1**](/merchant-api-v1/merchant-api) — la nouvelle API backend recommandée pour les nouvelles intégrations
* [Devises supportées](/payment/supported-devices)
* [Frais](/payment/fees)


# NODEJS API CLIENT

Bibliothèque Node.js pour INTRAM (intram.org), construite sur l'API HTTP Intram (bêta).

## GENERATE YOUR API KEYS

Les clés API sont vos références numériques vers les systèmes Intram. Nous les utilisons pour identifier votre compte et les applications que vous allez créer. Ces clés sont nécessaires à toute intégration des API de paiement d'Intram. Voici les étapes à suivre:

* Vous devez d'abord avoir un compte Intram Business activé. [Créez-en](https://app.intram.org/register) un si ce n'est pas encore le cas.
* [Connectez-vous ](https://app.intram.org/login)à votre compte et cliquez sur Développeurs au niveau du menu à gauche.
* Cliquez ensuite sur API, vous verrez toutes vos clés API.
* Vous pouvez naviguer du mode `SANDBOX, au mode LIVE.`

## INSTALLATION

### INSTALLATION INTRAM VIA LA COMMANDE NPM

```
npm i @intram-sdk/nodejs
```

## CONFIGURATION BASIC

[Connectez-vous](https://app.intram.org/login) à votre compte puis cliquez sur Développeurs et cliquez sur API au niveau du menu à gauche. Les clés API de votre compte marchand sont directement disponibles selon le mode ('sandbox' ou 'live') choisi. Récupérez les clés API et donnez-les comme arguments aux méthodes suivantes

{% code title="Setup" %}

```javascript
var intram = require('intram');

var setup = new intram.Setup({
  mode: 'sandbox', // optional. use in sandbox mode.
  marchandKey: 'tpk_5e9469e65341de91988b352eba11f9f0c5f671384e1d6bfb09ce30103',
  privateKey: 'tpk_5e9469e65341de91988b352eba11f9f0c5f671384e1d6bfb09ce30103b',
  publicKey: "5e59e0c34bb8737cedf4c0ec92d9ae94007e33e5c30280596456990d9fc2f60",
  secret: 'tsk_243a7b89fd82a2b4e049c0c8ff39c3012ee6ec70bda3288ad2bf6a1270439c'          
});
```

{% endcode %}

{% hint style="info" %}
**INFOS**

Si vous êtes en test, utilisez les clés sandbox, sinon utilisez les clés de production et spécifiez le mode en remplaçant `«sandbox»` par `«live»` dans le code ci-dessus.
{% endhint %}

{% hint style="success" %}
**BEST PRATICE**

Il peut généralement être approprié de placer votre configuration d'API dans des variables d'environnement. Dans ce cas, vous pouvez initialiser `intram.Setup` sans passer les paramètres de configuration. La bibliothèque détectera automatiquement les variables d'environnement et les utilisera. Variables d'environnement détectées automatiquement : **INTRAM\_MARCHAND\_KEY**, **INTRAM\_PRIVATE\_KEY**, **INTRAM\_PUBLIC\_KEY**, **INTRAM\_SECRET**
{% endhint %}

### 1- CONFIGURATION DE VOTRE SERVICE / ACTIVITÉ / INFORMATIONS SUR LA SOCIÉTÉ

Vous pouvez configurer vos informations de service / activité / entreprise comme indiqué ci-dessous. Intram utilise ces paramètres pour configurer les informations qui apparaîtront sur la page de paiement, les factures PDF et les reçus imprimés.

Intram vous propose plusieurs designs pour le portail de paiement. Vous pouvez donc choisir un modèle spécifique et ajouter la référence à votre boutique.

Vous pouvez également définir une couleur pour le portail de paiement. Seule la couleur du code est nécessaire

```javascript
var store = new intram.Store({
  name: 'LandryShop', // only name is required
  tagline: "Votre satisfaction, notre priorité.",
  phoneNumber: '229670000000',
  postalAddress: 'Benin-Cotonou Akpakpa',
  websiteURL: 'Benin - Cotonou - Akpakpa',
  logoURL: 'http://www.landryShop/logo.png',
  color:"#a845f4", //for custom paiement portal color,
  template:"default", // Choose the payment portal template that your customers will see
});
```

### 2 - CONFIGURATION DE LA NOTIFICATION DE PAIEMENT INSTANTANÉ

La NOTIFICATION DE PAIEMENT INSTANTANÉ est l'URL du fichier sur lequel vous souhaitez recevoir les informations de transaction de paiement pour un éventuel traitement en backoffice. Intram utilise cette URL pour vous envoyer instantanément, sur requête `POST`, les informations relatives à l'opération de paiement.

{% hint style="success" %}
Il existe deux façons de configurer l'URL de notification instantanée de paiement: soit en accédant à votre compte Intram au niveau des informations de configuration de votre application, soit directement dans votre code.
{% endhint %}

L'utilisation de la deuxième option vous offre les deux possibilités ci-dessous.

#### **2-1 -** CONFIGURATION GLOBALE DE L'URL DE NOTIFICATION DE PAIEMENT INSTANTANÉ

Cette instruction doit être incluse dans la configuration de votre service / activité.

```javascript
var store = new intram.Store({
    name: 'Store at Sandra',
    callbackURL: 'http://www.my-shop.com/fichier_de_reception_des_données_de_facturation'
});
```

#### **2-2 -** CONFIGURATION DE L'URL DE NOTIFICATION DE PAIEMENT INSTANTANÉ OU D'UNE INSTANCE DE FACTURE

{% hint style="info" %}
Cette configuration écrasera les paramètres de redirection globale s'ils ont déjà été définis
{% endhint %}

```javascript
invoice.callbackURL = 'http://www.my-shop.com/fichier_de_reception_des_données_de_facturation'
```

{% hint style="info" %}
La validation réussie de la transaction de paiement renvoie la structure ci-dessous contenant les informations client, l'URL de sa facture Intram au format PDF ainsi qu'un hachage pour vérifier que les données reçues proviennent de nos serveurs.
{% endhint %}

#### **EXPECTED JSON ANSWER**

```javascript
 {
  +"status": "PENDING"
  +"amount": 1000
  +"currency": "XOF"
  +"account_number":"22996XXXXXX"
  +"source":"MOOV"
  +"fees":50
  +"net_amount":1050
}
    
```

**SI VOUS UTILISEZ EXPRESS**

```bash
npm add body-parser
```

```javascript
var bodyParser = require('body-parser')
  app.use( bodyParser.json() );       // to support JSON-encoded bodies
  app.use(bodyParser.urlencoded({     // to support URL-encoded bodies
    extended: true
}));
```

### 3 - RECUPERATION DU STATUT DE PAIEMENT

```javascript
app.post('/payment-url', function(req, res) {
    var status = req.body.status;
});
```

### 4 - RECOUVREMENT DU MONTANT TOTAL DU PAIEMENT

```javascript
app.post('/payment-url', function(req, res) {
    var amount = req.body.transaction.invoice.amount;
});
```

## APIs

INITIALISATION D'UN PAIEMENT

Ce service Api vous permet de créer une facture et de rediriger votre client vers notre plateforme afin qu'il puisse terminer le processus de paiement. Nous vous recommandons d'utiliser cette API car elle est la plus adaptée dans 99% des cas. Le principal avantage de cette option est que les clients peuvent choisir de payer à partir d'une variété d'options de paiement disponibles sur notre plateforme. De plus, si une nouvelle option est ajoutée ultérieurement, elle apparaîtra directement sur la page de paiement sans que vous ayez à modifier quoi que ce soit dans votre code source.

```javascript
// Do this if you want to redirect your customers to our website so that they can complete the payment process
// It is important to note that the constructor requires respectively as parameters
// an instance of the intram.Setup and intram.Store classes
var invoice = new intram.CheckoutInvoice(setup, store);
```

### 1 - AJOUTER DES INFORMATIONS DE PAIEMENT

#### **1-1 -** AJOUT D'ARTICLES ET DESCRIPTION DE LA FACTURE:

Il est important de savoir que les éléments de facturation sont principalement utilisés à des fins de présentation sur la page de paiement. Intram n'utilisera aucun des montants déclarés pour facturer le client. Pour ce faire, vous devez explicitement utiliser la méthode SetTotalAmount de l'API pour spécifier le montant exact à facturer au client.

```javascript
// Adding items to your bill is very basic.
// The expected parameters are product name, quantity, unit price,
// the total price and an optional description.
invoice.addItem('Croco shoes', 1, 10000, 30000, 'Shoes made of genuine crocodile skin that hunts poverty');
invoice.addItem('Ice Shirt', 1, 5000, 5000);
```

{% hint style="info" %}
Vous pouvez éventuellement définir une description générale de la facture qui sera utilisée dans les cas où vous devez inclure des informations supplémentaires dans votre facture.
{% endhint %}

```javascript
invoice.description = "Optional Description";
```

#### **1-2 -** CONFIGURATION DU MONTANT TOTAL DE LA FACTURE\*\*:\*\*

Intram attend de vous que vous spécifiiez le montant total de la facture du client. Ce sera le montant qui sera facturé à votre client. Nous considérons que vous avez déjà effectué tous les calculs sur votre serveur avant de définir ce montant.

{% hint style="info" %}
Intram n'effectuera pas de calculs sur ses serveurs. Le montant total de la facture définie depuis votre serveur sera celui qu'Intram utilisera pour facturer votre client.
{% endhint %}

```javascript
invoice.totalAmount = 42300;
```

#### **1-3 -** CONFIGURATION DE LA MONNAIE DE LA FACTURE:

Intram attend de vous que vous spécifiiez la devise de la facture du client. Ce sera la devise qui sera attachée à votre client facturé.

{% hint style="success" %}
Actuellement, les devises disponibles sont:

* XOF
  {% endhint %}

```javascript
invoice.currency = 'XOF';
```

### 2 - REDIRECTION VERS LA PAGE DE PAIEMENT INTRAM

Après avoir ajouté des articles à votre facture et configuré le montant total de la facture, vous pouvez rediriger votre client vers la page de paiement en appelant la méthode de création à partir de votre objet `invoice`. Veuillez noter que la méthode `invoice.create()` renvoie un booléen (vrai ou faux) selon que la facture a été créée avec succès ou non. Cela vous permet de mettre une instruction `if - else` et de gérer le résultat comme bon vous semble.

```javascript
// The following code describes how to create a payment invoice at our servers,
// then redirect the customer to the payment page
// and then post his payment receipt if successful.
invoice.create()
  .then(function (){
    console.log(transaction.invoice.status);
    console.log(transaction.invoice.transaction); // Bill Token
    console.log(transaction.invoice.message);
    console.log('https://account.intram.cf/payment/gate/'+invoice.transaction); // Intram invoice payment redirection URL
  })
  .catch(function (e) {
    console.log(e);
  });
```

### MÉTHODES D'API SUPPLÉMENTAIRES

#### 1 - AJOUT DE TAXES (OPTIONNEL)

Vous pouvez ajouter des informations fiscales sur la page de paiement. Ces informations seront ensuite affichées sur la page de paiement, les factures PDF et les reçus imprimés, les reçus électroniques.

```javascript
// The parameters are the name of the tax and the amount of the tax.
invoice.addTax('TVA (18%)', 6300);
invoice.addTax("Livraison", 1000);
```

#### 2 - AJOUT DE DONNÉES SUPPLÉMENTAIRES (FACULTATIF)

Si vous avez besoin d'ajouter des données supplémentaires à vos informations de demande de paiement pour une utilisation future, nous vous offrons la possibilité de sauvegarder des données personnalisées sur nos serveurs et de les récupérer une fois le paiement effectué.

{% hint style="info" %}
Les données personnalisées ne sont affichées nulle part sur la page de paiement, les factures / reçus, les téléchargements et les impressions. Ils sont uniquement récupérés à l'aide de notre action de rappel de `confirm` au niveau de l'API
{% endhint %}

```javascript
// Custom data allows you to add additional data to your billing information
// that can be retrieved later using our callback Confirm action
invoice.addCustomData("category", "contest");
invoice.addCustomData("period", "Christmas 2015");
invoice.addCustomData("numero_gagnant", 5);
invoice.addCustomData("price", "50% discount coupon");
```

#### 3 - RESTRICTION DES MOYENS DE PAIEMENT À AFFICHER (OPTIONNEL)

Par défaut, les modes de paiement activés dans votre configuration d'intégration seront tous affichés sur la page de paiement pour toutes vos factures.

Cependant, si vous souhaitez conserver la liste des modes de paiement à afficher sur la page de paiement d'une facture donnée, nous vous offrons la possibilité de le faire en utilisant la méthode `addChannels`.

{% hint style="success" %}
Actuellement, les moyens de paiement disponibles sont:

`MOOV`, `MTN-BENIN`, `VISA, MASTER-CARD`.
{% endhint %}

```javascript
// Addition of several payment methods at a time
invoice.addChannels(['moov-benin', 'mtn-benin', 'visa'])
```

### CONFIGURER UNE URL REDIRECT APRÈS ANNULATION DE PAIEMENT

#### **1-1 -** CONFIGURER UNE URL REDIRECT APRÈS ANNULATION DE PAIEMENT

Vous pouvez éventuellement définir une URL vers laquelle vos clients seront redirigés après une annulation de commande.

{% hint style="info" %}
Il existe deux façons de configurer l'URL d'annulation de commande: soit globalement au niveau des informations de configuration de votre application, soit par commande.
{% endhint %}

{% hint style="danger" %}
L'utilisation de la deuxième option écrase les paramètres globaux s'ils ont déjà été définis.
{% endhint %}

```javascript
var store = new intram.Store({
name: 'Alexandra',
cancelURL: 'http://alexandra.com/cancel_url'
});
```

#### **1-2 -** CONFIGURATION DE L'URL DE REDIRECTION APRÈS ANNULATION DE PAIEMENT SUR UNE INSTANCE DE FACTURE

{% hint style="danger" %}
Cette configuration écrasera les paramètres de redirection globale s'ils ont déjà été définis.
{% endhint %}

```javascript
invoice.cancelURL = 'http://magasin-le-choco.com/cancel_url';
```

#### **1-3 -** CONFIGURER UNE URL REDIRECT APRÈS LA CONFIRMATION DE PAIEMENT

Intram fait un excellent travail de gestion des téléchargements et de l'impression des reçus des paiements une fois que votre client a payé avec succès sa commande. Cependant, il peut arriver que vous souhaitiez rediriger vos clients vers une autre URL après avoir payé leur commande avec succès. La configuration ci-dessous répond à ce besoin.

{% hint style="info" %}
Intram ajoutera `?token=invoice_token` à votre URL. Nous expliquerons comment utiliser ce jeton dans le chapitre suivant.
{% endhint %}

#### **1-4 -** CONFIGURATION GLOBALE DE L'URL REDIRECT APRÈS LA CONFIRMATION DE PAIEMENT.

Cette instruction doit être incluse dans la configuration de votre service / activité

```javascript
var store = new intram.Store({
  name: 'Alexandra',
  returnURL: 'http://alexandra.com/return_url'
});
```

#### **4-5 -** CONFIGURATION DE L'URL REDIRECT APRÈS CONFIRMER LE PAIEMENT SUR UNE INSTANCE DE FACTURE

{% hint style="danger" %}
Cette configuration écrasera les paramètres de redirection globale s'ils ont déjà été définis.
{% endhint %}

```javascript
invoice.returnURL = 'http://magasin-le-choco.com/return_url';
```

## VÉRIFICATION DU STATUT DE PAIEMENT

Notre API vous permet de vérifier l'état de toutes les transactions de paiement à l'aide du jeton de facture. Vous pouvez donc conserver votre jeton de facture et l'utiliser pour vérifier le statut de paiement de la facture. Le statut d'une facture peut être soit `PENDING (PENDING)` , `CANCELED (CANCELED)` , ou `COMPLETED (COMPLETED)` selon si oui ou non le client a payé la facture.

{% hint style="info" %}
Cependant, cette option convient aux paiements PAR car elle vous permettrait par exemple de connaître l'état de paiement de votre facture même si le client est toujours sur notre page de paiement.
{% endhint %}

```javascript
// Intram will automatically add the invoice token as a QUERYSTRING "token"
// if you have configured a "return_url" or "cancel_url".
var token = 'test_VPGPZNnHOC';

var invoice = new intram.CheckoutInvoice(setup, store);
invoice.confirm(token)
.then(function (){
  // Retrieve payment status
  // The payment status can be either completed, pending, cancelled
  console.log(invoice.status);

  console.log(invoice.responseText);  // Server response

  // The following fields will be available if and
  // only if the payment status is equal to "completed".

  // You can retrieve the name, the email address and the
  // customer's phone number using the following object
  console.log(invoice.customer); // {name: 'Alioune', phone: '773830274', email: 'aliounebadara@gmail.com'}

  // URL of the electronic PDF receipt for download
  console.log(invoice.receiptURL); // 'https://app.intram.org/sandbox-checkout/receipt/pdf/test_VPGPZNnHOC.pdf'
})
.catch(function (e) {
  console.log(e);
});
```

{% hint style="success" %}
Numero de test

**MTN** : 61000000

**MOOV** : 94000000
{% endhint %}


# DJANGO-INTRAM

Django-Intram est un plugin Django qui permet d'intégrer facilement les paiements Intram dans votre application Django. Ce guide vous accompagnera pas à pas dans l'implémentation de la solution.

### Prérequis

* Django 3.2 ou supérieur
* Python 3.7 ou supérieur
* Un compte Intram avec vos clés d'API
* pip installé sur votre système

### Installation

1. Installez le package via pip :

```bash
pip install django-intram
```

2. Ajoutez 'django\_intram' à vos INSTALLED\_APPS dans settings.py :

```python
INSTALLED_APPS = [
    ...
    'django_intram',
]
```

### Configuration

#### 1. Configuration des clés API

Dans votre fichier settings.py, ajoutez vos clés API Intram :

```python
# Configuration Intram
INTRAM_PUBLIC_KEY = 'votre_cle_publique'
INTRAM_PRIVATE_KEY = 'votre_cle_privee'
INTRAM_SECRET = 'votre_cle_secrete'
INTRAM_MARCHAND_ID = 'votre_id_marchand'
INTRAM_SANDBOX = True  # Mettre False en production
```

#### 2. Configuration des URLs

Dans votre urls.py principal :

```python
from django.urls import path, include

urlpatterns = [
    ...
    path('payments/', include('django_intram.urls')),
]
```

### Utilisation

#### 1. Exemple basique d'intégration

Dans votre vue (views.py) :

```python
from django.shortcuts import render
from django.conf import settings
from django_intram import Intram

def process_payment(request):
    # Créez une instance Intram
    intram = Intram(
        public_key=settings.INTRAM_PUBLIC_KEY,
        private_key=settings.INTRAM_PRIVATE_KEY,
        secret=settings.INTRAM_SECRET,
        marchand_id=settings.INTRAM_MARCHAND_ID,
        sandbox=settings.INTRAM_SANDBOX
    )
    
    # Configurez le paiement
    intram.set_currency("XOF")
    intram.set_amount(5000)  # Montant en centimes
    intram.set_description("Achat de produit")
    intram.set_name_store("Ma Boutique")
    
    # URLs de redirection
    intram.set_return_url("https://votre-site.com/success")
    intram.set_cancel_url("https://votre-site.com/cancel")
    intram.set_redirection_url("https://votre-site.com/callback")
    
    # Initialisez le paiement
    response = intram.set_request_payment()
    
    return render(request, 'payment.html', {'payment_url': response['payment_url']})
```

Dans votre template (payment.html) :

```html
<form action="{{ payment_url }}" method="POST">
    <button type="submit">Procéder au paiement</button>
</form>
```

#### 2. Gestion des callbacks

Créez une vue pour gérer les retours de paiement :

```python
from django.views.decorators.csrf import csrf_exempt

@csrf_exempt
def payment_callback(request):
    if request.method == 'POST':
        # Récupérez les données de la transaction
        transaction_data = request.POST
        
        # Vérifiez le statut de la transaction
        intram = Intram(...)
        status = intram.get_transaction_status(transaction_data['transaction_id'])
        
        if status['status'] == 'success':
            # Traitez le paiement réussi
            return JsonResponse({'status': 'success'})
        else:
            # Gérez l'échec du paiement
            return JsonResponse({'status': 'failed'})
```

#### 3. Vérification du statut d'une transaction

```python
def check_payment_status(request, transaction_id):
    intram = Intram(...)
    status = intram.get_transaction_status(transaction_id)
    return JsonResponse(status)
```

### Personnalisation

#### Template de paiement personnalisé

Créez un template dans `templates/django_intram/custom_payment.html` :

```html

<div data-gb-custom-block data-tag="extends" data-0='base.html'></div>

<div data-gb-custom-block data-tag="block">

<div class="payment-container">
    <h2>Confirmation de paiement</h2>
    <div class="payment-details">
        <p>Montant : {{ amount }} XOF</p>
        <p>Description : {{ description }}</p>
    </div>
    <form action="{{ payment_url }}" method="POST">
        <button type="submit" class="payment-button">
            Payer maintenant
        </button>
    </form>
</div>

</div>
```

### Environnements

#### Développement (Sandbox)

```python
INTRAM_SANDBOX = True
```

#### Production

```python
INTRAM_SANDBOX = False
```

### Gestion des erreurs

Exemple de gestion des erreurs :

```python
try:
    response = intram.set_request_payment()
    if response.get('status') == 'ERROR':
        # Gérer l'erreur
        logger.error(f"Erreur Intram: {response.get('message')}")
        return render(request, 'error.html', {'error': response.get('message')})
except Exception as e:
    # Gérer les exceptions
    logger.error(f"Exception: {str(e)}")
    return render(request, 'error.html', {'error': 'Une erreur est survenue'})
```

### Sécurité

* Les clés API doivent être stockées dans les variables d'environnement
* Utilisez HTTPS en production
* Validez toujours les données reçues dans les callbacks
* Vérifiez la signature des transactions

### Bonnes pratiques

1. Utilisez les logs pour suivre les transactions :

```python
import logging
logger = logging.getLogger(__name__)

logger.info(f"Transaction initiée: {transaction_id}")
```

2. Créez des modèles pour suivre les transactions :

```python
from django.db import models

class Transaction(models.Model):
    transaction_id = models.CharField(max_length=100)
    amount = models.DecimalField(max_digits=10, decimal_places=2)
    status = models.CharField(max_length=20)
    created_at = models.DateTimeField(auto_now_add=True)
```

### Dépannage

#### Problèmes courants

1. Erreur "KEY MISSING"
   * Vérifiez que toutes vos clés API sont correctement configurées
2. Erreur de callback
   * Vérifiez que votre URL de callback est accessible publiquement
   * Vérifiez que CSRF est désactivé pour l'endpoint de callback
3. Problème de connexion
   * Vérifiez votre connexion internet
   * Vérifiez que les serveurs Intram sont accessibles

### Support

Pour toute assistance :

* Documentation Intram : <https://intram.org/docs>
* Support email : <contact@intram.org>

{% embed url="<https://github.com/suntechdevs/django-intram>" %}


# PHP SERVER SDK

SDK PHP pour l'intégration côté serveur.

## GENEREZ VOS CLES API

Les clés API sont vos références numériques vers les systèmes Intram. Nous les utilisons pour identifier votre compte et les applications que vous allez créer. Ces clés sont nécessaires pour toute intégration des API de paiements d'Intram. Voici les étapes à suivre :

* Tout d'abord, vous devez avoir un compte Intram Business activé. Créez-en un si ce n'est pas encore le cas.
* [Connectez-vous](https://app.intram.org/login) à votre compte et cliquez sur Développeurs au niveau du menu à gauche.
* Cliquez ensuite sur API, vous verrez toutes vos clés API.
* Vous pouvez passer au mode `SANDBOX` ou activer le mode `LIVE`.

### Installation via composer

```bash
composer require intram/php-sdk
```

### Api configuration

[Login](https://app.intram.org/login) à votre compte Intram, cliquez sur `Developer`, puis sur `API` à ce niveau, récupérez les clés API et donnez-les comme arguments au contrôleur. Initialiser Intram en entrant dans l'ordre : `PUBLIC_KEY`, `PRIVATE_KEY`, `INTRAM_SECRET`, `INTRAM_MARCHAND_KEY`, `MODE` Le mode : `true` pour le mode live et `false` pour le mode test.

```php
$intram = new \Intram\Intram(
            "5b06f06a0aad7d0163c414926b635ee",
            "pk_9c0410014969f276e8b3685fec7",
            "sk_08bd75f9468b484d8a9f24daddf",
            "marchand_id",
            true)
```

### Configurez les informations relatives à votre département / entreprise

#### Nom de votre entreprise (requis)

```php
$intram->setNameStore("Suntech Store");
```

#### URL du Logo de votre entreprise (optionnel)

```php
$intram->setLogoUrlStore("https://www.suntechshop/logo.png");
```

#### Le site web de votre entreprise (optionnel)

```php
$intram->setWebSiteUrlStore("https://www.suntechshop");
```

#### Le numéro de téléphone de votre entreprise

```php
$intram->setPhoneStore("97000000");
```

#### L'adresse postale de votr eentreprise

```php
$intram->setPostalAdressStore("BP 35");
```

### Créer une requête de paiement

Afin de permettre à l'utilisateur d'effectuer un paiement sur votre boutique, vous devez créer la transaction puis lui envoyer l'URL de paiement ou le QR code à scanner. Pour cela :

Ajouter des éléments de facture

Ajouter les différents produits de l'achat

```php
$intram->setItems([
            ['name'=>"T-shirt",'qte'=>"2",'price'=>"500",'totalamount'=>"1000"],
            ['name'=>"trouser",'qte'=>"1",'price'=>"12500",'totalamount'=>"12500"],
        ]);
```

#### La TVA

```php
$intram->setTva([["name" => "VAT (18%)", "amount" => 1000],["name" => " other VAT", "amount" => 500]]);
```

#### Données personnelles

```php
$intram->setCustomData([['CartID',"32393"],['PERIOD',"TABASKI"]]);
```

#### Prix total de la transaction (requis)

```php
$intram->setAmount(13600);
```

#### Devise de la transaction (requis)

```php
$intram->setCurrency("XOF");
```

#### Description de la transaction (optionnelle)

```php
$intram->setDescription("Pretty and suitable for your waterfall");
```

#### Modèle de page de paiement (requis)

```php
$intram->setTemplate("default");
```

#### URL de Redirection

```php
$intram->setRedirectionUrl("https://www.suntechshop/redirection-url");
```

#### URL Retour après transaction

```php
$intram->setReturnUrl("https://www.suntechshop/return-url");
```

#### URL retour après annulation de la transaction

```php
$intram->setCancelUrl("https://www.suntechshop/cancel-url");
```

#### Effectuer la demande de paiement

```php
$response = $intram->setRequestPayment();
```

Réponse attendue

```php
{
 +"status": "PENDING"
 +"transaction_id": "5f2d7a96b97d9d3fea912c11"
 +"receipt_url": "localhost:3000/payment/gate/5f2d7a96b97d9d3fea912c11"
 +"total_amount": 1000
 +"currency":"xof"
 +"qr_code":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHQAAAB0CAYAAABUmhYnAAAAAklEQVR4AewaftIAAAL9SURBVO3BQQ7jCAhFwZcvi6NyKI7KgkyWrCxZdtLdDFWv9wdrDLFGEWsUsUYRaxSxRhFrFL ▶"
}
```

#### Récupérer des données

```php
$transaction_id = $response->transaction_id;
$status = $response->status;
$receipt_url = $response->receipt_url;
$total_amount = $response->total_amount;
.......
```

### Obtenir le statut de la transaction

Donnez l'identifiant de la transaction comme argument à la fonction

```php
$intram->getTransactionStatus($transaction_id);
```

#### Réponse attendue

```php
{
  +"status": "PENDING"
  +"amount": 1000
  +"currency": "XOF"
  +"account_number":"22996XXXXXX"
  +"source":"MOOV"
  +"fees":50
  +"net_amount":1050
}
```

{% hint style="success" %}
Numero de test

**MTN** : 61000000

**MOOV** : 94000000
{% endhint %}

## Tests

Pour exécuter les tests, il suffit de définir les variables d'environnement de la configuration de l'API. Une connexion Internet est nécessaire pour que certains tests soient réussis.

### Licence

MIT


# WOOCOMMERCE PLUGIN

Le plugin WooCommerce est conçu afin de permettre l'intégration de Intram sur votre site WordPress.

Téléchargeable [ici](https://intram.org/intram-wc-plugin.zip) le plugin WooCommerce de Intram est utilisable votre site e-commerce lorsque ce dernier est conçu à partir de WordPress. Lorsqu'il est installé et actif, il est facilement repérable parmi vos extensions.

<figure><img src="https://2777829266-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MGIEqBMMwAoeIigQGKA-887967055%2Fuploads%2Fgit-blob-8f162d9ba8ddb542f336e36fdbbac642edd03490%2FCapture%20d%E2%80%99%C3%A9cran%202022-10-08%20%C3%A0%2000.49.50.png?alt=media" alt=""><figcaption></figcaption></figure>

Afin de le configurer, vous pouvez soit accéder à l'onglet **Paramètres** du plugin WooCommerce ou à l'onglet **Réglages** de votre plugin Intram. Ensuite, cliquez sur l'onglet **Paiements**. Vous verrez alors s'afficher l'interface ci-dessous qui vous présente la liste des différents plugins WooCommerce installés.

<figure><img src="https://2777829266-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MGIEqBMMwAoeIigQGKA-887967055%2Fuploads%2Fgit-blob-fa2d705fa5d2e27cc8fd73334b3f6341ebaf8a17%2FCapture%20d%E2%80%99%C3%A9cran%202022-10-08%20%C3%A0%2000.55.25.png?alt=media" alt=""><figcaption></figcaption></figure>

Procédez alors à la configuration du plugin Intram en cliquant sur l'onglet **Gérer** et en renseignant un certain nombre d'informations dont vos clés API. Récupérez ces dernières directement sur votre tableau de bord Intram (**Développeurs > Clés API**).

{% hint style="info" %}
Si vous évoluez en mode test avec [Intram Sandbox](https://github.com/suntechdevs/paycfa/blob/master/backend/broken-reference/README.md), il vous faudra impérativement activer le mode test lors de la configuration du plugin Intram. Dans ce cas, vous pouvez user des [numéros de téléphone de test](https://github.com/suntechdevs/paycfa/blob/master/backend/broken-reference/README.md) que vous offre Intram afin de vous faire une idée de l'environnement live de Intram.
{% endhint %}

{% hint style="success" %}
Numero de test

**MTN** : 61000000

**MOOV** : 94000000
{% endhint %}

{% hint style="warning" %}
Veuillez à fournir les informations nécessaires pour que l'initialisation du moyen de paiement se fasse sans contrainte au cours d'un achat.

Les informations requises sont : **`PUBLIC API KEY - PRIVATE API KEY - SECRET API KEY - MARCHAND ID - CURRENCY`**
{% endhint %}

{% hint style="info" %}
La devise par défaut d'Intram est le XOF. Cette liste sera mise à jour.

CURRENCY = XOF
{% endhint %}

Configurez vos préférences puis cliquez sur "*Enregistrer"*. Vous pouvez dès lors utiliser Intram pour recevoir les paiements de vos clients.


# MOBILE

SDK et intégrations mobiles pour accepter les paiements Intram dans vos applications iOS et Android.

Intégrez Intram dans vos applications mobiles pour proposer un parcours de paiement fluide à vos utilisateurs.

## SDK disponibles

| Plateforme                    | Statut | Documentation                                                                                     |
| ----------------------------- | :----: | ------------------------------------------------------------------------------------------------- |
| **Flutter** (iOS + Android)   |    ✅   | [Flutter](/mobile/flutter)                                                                        |
| iOS natif (Swift)             |   🔜   | À venir — utilisez le [widget WebView](#alternative-webview-pour-les-stacks-natives) en attendant |
| Android natif (Kotlin / Java) |   🔜   | À venir — utilisez le [widget WebView](#alternative-webview-pour-les-stacks-natives) en attendant |
| React Native                  |   🔜   | À venir — utilisez le [widget WebView](#alternative-webview-pour-les-stacks-natives) en attendant |

## Alternative WebView pour les stacks natives

En attendant un SDK natif, vous pouvez intégrer le paiement Intram dans n'importe quelle app mobile en chargeant le widget JavaScript dans une **WebView** :

1. Créez la transaction depuis votre backend via l'[API publique](/backend/http) ou la [Merchant API](/merchant-api-v1/payment-requests).
2. Récupérez l'URL du gateway (`gateway_url` retourné par `/payment-requests`, ou `receipt_url` retourné par `/payments/request`).
3. Chargez cette URL dans une `WebView` (iOS), `WebView` (Android) ou composant équivalent.
4. Écoutez les redirections vers vos `success_url` / `cancel_url` pour détecter la fin du parcours.
5. Vérifiez le statut final en interrogeant l'[API depuis votre backend](/merchant-api-v1/transactions) — ne vous fiez pas uniquement à la redirection côté client.

## Voir aussi

* [Flutter SDK](/mobile/flutter) — le client mobile recommandé
* [JavaScript widget](/fontend/javascript) — pour les stacks hybrides ou WebView
* [Merchant API — Payment requests](/merchant-api-v1/payment-requests) — créer la demande de paiement côté backend
* [Webhooks signés](/merchant-api-v1/webhooks) — recevoir le résultat final côté serveur en push


# Flutter

Un package Flutter qui simplifie l'implémentation des paiements mobiles avec Intram.

* Support de Flutter 3.0+
* Amélioration de la stabilité
* Corrections de bugs mineurs
* Version initiale
* Support des paiements simples et complets
* Intégration avec l'API Intram

### Installation

Ajoutez cette ligne à votre fichier `pubspec.yaml` :

```yaml
dependencies:
  intram_sdk_flutter: ^1.0.1
```

Puis exécutez :

```bash
flutter pub get
```

### Utilisation

#### Import

```dart
import 'package:intram_sdk_flutter/intram_sdk_flutter.dart';
```

#### Configuration

**Génération des clés API**

Les clés API sont vos références numériques aux systèmes Intram. Pour les obtenir :

1. Créez un compte Intram Business sur <https://app.intram.org/register>
2. Connectez-vous à [votre compte](https://app.intram.org/login)
3. Accédez à "Developers" puis "API"
4. Récupérez vos clés API

**Configuration des clés**

```dart
String public_key = "6a695a2def2ba8e68c773a260f95c0a081e05e79fa6f55a815ef815244e8083a";
String private_key = "tpk_6296f02e367ddee79462671b54dbe568e12f4f41b0bf6b8f758de56cd1ccc68d";
String secret_key = "tsk_e29b25acdef149504854a5dc8c63ff472598c865b38e0829ebb984f3c93b8d16";
String merchant_key = "5ef22f1df84ed86d57b17519";
```

#### Implémentation

**Paiement Complet**

```dart
bool required_user_information = true;
dynamic custom_user_information = {};
```

**Paiement Simple**

```dart
bool required_user_information = false;
dynamic custom_user_information = {
  "message": "Bienvenue sur Hangbé Store, veuillez éffectuer votre paiement pour faire cette commande",
  "firstname": "Client",
  "lastname": "Hangbé",
  "email": "contact@hangbe.com"
};
```

**Initialisation**

```dart
IntramSdkPayment intramSdkPayment = IntramSdkPayment(
  public_key, 
  private_key, 
  secret_key, 
  merchant_key,
  required_user_information, 
  custom_user_information
);
```

#### Paiement

**Effectuer un paiement**

```dart
int amount = 5000;
bool sandbox = true;
String store_name = "Hangbé Store";
String color = "#08a0cf";
String logo = "https://intram.org/images/logo-1.png";

dynamic init_payment_result = await intramSdkPayment.makePayment(
  context, 
  amount, 
  sandbox, 
  store_name, 
  color, 
  logo
);
```

**Réponse de paiement**

```yaml
{success: true, transaction: PJ2flG2YVc}
```

**Vérifier une transaction**

```dart
dynamic transaction = await intramSdkPayment.getTransaction(init_payment_result["transaction"]);
```

### Paramètres

#### Configuration principale

| Paramètre     | Type   | Description                     |
| ------------- | ------ | ------------------------------- |
| public\_key   | String | Clé publique du compte marchand |
| private\_key  | String | Clé privée du compte marchand   |
| secret\_key   | String | Clé secrète du compte marchand  |
| merchant\_key | String | Identifiant du marchand         |

#### Paiement

| Paramètre   | Type   | Description                              |
| ----------- | ------ | ---------------------------------------- |
| amount      | int    | Montant du paiement                      |
| sandbox     | bool   | Mode test (true) ou production (false)   |
| store\_name | String | Nom de la boutique                       |
| color       | String | Couleur du template (format hexadécimal) |
| logo        | String | URL du logo                              |

### Support

Pour toute assistance :

* Email : <contact@intram.org>


# Glossaire

Définition des termes métier et techniques utilisés dans la documentation Intram (API publique, Merchant API, dashboard).

Termes classés par ordre alphabétique. Quand un terme renvoie à une page dédiée, le lien est indiqué.

## A

**`amount`** Montant brut d'une transaction, hors frais. Toujours en unité entière de la devise (ex : `1000` pour 1 000 XOF).

**`amount_total`** Ce que paie effectivement le client : `amount + fees`. Calculé par Intram à partir des [taux applicables](/payment/fees).

**API key** Identifiant numérique de votre compte marchand. Trois clés par environnement :

* `public_key` — identifiant non sensible (équivalent d'un username)
* `private_key` — utilisée par les anciennes intégrations SDK
* `secret_key` — **secret partagé**, sert à signer les requêtes [Merchant API](/merchant-api-v1/authentication) et à vérifier les [webhooks signés](/merchant-api-v1/webhooks)

## B

**Backend** Vos serveurs (production / staging / dev) qui appellent l'API Intram pour piloter votre compte. Distinct du frontend (navigateur du client).

**Balance / Solde** Montant disponible sur votre wallet. Exposé via [`GET /merchant/balance`](/merchant-api-v1/balance). Composé de :

* `available` — disponible pour payouts immédiats
* `pending` — en attente de settlement

## C

**Callback URL** URL fournie au moment d'initier un paiement, vers laquelle Intram redirige le client après le checkout. Différent d'un webhook : c'est une redirection navigateur, pas un appel server-to-server signé.

**CIDR** Notation pour désigner un bloc d'adresses IP. Exemple : `203.0.113.0/24` couvre les 256 IPs de `203.0.113.0` à `203.0.113.255`. Utilisé dans [l'allowlist IP](/merchant-api-v1/sandbox-vs-live#la-whitelist-ip-en-mode-live).

**Compte business / Compte marchand** Entité légale enregistrée chez Intram, distincte de votre compte utilisateur. Un utilisateur peut posséder plusieurs comptes business. Voir [Activation du compte marchand](/account/account-activation).

**Customer / Client** Le payeur final dans une transaction — la personne dont le wallet ou le compte Mobile Money est débité.

## D

**Dashboard** L'interface web sur <https://app.intram.org> permettant de gérer votre compte : clés API, IPs autorisées, webhooks, transactions, balances, paramètres.

**Devise** Code monétaire ISO 4217. Actuellement supporté : `XOF`. Voir [Devises supportées](/payment/supported-devices).

## F

**Fees / Frais** Commissions prélevées sur chaque transaction. Réparties entre **Frais opérateur** (reversés au provider) et **Commission Intram** (conservés par Intram). Voir [Frais](/payment/fees).

## G

**Gateway / Gateway URL** Page web servie par Intram (`gateway.intram.org/<transaction_id>`) où le client effectue son paiement. Renvoyée dans `result.gateway_url` après une [demande de paiement](/merchant-api-v1/payment-requests).

## H

**HMAC / HMAC-SHA256** Algorithme de signature cryptographique. Vous prouvez l'authenticité d'un message en calculant `HMAC-SHA256(secret, message)` ; le receveur recalcule la même chose et compare. Utilisé pour [signer les requêtes Merchant API](/merchant-api-v1/authentication) et [les webhooks sortants](/merchant-api-v1/webhooks).

## I

**Idempotency-Key** Header HTTP unique par opération métier, qui permet de retenter une requête sans risque de double exécution. Obligatoire sur toutes les mutations Merchant API, TTL 24 h. Voir [Idempotency](/merchant-api-v1/idempotency).

**IP allowlist** Liste d'IPs autorisées à appeler l'API en mode live, par marchand. Configurable depuis le dashboard (**Développeurs → IPs autorisées**). Voir [Sandbox vs Live](/merchant-api-v1/sandbox-vs-live).

**ISO 4217** Standard de codification des devises sur 3 lettres (`XOF`, `EUR`, `USD`…).

**ISO 8601** Standard de format des dates : `2026-05-20T10:30:00.000Z`. Utilisé pour le header `X-Timestamp`.

## L

**Live / Mode live** Environnement de production où les transactions touchent l'argent réel. Identifié par les clés `pk_live_…` / `sk_live_…`. Exige une [allowlist IP](/merchant-api-v1/sandbox-vs-live) configurée.

## M

**Mobile Money** Moyen de paiement par téléphone, opéré par des opérateurs télécoms ou bancaires. Codes utilisés :

* `MTN_BENIN_229` — MTN Bénin
* `MOOV_AFRICA_BENIN_229` — Moov Africa Bénin
* `SBIN_BENIN_229` — Sirius Bank Bénin

Voir [Devises supportées](/payment/supported-devices) pour la liste à jour.

**`msisdn`** Numéro de téléphone international au format E.164 sans le `+`. Exemple : `22961234567` pour le Bénin (indicatif 229).

## O

**Operation** Trace d'une opération asynchrone Merchant API (`payout`, `payment_request`, `refund`). Identifiée par un `operation_id` (`op_…`). Statuts : `queued`, `processing`, `succeeded`, `failed`. Voir [Opérations](/merchant-api-v1/operations).

## P

**Payment Link / Intram Direct** Lien de paiement personnalisé que vous générez pour un client, sans qu'il ait à passer par un checkout. Peut être envoyé par SMS, email, ou intégré à un site.

**Payment request** Demande de paiement asynchrone créée via [`POST /merchant/payment-requests`](/merchant-api-v1/payment-requests). Génère un `gateway_url` et un QR code à partager au client.

**Payout / Reversement** Sortie d'argent depuis votre wallet vers un compte externe (Mobile Money ou compte bancaire). Voir [Payouts](/merchant-api-v1/payouts).

**`pending`**

* Sur une transaction : en attente de confirmation provider/client.
* Sur un solde : reçu mais pas encore disponible pour payout.

**Provider** Fournisseur externe de moyen de paiement (MTN, Moov, SBIN, Stripe…). Intram fait la passerelle entre vous et eux.

## Q

**QR code** Code-barres 2D encodant l'URL du gateway. Permet à un client de scanner et payer depuis son téléphone. Renvoyé dans `result.qr_code` au format data-URL PNG.

## R

**`reference`** Identifiant public d'une transaction ou d'un payout, lisible par l'humain (ex : `AB12CD34EF`).

**Refund / Remboursement** Inverser une transaction (totalement ou partiellement). Voir [Refunds](/merchant-api-v1/refunds).

## S

**Sandbox** Environnement de test où aucune somme réelle n'est manipulée. Identifié par les clés `pk_sandbox_…` / `sk_sandbox_…`. Pas de restriction IP. Voir [Sandbox vs Live](/merchant-api-v1/sandbox-vs-live).

**`secret_key`** Clé secrète de votre compte. Sert à calculer la signature HMAC des requêtes Merchant API. **Ne sort jamais** d'un environnement sécurisé.

**Frais opérateur** Part des frais reversée au provider du moyen de paiement (MTN, MOOV, SBIN, Stripe…). Voir [Frais](/payment/fees).

**Commission Intram** Part des frais conservée par Intram pour le service de plateforme.

**`service_reference`** Identifiant de la transaction côté provider (ex : référence MTN, ID Stripe). Différent du `reference` Intram. Exposé dans les réponses des endpoints transaction.

**Settlement** Le moment où une transaction passe de `PENDING` à `SUCCESS` côté provider, et où l'argent crédite réellement votre wallet.

**Signature** Header `X-Signature: sha256=<hex>` calculé à partir de `(timestamp + method + path + query + body)` et de votre `secret_key`. Anti-tampering + anti-replay. Voir [Authentification](/merchant-api-v1/authentication).

## T

**Timestamp** Header `X-Timestamp` en format [ISO 8601](#i) UTC. Rejeté si > 5 minutes d'écart avec l'heure serveur (anti-replay).

**Transaction** Trace d'un paiement reçu par le marchand. Distincte d'un payout (sortant vers Mobile Money ou compte bancaire).

**Trusted proxy** Quand l'application est derrière Nginx, on fait confiance au header `X-Real-IP` qu'il injecte pour résoudre la vraie IP appelante. Contrôlé par la variable d'environnement `MERCHANT_API_TRUSTED_PROXY`.

## U

**Upstream path** Le path que le serveur Node voit après le rewrite Nginx. Pour la [Merchant API](/merchant-api-v1/authentication), l'URL publique `https://api.intram.org/v1/payouts` est réécrite en `/api/v1/merchant/payouts` — c'est ce dernier qui est utilisé pour calculer la signature.

## W

**Wallet** Le porte-monnaie virtuel de votre compte marchand chez Intram. Possède un solde par devise et par moyen de paiement. Les soldes sont strictement isolés entre les environnements `sandbox` et `live`.

**Webhook** Notification HTTP signée envoyée par Intram à votre URL configurée quand un événement se produit (ex : payout réussi, refund accepté). Politique de retry exponentiel jusqu'à 5 tentatives. Voir [Recevoir les webhooks](/merchant-api-v1/webhooks).

## Voir aussi

* [Troubleshooting](/annexes/troubleshooting) — symptômes courants → causes → solutions
* [Merchant API — Overview](/merchant-api-v1/merchant-api)
* [API publique de paiement](/api-index)


# Troubleshooting

Symptômes courants rencontrés lors de l'intégration des APIs Intram, leurs causes probables et les solutions à appliquer.

Cette page regroupe les problèmes les plus fréquents — sur le compte, sur l'API publique de paiement et sur la Merchant API. Cherchez votre symptôme dans la table, puis suivez le lien vers la section détaillée.

## Index par symptôme

| Symptôme                                      | Section                                                                                     |
| --------------------------------------------- | ------------------------------------------------------------------------------------------- |
| `401 invalid token` / `401 No token provided` | [Authentification dashboard](#401-token-invalide-ou-absent)                                 |
| `401 missing_api_key` / `401 invalid_api_key` | [Clés API Merchant manquantes ou invalides](#401-clés-api-merchant-manquantes-ou-invalides) |
| `401 signature_invalid`                       | [Signature HMAC invalide](#401-signature-hmac-invalide)                                     |
| `403 ip_allowlist_empty`                      | [Allowlist IP vide en mode live](#403-ip_allowlist_empty)                                   |
| `403 ip_not_allowed`                          | [IP appelante non autorisée](#403-ip_not_allowed)                                           |
| `403 merchant_inactive`                       | [Compte non validé](#403-merchant_inactive)                                                 |
| `400 missing_idempotency_key`                 | [Idempotency-Key manquante](#400-missing_idempotency_key)                                   |
| `409 idempotency_conflict`                    | [Conflit d'idempotence](#409-idempotency_conflict)                                          |
| `400 validation_error`                        | [Body invalide](#400-validation_error)                                                      |
| `404 transaction_not_found`                   | [Transaction introuvable](#404-transaction_not_found)                                       |
| Webhook jamais reçu                           | [Aucun webhook ne parvient](#aucun-webhook-ne-parvient)                                     |
| Signature webhook invalide chez moi           | [Vérification signature webhook échoue](#vérification-signature-webhook-échoue)             |
| Opération bloquée en `queued`                 | [Opération asynchrone ne progresse pas](#opération-asynchrone-ne-progresse-pas)             |
| `insufficient_balance` au runtime             | [Solde insuffisant](#solde-insuffisant)                                                     |
| `provider_rejected` au runtime                | [Provider a rejeté](#provider_rejected)                                                     |
| Le client paie mais je vois `PENDING`         | [Transaction reste pending après paiement](#transaction-reste-pending-après-paiement)       |
| Refund refusé `not_refundable`                | [Statut transaction non remboursable](#not_refundable)                                      |
| Sandbox ne fonctionne pas                     | [Confusion sandbox / live](#confusion-sandbox--live)                                        |
| `502 Bad Gateway` / `504 Gateway Timeout`     | [Problème côté infrastructure](#502--504-côté-infrastructure)                               |

***

## 401 : token invalide ou absent

**Symptôme**

```json
{ "error": true, "message": "invalid token" }
```

ou

```json
{ "error": true, "message": "No token provided." }
```

**Quand** Sur les endpoints **dashboard** (qui exigent un JWT utilisateur).

**Causes**

* Le header `Authorization` n'est pas envoyé.
* Le token JWT est expiré (durée de vie 24 h par défaut).
* Le token a été signé avec un autre `APP_SECRET` que celui du serveur.

**Fix**

1. Vérifiez que votre client envoie bien `Authorization: <token>` (sans préfixe `Bearer` côté admin Intram).
2. Re-loggez-vous depuis le dashboard pour obtenir un nouveau token.
3. Si vous appelez depuis votre code, refresh-loggez avant chaque appel sensible ou implémentez un mécanisme de refresh.

***

## 401 : clés API Merchant manquantes ou invalides

**Symptôme**

```json
{ "error": true, "code": "missing_api_key" }
```

ou

```json
{ "error": true, "code": "invalid_api_key" }
```

**Quand** Sur tout endpoint [`/api/v1/merchant/*`](/merchant-api-v1/merchant-api).

**Causes**

* Vous n'envoyez pas le header `X-Api-Key`.
* La clé est faussée (espace ajouté en fin de chaîne, copier-coller incomplet).
* Vous utilisez une clé sandbox sur une URL live, ou inversement (vérifiez l'env de la clé : préfixe `pk_sandbox_…` vs `pk_live_…`).
* La clé a été régénérée depuis le dashboard et l'ancienne ne fonctionne plus.

**Fix**

1. Récupérez la clé depuis **Développeurs → API** dans le dashboard.
2. Vérifiez le préfixe (`pk_sandbox_` ou `pk_live_`).
3. Mettez à jour vos variables d'environnement et redéployez.

***

## 401 : signature HMAC invalide

**Symptôme**

```json
{
  "error": true,
  "code": "signature_invalid",
  "message": "Signature verification failed: <reason>"
}
```

Où `<reason>` ∈ `missing_signature`, `missing_timestamp`, `invalid_timestamp_format`, `timestamp_out_of_window`, `signature_mismatch`.

**Causes par `reason`**

| `reason`                   | Cause                                                             |
| -------------------------- | ----------------------------------------------------------------- |
| `missing_signature`        | Header `X-Signature` absent                                       |
| `missing_timestamp`        | Header `X-Timestamp` absent                                       |
| `invalid_timestamp_format` | Timestamp n'est pas en ISO 8601                                   |
| `timestamp_out_of_window`  | Plus de 5 minutes d'écart entre votre horloge et celle du serveur |
| `signature_mismatch`       | Le HMAC calculé ne correspond pas                                 |

**Fix pour `signature_mismatch` — checklist**

* Le path utilisé pour signer est **le path interne** `/api/v1/merchant/<endpoint>`, pas l'URL publique `https://api.intram.org/v1/<endpoint>`.
* Le body utilisé est **le raw body envoyé**, octet pour octet. Pas une re-sérialisation post-modification.
* La query string est triée alphabétiquement par clé, jointe par `&`.
* Le séparateur entre les 5 champs est un `\n` (line feed), pas `\r\n`.
* Le secret utilisé est bien votre **clé secrète** (pas la clé privée, ni la clé publique).
* Vous comparez la **même casse** : l'en-tête doit commencer par `sha256=` minuscule, suivi du hex en minuscule.

**Fix pour `timestamp_out_of_window`**

* Synchronisez l'horloge serveur via NTP : `timedatectl status` doit afficher `System clock synchronized: yes`.
* Vérifiez que vous n'envoyez pas un timestamp pré-calculé qui dort dans un queue.

Voir le [détail complet de la signature](/merchant-api-v1/authentication).

***

## 403 : `ip_allowlist_empty`

**Symptôme**

```json
{
  "error": true,
  "code": "ip_allowlist_empty",
  "message": "Live API keys require at least one allowed IP."
}
```

**Cause** Vous utilisez une clé `pk_live_…` mais aucune IP n'est whitelistée pour votre compte. C'est volontaire : une liste vide rejette tout (fail-closed).

**Fix**

1. Récupérez l'IP publique de votre serveur backend : `curl -4 ifconfig.me` depuis le serveur.
2. Dashboard → **Développeurs → IPs autorisées → Ajouter un serveur**.
3. Collez l'IP, ajoutez un libellé, sauvegardez.
4. Réessayez votre appel — c'est immédiat (pas de cache).

⚠️ N'ajoutez pas l'IP de votre **navigateur** par erreur ; c'est l'IP du **serveur** qui doit être autorisée.

Voir [Sandbox vs Live + IP allowlist](/merchant-api-v1/sandbox-vs-live).

***

## 403 : `ip_not_allowed`

**Symptôme**

```json
{
  "error": true,
  "code": "ip_not_allowed",
  "message": "This source IP is not in the merchant allowlist",
  "client_ip": "203.0.113.99"
}
```

**Causes**

* Votre serveur a une nouvelle IP publique (changement de fournisseur, NAT modifié, scale-out vers une autre région).
* Votre allowlist contient `203.0.113.4` mais la requête vient de `203.0.113.5` (différence d'un octet).
* Vous appelez depuis une machine de dev non whitelistée (alors qu'il faudrait utiliser une clé sandbox).

**Fix**

1. Le champ `client_ip` dans la réponse vous dit **exactement** quelle IP a été vue.
2. Si c'est légitime : ajoutez cette IP à l'allowlist (ou un CIDR englobant).
3. Si vous êtes en dev local : utilisez une clé sandbox (pas de restriction).

***

## 403 : `merchant_inactive`

**Symptôme**

```json
{ "error": true, "code": "merchant_inactive", "message": "Merchant account not validated" }
```

**Cause** Votre compte business n'est pas encore validé par l'équipe Intram.

**Fix**

1. Suivez la procédure [Activation du compte marchand](/account/account-activation).
2. Téléversez les pièces justificatives requises.
3. Attendez la validation (1 à 3 jours ouvrés).
4. En attendant, vous pouvez intégralement développer en `SANDBOX`.

***

## 400 : `missing_idempotency_key`

**Symptôme**

```json
{ "error": true, "code": "missing_idempotency_key" }
```

**Cause** Vous faites un POST/PUT/PATCH sans header `Idempotency-Key`.

**Fix** Générez une clé unique par opération métier (`crypto.randomUUID()` par exemple) et passez-la en header :

```
Idempotency-Key: payout-2026-05-20-batch-1
```

Format : 8 à 128 caractères, `[A-Za-z0-9_-]`. Voir [Idempotency](/merchant-api-v1/idempotency).

***

## 409 : `idempotency_conflict`

**Symptôme**

```json
{ "error": true, "code": "idempotency_conflict",
  "message": "Idempotency-Key already used with a different request body" }
```

**Cause** Vous réutilisez une clé d'idempotence avec un body différent. Le serveur refuse car il garantit qu'une clé = une opération.

**Fix**

* Si c'est un retry de la **même** opération : assurez-vous d'envoyer **exactement le même body**.
* Si c'est une **nouvelle** opération (montant différent, destinataire différent…) : utilisez une **nouvelle clé**.

Voir [Idempotency — quand changer de clé](https://docs.intram.org/annexes/pages/J8XDOH0wAqnrSVMJNbSI#quand-tu-dois-changer-de-clé).

***

## 400 : `validation_error`

**Symptôme**

```json
{
  "error": true,
  "code": "validation_error",
  "message": "Invalid …",
  "details": [
    { "path": "amount", "msg": "amount must be > 0" }
  ]
}
```

**Cause** Le body ne respecte pas le schéma de l'endpoint.

**Fix** Lisez `details[]` pour voir précisément quel champ pose problème. Référez-vous à la doc de l'endpoint concerné dans la [Reference Merchant API](/merchant-api-v1/balance).

***

## 404 : `transaction_not_found`

**Symptôme**

```json
{ "error": true, "code": "transaction_not_found",
  "message": "Transaction not found for this merchant" }
```

**Causes**

* La référence n'existe pas (faute de frappe).
* La transaction existe mais appartient à un autre marchand. Vous ne pouvez accéder qu'aux vôtres.
* Vous êtes en sandbox et vous cherchez une transaction live (ou inverse) — les wallets sont isolés.

**Fix**

1. Listez vos transactions via [`GET /merchant/transactions?limit=10`](/merchant-api-v1/transactions) pour vérifier le format des références.
2. Vérifiez l'environnement de la clé que vous utilisez.

***

## Aucun webhook ne parvient

**Symptôme** Vous avez configuré un webhook mais aucun appel n'arrive sur votre endpoint.

**Causes possibles & vérifications**

| Cause                                                    | Vérification                                                                                                                                     |
| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| URL en HTTP (pas HTTPS)                                  | Le serveur refuse de créer un webhook non-HTTPS. La création aurait dû échouer en 400 — recréez en HTTPS.                                        |
| URL inaccessible publiquement                            | Votre URL doit être joignable depuis Internet (pas `localhost`, pas IP privée). Utilisez ngrok / Cloudflare Tunnel pour les tests locaux.        |
| Firewall bloque le trafic Intram                         | Vérifiez vos règles entrantes côté serveur applicatif. Vous pouvez whitelister les IPs sortantes Intram (demandez au support la liste actuelle). |
| Souscription mal configurée (event qui ne match pas)     | `GET /merchant/webhooks` pour lister vos souscriptions ; vérifiez le pattern `event`.                                                            |
| Aucun event émis (l'opération n'a peut-être pas eu lieu) | `GET /merchant/operations/:id` pour voir le statut de l'opération concernée.                                                                     |

**Test rapide**

```bash
# Envoyer une livraison test à votre URL
POST /merchant/webhooks/<webhook_id>/test
```

Si ce test passe et qu'aucun event réel n'arrive ensuite : c'est que les events ne sont pas émis (cf. dernière ligne du tableau).

Voir [Webhooks signés](/merchant-api-v1/webhooks).

***

## Vérification signature webhook échoue

**Symptôme** Vous recevez bien les webhooks mais votre vérification HMAC échoue.

**Causes courantes**

1. **Vous utilisez le body parsé** (`req.body` après `express.json()`) au lieu du **raw body** brut. La signature porte sur les octets bruts — re-sérialiser produit un body légèrement différent.
2. **Mauvais secret** — assurez-vous d'utiliser le secret affiché **une seule fois** à la création du webhook. Si vous l'avez perdu, supprimez et recréez la souscription.
3. **Timestamp trop vieux** — vérifiez l'écart entre `X-Intram-Timestamp` et votre horloge serveur.
4. **Encodage du body** — si vous lisez le body en UTF-8 décodé, vous pouvez perdre des octets multi-byte. Lisez en `Buffer` brut.

**Pattern correct (Node + Express)**

```javascript
app.post('/webhook',
  express.raw({ type: 'application/json' }),   // ← raw, pas json()
  (req, res) => {
    const sig = req.headers['x-intram-signature'];
    const ts  = req.headers['x-intram-timestamp'];
    const expected = 'sha256=' + crypto
      .createHmac('sha256', SECRET)
      .update(`${ts}.${req.body}`)             // req.body est un Buffer
      .digest('hex');
    // ...
  }
);
```

Voir [Recevoir les webhooks](https://docs.intram.org/annexes/pages/y3OX0ZXtp5SI26iS0jqX#vérifier-la-signature).

***

## Opération asynchrone ne progresse pas

**Symptôme** `GET /merchant/operations/:id` renvoie `status: "queued"` ou `"processing"` depuis plusieurs minutes sans bouger.

**Causes**

* Le worker (`merchant-api-worker`) est arrêté côté infrastructure. Côté ops : `pm2 status`.
* Backlog de la queue (peu probable en charge normale).
* Pour les payment requests : la transaction attend le webhook provider qui n'arrive pas.

**Fix côté intégrateur**

* Patientez quelques secondes — la grande majorité des opérations se terminent en moins de 30 s.
* Si > 5 minutes : escaladez au support en fournissant l'`operation_id`.
* Mettez en place des webhooks pour ne plus avoir à poller (préféré).

***

## `insufficient_balance`

**Symptôme** (au niveau opération, dans `op.error`)

```json
{ "code": "insufficient_balance", "message": "Insufficient merchant balance" }
```

**Causes**

* Votre wallet est effectivement vide ou en dessous du montant + frais demandés.
* Le `pending` est conséquent mais pas encore en `available`.

**Fix**

1. Vérifiez votre solde réel : `GET /merchant/balance` et regardez `available` vs `pending`.
2. Attendez le settlement du `pending` (cycle variable selon les providers).
3. Faites des payouts plus petits ou groupez-les différemment.

***

## `provider_rejected`

**Symptôme** (au niveau opération)

```json
{
  "code": "provider_rejected",
  "message": "...",
  "details": { "responsecode": "...", "responsemsg": "..." }
}
```

**Cause** Le provider externe (MTN, Moov, SBIN, Stripe) a refusé l'opération.

**Fix**

1. Lisez `details.responsemsg` pour comprendre la raison (compte fermé, MSISDN invalide, plafond atteint…).
2. Pour Mobile Money : vérifiez le format du `msisdn` (international sans `+`, ex : `22961234567`).
3. Pour les bank wires : vérifiez le SWIFT et le numéro de compte.
4. Le wallet a été automatiquement recrédité — vous pouvez retenter avec une nouvelle clé d'idempotence après correction.

***

## Transaction reste `pending` après paiement

**Symptôme** Le client dit avoir payé via Mobile Money mais `GET /merchant/transactions/:reference` montre `status: PENDING`.

**Causes**

* Le webhook provider n'est pas encore arrivé chez Intram (latence typique : 5–30 s, max 6 minutes).
* Le client a abandonné après l'OTP sans confirmer.
* Erreur réseau côté provider.

**Fix**

1. Patientez jusqu'à 6 minutes (timeout côté provider).
2. Si toujours `pending` après : la transaction est officiellement timeout — informez le client. Si l'argent a quand même été débité chez lui, un rapprochement manuel est nécessaire (escaladez au support).
3. Pour éviter le polling : abonnez-vous aux webhooks `payment_request.paid` / `payment_request.failed`.

***

## `not_refundable`

**Symptôme**

```json
{ "code": "not_refundable", "message": "Transaction status is ERROR" }
```

**Cause** La transaction n'est pas dans un statut qui permet le refund. Statuts refundables : `SUCCESS` (refund complet ou partiel) et `PENDING` (annulation côté Stripe). Pas refundables : `ERROR`, `REFUNDED`.

**Fix**

* Si la transaction est `ERROR` : pas de remboursement nécessaire, l'argent n'a pas été débité.
* Si la transaction est déjà `REFUNDED` : un refund a déjà été appliqué, voir `refunds[]` dans `GET /merchant/transactions/:reference`.

***

## Confusion sandbox / live

**Symptômes possibles**

* "Mon paiement sandbox n'apparaît pas dans le dashboard live."
* "Mon payout sandbox ne crédite pas le destinataire."
* "Le client a payé mais je ne vois rien dans mon wallet."

**Cause** Vous mélangez les deux environnements. Les wallets sandbox et live sont **complètement isolés**.

**Fix**

1. Vérifiez le préfixe de votre clé : `pk_sandbox_…` vs `pk_live_…`.
2. Dans le dashboard, le toggle SANDBOX/LIVE en haut de page filtre les transactions affichées.
3. En sandbox, aucun argent réel ne bouge — c'est normal que le destinataire ne reçoive rien sur son téléphone.

Voir [Sandbox vs Live](/merchant-api-v1/sandbox-vs-live).

***

## 502 / 504 côté infrastructure

**Symptômes**

* `502 Bad Gateway`
* `504 Gateway Timeout`
* Réponse vide / connexion fermée

**Causes**

* Le service Intram est temporairement indisponible (maintenance, incident).
* Votre requête dépasse le timeout du proxy (rare avec le Merchant API qui est async ; possible avec l'ancienne API `/payments/update` qui attend jusqu'à 6 min).

**Fix**

1. Vérifiez le [statut Intram](https://app.intram.org) (notifications dans le dashboard).
2. Retentez avec backoff exponentiel.
3. Pour la Merchant API : utilisez votre `Idempotency-Key` originale lors des retries — c'est sûr.
4. Si le problème persiste plus de quelques minutes, contactez le support avec l'horodatage UTC et l'`operation_id` si applicable.

***

## Vous ne trouvez pas votre symptôme ?

* Consultez le [glossaire](/annexes/glossary) pour clarifier les termes.
* Vérifiez les pages dédiées : [Merchant API errors](/merchant-api-v1/errors), [Webhooks](/merchant-api-v1/webhooks), [Authentification](/merchant-api-v1/authentication).
* Contactez le support depuis le dashboard avec :
  * L'horodatage UTC précis de votre requête
  * L'`operation_id` (si Merchant API) ou la `reference` (si API publique)
  * Le code d'erreur reçu
  * Un extrait des headers que vous envoyez (en masquant les secrets)


