> For the complete documentation index, see [llms.txt](https://docs.intram.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.intram.org/annexes/troubleshooting.md).

# Troubleshooting

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.md).

**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.md).

***

## 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.md).

***

## 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.md).
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.md).

***

## 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.md).

***

## 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.md) 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.md).

***

## 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.md).

***

## 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.md) pour clarifier les termes.
* Vérifiez les pages dédiées : [Merchant API errors](/merchant-api-v1/errors.md), [Webhooks](/merchant-api-v1/webhooks.md), [Authentification](/merchant-api-v1/authentication.md).
* 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)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.intram.org/annexes/troubleshooting.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
