# Kratos Pay — Guide d'intégration marchand

Parcours pas à pas, routes, corps de requête et réponses possibles (API v1, version du 11 novembre 2026).

- Production : `https://api.kratospay.com/api/v1`
- Recette : `https://dev-aggregator-api.kratospay.com/api/v1`

## Sommaire

- **Introduction**
  - Vue d'ensemble
  - Conventions communes
- **Marchand particulier (développeur)**
  - 1. Créer son compte
  - 2. Se connecter (WhatsApp)
  - 3. Application et webhook
  - 4. Générer une clé API
  - 5. Encaisser un client
  - 6. Suivre et annuler
  - 7. Liens de paiement
  - 8. Solde et retraits
  - 9. Rembourser
  - Droits et limites
- **Marchand entreprise**
  - 1. Créer le compte
  - 2. Se connecter (TOTP)
  - 3. Valider le compte (KYC)
  - 4. Créer une application
  - 5. Générer ses clés API
  - 6. Encaisser et envoyer
  - 7. Liens de paiement
  - 8. Solde et retraits
  - 9. Paiements de masse
  - 10. Équipe et autres modules
- **Encaissement par QR GIMAC**
  - Code marchand et points QR
- **Annexes**
  - Webhooks
  - Statuts d'une transaction
  - Erreurs fréquentes

## Introduction

### Vue d'ensemble

Kratos Pay API est la passerelle de paiement de Kratos Financials Inc. Elle permet à n'importe quelle application — boutique en ligne, plateforme SaaS, application mobile — d'encaisser et d'envoyer des paiements Mobile Money, d'accepter les cartes bancaires et les paiements QR GIMAC, puis de suivre les transactions par webhook.

#### Ce que permet l'API

- Encaisser par Mobile Money (`collection`), carte bancaire via une page hébergée, ou QR GIMAC.
- Envoyer des fonds vers un portefeuille Mobile Money depuis votre solde (`disbursement`), retirer votre argent, programmer des retraits ou payer plusieurs bénéficiaires en une fois.
- Encaisser sans coder grâce aux liens de paiement et aux QR codes.
- Être notifié en temps réel de chaque changement de statut via des webhooks signés.
- Tester l'intégralité du parcours sans argent réel avec une clé `sk_test_…`.

#### Vocabulaire clé

- **Application** : une intégration (site, application, terminal) avec ses clés, son webhook et son solde. Son identifiant s'envoie dans `X-Application-ID`.
- **Clé API** : `sk_test_…` (test, aucun argent réel) ou `sk_live_…` (vrais paiements). Elle reste sur votre serveur.
- **Transaction** : un mouvement d'argent identifié par un `uuid`, dans un des statuts `pending`, `success`, `failed`, `expired`, `refunded`…
- **Webhook** : une notification HTTP POST envoyée par Kratos Pay vers votre serveur à chaque changement de statut d'une transaction.
- **Idempotency key** : un identifiant unique que vous générez pour chaque opération, pour éviter qu'une même transaction soit créée deux fois en cas de nouvel essai réseau.

#### Deux profils de marchand

|   | Marchand particulier (développeur) | Marchand entreprise |
| --- | --- | --- |
| Pour qui | Une personne qui encaisse pour elle-même et intègre l'API (site, application, bot). | Une société qui encaisse pour son activité. |
| Connexion | Téléphone + code WhatsApp | E-mail + mot de passe + TOTP obligatoire |
| Validation KYC | Pas exigée pour encaisser en live | Obligatoire avant tout paiement réel |
| Applications | Une application personnelle, créée automatiquement | Plusieurs applications (site, application mobile, terminal…) |
| Fonctions réservées | — | Équipe, retraits programmés, paiements de masse |

- **Particulier :** créer le compte → se connecter → récupérer son application → configurer le webhook → générer une clé API → encaisser → suivre → retirer son argent.
- **Entreprise :** créer le compte → activer le TOTP → soumettre le KYC → créer une application → clé API → encaisser → liens de paiement → solde et retraits → équipe.

### Conventions communes

Ce chapitre vaut pour toutes les routes des parcours particulier et entreprise. Il suffit de le lire une fois.

#### Adresses de l'API

| Environnement | Adresse de base |
| --- | --- |
| Production | https://api.kratospay.com/api/v1 |
| Recette (staging) | https://dev-aggregator-api.kratospay.com/api/v1 |

Tous les chemins de cette documentation sont relatifs à cette adresse de base (exemple : `POST /transactions` devient `POST https://api.kratospay.com/api/v1/transactions`). Les corps sont en JSON (`Content-Type: application/json`, `Accept: application/json`).

#### Deux types d'authentification

| Type | Pour quoi faire | Comment l'envoyer |
| --- | --- | --- |
| Jeton de session (dashboard) | Gérer son compte : profil, applications, clés API, KYC, liens de paiement, solde, retraits. | `Authorization: Bearer {token}`, obtenu à la connexion. |
| Clé API (`sk_test_…` / `sk_live_…`) | Intégrer les paiements dans votre serveur : créer, suivre, annuler ou rembourser une transaction, et gérer les points QR GIMAC. | `Authorization: Bearer sk_live_…` (ou en-tête `X-Api-Key`), plus `X-Application-ID: {id de votre application}`. |

> La clé API ne doit **jamais** être placée dans une application mobile ou un navigateur : elle peut encaisser et rembourser. Gardez-la sur votre serveur.

- `sk_test_…` : environnement de test, réseaux simulés, **aucun argent réel**. `sk_live_…` : vrais paiements.
- La clé n'est affichée qu'une seule fois, à sa création. Perdue, elle se révoque et se régénère.

#### Format des réponses

Succès (HTTP 2xx) :

```json
{
  "success": true,
  "data": {},
  "meta": {
    "request_id": "550e8400-e29b-41d4-a716-446655440000",
    "timestamp": "2026-11-02T10:30:00Z"
  }
}
```

Échec (HTTP 4xx/5xx) :

```json
{
  "success": false,
  "error": {
    "code": "INSUFFICIENT_BALANCE",
    "message": "Solde insuffisant.",
    "details": {
      "amount": [
        "Le montant dépasse le solde disponible."
      ]
    }
  },
  "meta": {
    "request_id": "...",
    "timestamp": "..."
  }
}
```

`error.details` n'est présent que pour les erreurs de validation (422) : une entrée par champ fautif, chacune avec la liste de ses messages. Communiquez le `meta.request_id` au support en cas de problème.

#### Codes HTTP

| HTTP | Signification | Que faire |
| --- | --- | --- |
| 200 / 201 / 202 | Succès (201 = ressource créée, 202 = traitement accepté, en cours). | — |
| 401 | Jeton ou clé API absent, invalide, expiré ou révoqué. | Se reconnecter / vérifier la clé et son environnement. |
| 403 | Action réservée (ex. fonction réservée aux entreprises, permission d'équipe manquante). | Vérifier le type de compte et les droits. |
| 404 | Ressource introuvable ou appartenant à un autre compte (jamais 403 dans ce cas). | Vérifier l'identifiant. |
| 422 | Données invalides ou règle métier refusée (solde, KYC, réseau indisponible…). | Lire `error.code` et `error.details`. |
| 429 | Trop de requêtes. | Réessayer après une courte attente. |
| 5xx | Incident côté Kratos Pay ou réseau de paiement. | Réessayer ; ne **jamais** créer deux fois la même opération sans clé d'idempotence. |

#### Réseaux de paiement et numéros

| Valeur de `provider` | Réseau | Encaissement | Envoi / retrait | Remboursement |
| --- | --- | --- | --- | --- |
| `orange_money_cm` | Orange Money Cameroun | Oui | Oui | Oui |
| `mtn_momo_cm` | MTN Mobile Money Cameroun | Oui | Oui | Non |
| `mastercard` | Cartes Mastercard et Visa (page de paiement sécurisée) | Oui | Non | Non |
| `gimac` | QR GIMAC (paiement par scan) | Oui | Non | Non |

- La liste à jour des valeurs acceptées s'obtient par `GET /payment-providers` (jeton de session). Recopiez la valeur `selector` telle quelle. Pour Mastercard comme pour Visa, le champ `provider` reste `mastercard`.
- Pour Mobile Money, envoyez le numéro sans indicatif (`696246686`) et correspondant au pays du réseau ; la carte bancaire ne requiert pas de numéro.
- Montants : envoyez un nombre (`5000`). Les réponses renvoient les montants en chaîne avec deux décimales (`"5000.00"`). Devise par défaut : `XAF`.
- Les paiements Mobile Money et carte sont **asynchrones** (`pending`) ; attendez le webhook ou consultez la transaction. Un paiement QR GIMAC reçu crée directement une transaction `success`.

#### Idempotence

Sur toute création d'argent, envoyez un `idempotency_key` unique par opération métier (ex. numéro de commande). Si votre appel échoue sur un timeout et que vous le renvoyez avec la même clé, vous recevez la transaction déjà créée au lieu d'en créer une seconde. Une clé déjà utilisée par une autre opération est refusée en 422.

## Marchand particulier (développeur)

### 1. Créer son compte

Le marchand particulier encaisse pour lui-même et intègre l'API (site, application, bot). Son compte est créé avec une application personnelle et un lien de paiement permanent, sans démarche supplémentaire. Il se connecte par numéro de téléphone et code WhatsApp (pas de mot de passe à la connexion, pas de TOTP).

#### Créer son compte

**POST** `/auth/register` — Public

Crée le compte particulier. Aucune clé API n'est générée à cette étape. Le téléphone sert à la connexion : renseignez-le (l'e-mail reste facultatif).

Corps de la requête :

```json
{
  "name": "Nom Prénom",
  "merchant_type": "individual",
  "phone": "696246686",
  "email": "contact@exemple.cm",
  "password": "<mot-de-passe>",
  "password_confirmation": "<mot-de-passe>",
  "country_id": 1
}
```

Réponse 201 :

```json
{
  "success": true,
  "data": {
    "id": 1,
    "name": "Nom Prénom",
    "email": "contact@exemple.cm",
    "merchant_type": "individual",
    "kyc_status": "none",
    "is_active": true,
    "personal_payment_link": {
      "...": "..."
    },
    "created_at": "2026-11-02T08:00:00Z"
  },
  "meta": {
    "request_id": "...",
    "timestamp": "..."
  }
}
```

#### Réponses possibles en cas d'échec

| Code | Quand |
| --- | --- |
| 422 | E-mail ou téléphone déjà utilisé, mot de passe de moins de 8 caractères, confirmation différente, champ obligatoire manquant. |

- Champs obligatoires : `name`, `merchant_type`, `password`, `password_confirmation`.
- Les identifiants de pays (`country_id`) se lisent via `GET /countries`.
- Pour retirer par virement bancaire, renseignez aussi `iban` et `bank_account_holder` dès l'inscription.

### 2. Se connecter (WhatsApp)

La connexion du particulier se fait en deux appels : le numéro de téléphone, puis le code à 6 chiffres reçu par WhatsApp.

#### Demander le code WhatsApp

**POST** `/auth/login/phone` — Public

Envoie un code à 6 chiffres par WhatsApp au numéro du compte. Réservée aux comptes particuliers.

Corps de la requête :

```json
{
  "phone": "696246686"
}
```

Réponse 200 :

```json
{
  "success": true,
  "data": {
    "requires_whatsapp_otp": true,
    "challenge_token": "550e8400-e29b-41d4-a716-446655440000"
  },
  "meta": {
    "request_id": "...",
    "timestamp": "..."
  }
}
```

| Code | Quand |
| --- | --- |
| 422 | Numéro inconnu, compte entreprise (utiliser la connexion par e-mail), compte désactivé, ou envoi WhatsApp en échec. |

#### Valider le code et recevoir le jeton

**POST** `/auth/whatsapp-otp/challenge` — Public

Valide le code reçu. Le `challenge_token` est à usage unique et expire après 5 minutes.

Corps de la requête :

```json
{
  "challenge_token": "550e8400-e29b-41d4-a716-446655440000",
  "code": "123456"
}
```

Réponse 200 :

```json
{
  "success": true,
  "data": {
    "merchant": {
      "id": 1,
      "name": "Nom Prénom",
      "merchant_type": "individual",
      "kyc_status": "none"
    },
    "token": "<jeton-de-session>",
    "token_type": "Bearer"
  },
  "meta": {
    "request_id": "...",
    "timestamp": "..."
  }
}
```

| Code | Quand |
| --- | --- |
| 422 | Code invalide, challenge expiré ou déjà consommé : relancer l'étape précédente. |

- Conservez `token` : c'est le jeton de session des étapes suivantes (`Authorization: Bearer {token}`).
- Déconnexion : `POST /auth/logout`.

### 3. Application et webhook

Le particulier dispose d'une application personnelle. Son identifiant est à envoyer dans l'en-tête `X-Application-ID` de tous les appels par clé API. Configurez ensuite l'adresse qui recevra les webhooks.

#### Récupérer son application personnelle

**GET** `/applications` — Jeton de session

Réponse 200 :

```json
{
  "success": true,
  "data": {
    "items": [
      {
        "id": 1,
        "name": "Nom Prénom",
        "webhook_url": null,
        "has_webhook": false,
        "is_active": true,
        "created_at": "2026-11-02T08:00:00Z"
      }
    ],
    "pagination": {
      "...": "..."
    }
  },
  "meta": {
    "request_id": "...",
    "timestamp": "..."
  }
}
```

| Code | Quand |
| --- | --- |
| 401 | Jeton absent ou expiré. |

#### Configurer le webhook (recommandé)

**PATCH** `/applications/{id}` — Jeton de session

Renseigne l'adresse HTTPS de votre serveur qui recevra les notifications de changement de statut (voir l'annexe Webhooks).

Corps de la requête :

```json
{
  "webhook_url": "https://mon-site.cm/webhooks/kratospay"
}
```

Réponse 200 :

```json
{
  "success": true,
  "data": {
    "id": 1,
    "webhook_url": "https://mon-site.cm/webhooks/kratospay",
    "has_webhook": true,
    "updated_at": "..."
  },
  "meta": {
    "request_id": "...",
    "timestamp": "..."
  }
}
```

| Code | Quand |
| --- | --- |
| 404 | Application introuvable. |
| 422 | URL invalide. |

#### Obtenir le secret de signature du webhook

**POST** `/applications/{id}/webhook/rotate-secret` — Jeton de session

Génère un secret de signature, affiché **une seule fois**. Il sert à vérifier que chaque webhook vient bien de Kratos Pay. Refaire l'appel invalide immédiatement l'ancien secret.

Réponse 200 :

```json
{
  "success": true,
  "data": {
    "application": {
      "id": 1,
      "webhook_url": "https://mon-site.cm/webhooks/kratospay",
      "has_webhook": true
    },
    "webhook_secret": "whsec_<secret>"
  },
  "meta": {
    "request_id": "...",
    "timestamp": "..."
  }
}
```

| Code | Quand |
| --- | --- |
| 404 | Application introuvable. |

### 4. Générer une clé API

La clé API sert à appeler les routes de paiement depuis votre serveur. Commencez par une clé `test`, puis créez une clé `live` quand votre intégration est validée.

#### Générer une clé API

**POST** `/auth/keys` — Jeton de session

Corps de la requête :

```json
{
  "name": "Mon serveur",
  "environment": "test",
  "expires_at": null
}
```

Réponse 201 :

```json
{
  "success": true,
  "data": {
    "key": "sk_test_<votre-cle>",
    "meta": {
      "id": 1,
      "name": "Mon serveur",
      "environment": "test",
      "is_active": true,
      "last_used_at": null,
      "expires_at": null,
      "created_at": "2026-11-02T08:00:00Z"
    }
  },
  "meta": {
    "request_id": "...",
    "timestamp": "..."
  }
}
```

| Code | Quand |
| --- | --- |
| 401 | Jeton de session absent ou expiré. |
| 422 | `environment` différent de `test` ou `live`, nom manquant. |

> **La valeur de `key` n'est visible qu'ici.** Copiez-la immédiatement.

- Lister vos clés : `GET /auth/keys`. Révoquer : `DELETE /auth/keys/{id}`.

### 5. Encaisser un client

Demande un paiement à un client : il reçoit une demande de confirmation sur son téléphone (code PIN Mobile Money). En-têtes : `Authorization: Bearer sk_…` et `X-Application-ID: 1`.

#### Encaisser un client

**POST** `/transactions` — Clé API + X-Application-ID

Corps de la requête :

```json
{
  "type": "collection",
  "provider": "orange_money_cm",
  "amount": 5000,
  "currency": "XAF",
  "phone_number": "696246686",
  "idempotency_key": "commande-1234",
  "metadata": {
    "description": "Commande 1234",
    "customer_name": "Nom du client"
  }
}
```

Exemples d'appel (recette) :

cURL :

```bash
curl -X POST 'https://dev-aggregator-api.kratospay.com/api/v1/transactions' \
  -H 'Authorization: Bearer sk_test_VOTRE_CLE' \
  -H 'X-Application-ID: 1' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -d '{"type":"collection","provider":"orange_money_cm","amount":5000,"currency":"XAF","phone_number":"696246686","idempotency_key":"commande-1234","metadata":{"description":"Commande 1234","customer_name":"Nom du client"}}'
```

JavaScript :

```javascript
// Côté serveur uniquement (Node.js) : la clé API ne doit jamais aller dans le navigateur.
const response = await fetch('https://dev-aggregator-api.kratospay.com/api/v1/transactions', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.KRATOSPAY_API_KEY}`,
    'X-Application-ID': '1',
    'Content-Type': 'application/json',
    'Accept': 'application/json',
  },
  body: JSON.stringify({
    "type": "collection",
    "provider": "orange_money_cm",
    "amount": 5000,
    "currency": "XAF",
    "phone_number": "696246686",
    "idempotency_key": "commande-1234",
    "metadata": {
      "description": "Commande 1234",
      "customer_name": "Nom du client"
    }
  }),
});

const result = await response.json();
if (!result.success) console.error(result.error.code, result.error.message);
```

PHP :

```php
$ch = curl_init('https://dev-aggregator-api.kratospay.com/api/v1/transactions');
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_HTTPHEADER => [
    'Authorization: Bearer ' . getenv('KRATOSPAY_API_KEY'),
    'X-Application-ID: 1',
    'Content-Type: application/json',
    'Accept: application/json',
  ],
  CURLOPT_POSTFIELDS => json_encode([
    'type' => 'collection',
    'provider' => 'orange_money_cm',
    'amount' => 5000,
    'currency' => 'XAF',
    'phone_number' => '696246686',
    'idempotency_key' => 'commande-1234',
    'metadata' => ['description' => 'Commande 1234', 'customer_name' => 'Nom du client'],
  ]),
  CURLOPT_RETURNTRANSFER => true,
]);
$result = json_decode(curl_exec($ch), true);
curl_close($ch);
```

Python :

```python
import os
import requests

response = requests.post(
    "https://dev-aggregator-api.kratospay.com/api/v1/transactions",
    headers={
        "Authorization": f"Bearer {os.environ['KRATOSPAY_API_KEY']}",
        "X-Application-ID": "1",
        "Accept": "application/json",
    },
    json={
      "type": "collection",
      "provider": "orange_money_cm",
      "amount": 5000,
      "currency": "XAF",
      "phone_number": "696246686",
      "idempotency_key": "commande-1234",
      "metadata": {
        "description": "Commande 1234",
        "customer_name": "Nom du client"
      }
    },
)
result = response.json()
print(response.status_code, result)
```

Réponse 201 :

```json
{
  "success": true,
  "data": {
    "uuid": "018f1a2b-3c4d-7e5f-a6b7-c8d9e0f1a2b3",
    "type": "collection",
    "provider": "orange_money",
    "status": "pending",
    "amount": "5000.00",
    "currency": "XAF",
    "reference": "MP240101.1234.A56789",
    "external_id": null,
    "phone_number": "696246686",
    "checkout_url": null,
    "metadata": {
      "description": "Commande 1234",
      "customer_name": "Nom du client"
    },
    "expires_at": null,
    "created_at": "2026-11-02T08:00:00Z",
    "updated_at": "2026-11-02T08:00:00Z"
  },
  "meta": {
    "request_id": "...",
    "timestamp": "..."
  }
}
```

#### Encaisser par carte bancaire

**POST** `/transactions` — Clé API + X-Application-ID

Pour Mastercard comme pour Visa, utilisez `provider: "mastercard"`. Les deux réseaux suivent ce même parcours : n'envoyez pas de numéro de téléphone ni de données de carte ; la réponse fournit `checkout_url`, à ouvrir pour que le client paie sur la page sécurisée. `return_url` est facultatif.

Corps de la requête :

```json
{
  "type": "collection",
  "provider": "mastercard",
  "amount": 25000,
  "currency": "XAF",
  "idempotency_key": "commande-5678",
  "return_url": "https://monsite.com/merci",
  "metadata": {
    "description": "Commande 5678"
  }
}
```

Réponse 201 :

```json
{
  "success": true,
  "data": {
    "uuid": "018f1a2b-3c4d-7e5f-a6b7-c8d9e0f1a2b4",
    "type": "collection",
    "provider": "mastercard",
    "status": "pending",
    "amount": "25000.00",
    "currency": "XAF",
    "reference": "K7H2M9QXP4R8T1W5Z3NB",
    "phone_number": null,
    "checkout_url": "https://master.kratos-travel.com/?sessionId=SESSION0002..."
  },
  "meta": {
    "request_id": "...",
    "timestamp": "..."
  }
}
```

| Code | Quand |
| --- | --- |
| 422 | Montant ou devise invalide, paiement par carte non activé sur le compte, ou passerelle momentanément indisponible. |

- Après paiement, le client revient sur `return_url`. Ne validez jamais la commande sur cette seule redirection : attendez `transaction.success` ou vérifiez par `GET /transactions/{uuid}`. Les statuts possibles sont `pending`, `success`, `failed` et `expired`.
- La carte ne prend pas en charge les décaissements ni les remboursements par API pour le moment.

#### Réponses possibles en cas d'échec

| Code | Quand |
| --- | --- |
| 401 | Clé API invalide, inactive, expirée, ou clé d'un autre environnement. |
| 422 | Montant invalide, réseau indisponible ou désactivé, numéro qui ne correspond pas au pays du réseau, `idempotency_key` déjà utilisée par une autre opération, ou paiement non initié (réessayer dans quelques instants). |

- Champs obligatoires : `type`, `provider`, `amount`, et `phone_number` pour une collecte. Autres types : `disbursement` (envoi), `airtime`, `tv`, `internet`.
- Le statut initial est **pending** : ce n'est pas un échec. Attendez le webhook ou consultez la transaction.
- Orange Money : renseignez `metadata.customer_name` si vous prévoyez des remboursements.
- En test (`sk_test_`), aucun argent ne bouge et le dénouement est simulé.

### 6. Suivre et annuler

Consultez l'état d'une transaction en complément du webhook (par exemple après une coupure réseau de votre côté), ou annulez-la tant qu'elle est en attente.

#### Suivre le paiement

**GET** `/transactions/{uuid}` — Clé API + X-Application-ID

Retourne l'état courant. Liste filtrable : `GET /transactions?status=success&type=collection&date_from=...&per_page=20`.

Réponse 200 :

```json
{
  "success": true,
  "data": {
    "uuid": "018f1a2b-3c4d-7e5f-a6b7-c8d9e0f1a2b3",
    "type": "collection",
    "provider": "orange_money",
    "status": "success",
    "amount": "5000.00",
    "currency": "XAF",
    "reference": "MP240101.1234.A56789",
    "external_id": null,
    "phone_number": "696246686",
    "checkout_url": null,
    "metadata": {
      "description": "Commande 1234",
      "customer_name": "Nom du client"
    },
    "expires_at": null,
    "created_at": "2026-11-02T08:00:00Z",
    "updated_at": "2026-11-02T08:00:00Z"
  },
  "meta": {
    "request_id": "...",
    "timestamp": "..."
  }
}
```

| Code | Quand |
| --- | --- |
| 401 | Clé API invalide. |
| 404 | Transaction inconnue ou appartenant à un autre compte. |

Statuts possibles : voir l'annexe **Statuts d'une transaction**.

#### Annuler un paiement en attente

**POST** `/transactions/{uuid}/cancel` — Clé API + X-Application-ID

Annule une transaction encore `pending`. Déclenche le webhook `transaction.cancelled`. La réponse a la même structure qu'une transaction, avec le statut mis à jour.

| Code | Quand |
| --- | --- |
| 404 | Transaction introuvable. |
| 422 | Transaction déjà terminée : elle ne peut plus être annulée. |

### 7. Liens de paiement

Encaissez sans coder : chaque particulier dispose d'un lien permanent à montant libre et d'un QR code. Vous pouvez aussi créer des liens à montant fixe. Le client paie sur la page hébergée par Kratos Pay, par Mobile Money ou par carte si le moyen est autorisé.

#### Lien permanent

**GET** `/payment-links/static` — Jeton de session

Retourne votre lien permanent : `public_url`, `qr_code_svg`, `qr_code_url`.

#### Créer un lien pour une cause précise

**POST** `/personal/payment-links` — Jeton de session

Pour une cagnotte, une facture… Autorisez les moyens de paiement voulus dans `allowed_providers`.

Corps de la requête :

```json
{
  "title": "Cagnotte anniversaire",
  "description": "Participation au cadeau",
  "amount": 5000,
  "currency": "XAF",
  "is_amount_fixed": true,
  "allowed_providers": [
    "orange_money_cm",
    "mtn_momo_cm"
  ],
  "redirect_url": "https://monsite.com/merci",
  "expires_at": "2026-12-31T23:59:59Z",
  "max_uses": 100
}
```

Réponse 201 (extrait) :

```json
{
  "success": true,
  "data": {
    "uuid": "b2c3d4e5-...",
    "slug": "cagnotte-anniversaire-xkf72a",
    "title": "Cagnotte anniversaire",
    "amount": "5000.00",
    "currency": "XAF",
    "is_amount_fixed": true,
    "status": "active",
    "use_count": 0,
    "max_uses": 100,
    "public_url": "https://payment.kratospay.com/cagnotte-anniversaire-xkf72a",
    "images": []
  },
  "meta": {
    "request_id": "...",
    "timestamp": "..."
  }
}
```

- États d'un lien (`status`) : **active**, **disabled**, **expired**, **max_uses_reached**. Liste : `GET /personal/payment-links`.
- Erreurs : 400/422 si le montant manque pour un lien fixe, ou si un réseau de `allowed_providers` n'existe pas.

#### Ce que fait la page de paiement

Pour information, ou si vous créez votre propre page de paiement :

| Route | Rôle | Réponses |
| --- | --- | --- |
| `GET /public/payment-links/{slug}` | Affiche le lien. Répond 200 même si le lien est indisponible (`is_available: false` + `unavailable_reason` : disabled, expired ou max_uses_reached). | 200, 404 si slug inconnu |
| `POST /public/payment-links/{slug}/transactions` | Crée le paiement du client (`provider`, `phone_number`, `amount`, `idempotency_key`). `amount` n'est obligatoire que si le lien est à montant libre. | 201 (pending), 404, 422 (lien indisponible, réseau non autorisé, montant manquant) |
| `GET /public/transactions/{uuid}/status` | Attente du résultat : reste ouverte jusqu'à 20 s tant que le statut est `pending` ; à rappeler immédiatement. | 200 : `{ uuid, status, amount, currency, provider }` |

### 8. Solde et retraits

Consultez votre solde puis retirez votre argent vers un compte Mobile Money ou, si un IBAN valide est enregistré dans votre profil, par virement bancaire.

#### Consulter son solde

**GET** `/merchant/wallet` — Jeton de session

Solde disponible par devise, toutes applications confondues. N'additionnez jamais deux devises. Paramètre facultatif : `environment=live|test`.

Réponse 200 :

```json
{
  "success": true,
  "data": {
    "balances": {
      "XAF": "980000.00"
    },
    "applications_without_activity": []
  },
  "meta": {
    "request_id": "...",
    "timestamp": "..."
  }
}
```

| Code | Quand |
| --- | --- |
| 401 | Jeton expiré. |
| 503 | Comptabilité momentanément indisponible (`LEDGER_MODULE_UNAVAILABLE`) : réessayer. |

#### Vérifier le titulaire du numéro

**POST** `/withdrawals/account-inquiry` — Jeton de session

Avant un retrait, vérifiez le titulaire du numéro destinataire. Aucun montant n'est engagé. `operator` vaut `orange` ou `mtn`.

Corps de la requête :

```json
{
  "operator": "orange",
  "phone_number": "696246686"
}
```

Réponse 200 :

```json
{
  "success": true,
  "data": {
    "operator": "orange",
    "phone_number": "696246686",
    "first_name": "PRENOM",
    "last_name": "NOM",
    "full_name": "PRENOM NOM"
  },
  "meta": {
    "request_id": "...",
    "timestamp": "..."
  }
}
```

| Code | Quand |
| --- | --- |
| 404 | `ACCOUNT_NOT_FOUND` : numéro inconnu chez l'opérateur. |
| 422 | `operator` ou `phone_number` invalide. |
| 502 | `GIMAC_UNAVAILABLE` : vérification momentanément indisponible, réessayer. |

#### Lancer le retrait

**POST** `/withdrawals` — Jeton de session

Le montant est retenu immédiatement sur votre solde, puis envoyé au réseau. Pour un particulier, `application_id` est facultatif (votre application personnelle est utilisée).

Corps de la requête :

```json
{
  "amount": 15000,
  "destination_provider": "orange_money_cm",
  "destination_phone_number": "696246686",
  "currency": "XAF"
}
```

Réponse 201 :

```json
{
  "success": true,
  "data": {
    "uuid": "...",
    "application_id": 1,
    "destination_provider": "orange_money",
    "destination_phone_number": "696246686",
    "amount": 15000,
    "currency": "XAF",
    "status": "pending",
    "failure_reason": null,
    "reference": "OM-9f3a1",
    "created_at": "2026-11-02T12:30:00Z"
  },
  "meta": {
    "request_id": "...",
    "timestamp": "..."
  }
}
```

| Code | Quand |
| --- | --- |
| 422 | Solde insuffisant (`INSUFFICIENT_BALANCE`), réseau indisponible, données invalides. |

- Statut initial : **pending** (ou success / failed selon la réponse immédiate du réseau). Suivi : `GET /withdrawals/{uuid}` ; historique : `GET /withdrawals`.
- Si le retrait échoue, le montant retenu est restitué à votre solde.

#### Retirer par virement bancaire

**POST** `/withdrawals` — Jeton de session

Ajoutez `destination_type: "bank"`. Le virement est envoyé à l'IBAN enregistré dans votre profil par le réseau GIMAC ; la banque est déduite de l'IBAN. Aucun fournisseur ni numéro de téléphone n'est envoyé dans la requête.

Corps de la requête :

```json
{
  "amount": 150000,
  "destination_type": "bank",
  "currency": "XAF"
}
```

Réponse 201 :

```json
{
  "success": true,
  "data": {
    "uuid": "018f1a2b-...",
    "application_id": 1,
    "destination_provider": "gimac",
    "destination_phone_number": null,
    "destination_bank": {
      "bank_name": "BICEC",
      "iban": "CM21 1000 5000 ...... 0117"
    },
    "amount": 150000,
    "currency": "XAF",
    "status": "pending",
    "failure_reason": null,
    "reference": "KP00000000000000001",
    "created_at": "2026-11-11T12:30:00Z"
  },
  "meta": {
    "request_id": "...",
    "timestamp": "..."
  }
}
```

| Code | Quand |
| --- | --- |
| 422 | IBAN absent ou invalide, banque non participante au réseau GIMAC, devise différente de XAF, ou solde insuffisant. |

- Conditions : IBAN CEMAC valide (27 caractères et clé de contrôle correcte), banque participante active, devise `XAF`. Le montant est retenu dès le lancement ; un échec certain est restitué, tandis qu'un incident réseau peut nécessiter un contrôle.
- Le retrait peut rester `pending` plus longtemps qu'un retrait Mobile Money. Le numéro complet d'IBAN est masqué dans `destination_bank.iban`. Pour modifier l'IBAN, contactez le support.

### 9. Rembourser

Remboursez intégralement une transaction réussie. L'opération est irréversible et possible une seule fois par transaction.

#### Rembourser un client

**POST** `/transactions/{uuid}/refund` — Clé API + X-Application-ID

Corps de la requête :

```json
{
  "reason": "Commande annulée par le client."
}
```

Réponse 201 :

```json
{
  "success": true,
  "data": {
    "transaction_uuid": "018f1a2b-...",
    "amount": "5000.00",
    "currency": "XAF",
    "reason": "Commande annulée par le client.",
    "status": "success",
    "processed_at": "2026-11-02T09:00:00Z",
    "created_at": "2026-11-02T09:00:00Z"
  },
  "meta": {
    "request_id": "...",
    "timestamp": "..."
  }
}
```

| Code | Quand |
| --- | --- |
| 404 | Transaction introuvable. |
| 422 | Transaction non remboursable : statut différent de `success`, déjà remboursée, ou réseau ne prenant pas en charge les remboursements (seul Orange Money les prend en charge). |

- Consulter un remboursement : `GET /transactions/{uuid}/refund`. Déclenche le webhook `transaction.refunded`.

### Droits et limites

Ce que le compte particulier peut faire, et ce qui lui est refusé.

#### Droits et limites du particulier

- Une clé `sk_live_` de l'application personnelle encaisse en production **sans validation KYC préalable**. Si la création d'un paiement répond 422 avec une erreur sur le champ `kyc`, le compte doit d'abord être validé (voir Marchand entreprise, étape 3).
- Certaines fonctions sont réservées aux entreprises (gestion d'équipe, retraits programmés, paiements de masse) : elles répondent 403 à un compte particulier.
- Connexion par téléphone uniquement (pas par e-mail et mot de passe).

## Marchand entreprise

### 1. Créer le compte

Le marchand entreprise encaisse pour son activité, avec plusieurs applications, une équipe, des retraits programmés et des paiements de masse. Différences majeures avec le particulier : connexion par **e-mail + mot de passe + TOTP obligatoire**, **validation KYC** avant le mode live, plusieurs applications.

#### Créer le compte entreprise

**POST** `/auth/register` — Public

Crée le compte et son profil société. Une application est créée automatiquement au nom de l'entreprise. Le compte est soumis à validation KYC avant tout paiement réel.

Corps de la requête :

```json
{
  "name": "Nom Prénom",
  "merchant_type": "business",
  "email": "contact@mon-entreprise.cm",
  "password": "<mot-de-passe>",
  "password_confirmation": "<mot-de-passe>",
  "phone": "696246686",
  "business_name": "Mon Entreprise SARL",
  "legal_form": "SARL",
  "rccm_number": "RC/DLA/2021/B/12345",
  "niu": "M012345678901B",
  "manager_name": "Nom Prénom",
  "address": "Rue de la Paix, Douala",
  "iban": "CM2110003001000123456789012",
  "bank_account_holder": "Mon Entreprise SARL",
  "mobile_money_number": "696246686",
  "activity_type": "commerce",
  "country_id": 1
}
```

Réponse 201 :

```json
{
  "success": true,
  "data": {
    "id": 2,
    "name": "Nom Prénom",
    "email": "contact@mon-entreprise.cm",
    "merchant_type": "business",
    "kyc_status": "none",
    "is_active": true,
    "created_at": "2026-11-02T08:00:00Z"
  },
  "meta": {
    "request_id": "...",
    "timestamp": "..."
  }
}
```

| Code | Quand |
| --- | --- |
| 422 | E-mail déjà utilisé, mot de passe trop court, confirmation différente, champ invalide (NIU de plus de 15 caractères, `activity_type` hors liste…). |

- `activity_type` accepte : commerce, restauration, transport, immobilier, sante, education, technologie, agriculture, services_financiers, tourisme, btp, autre.

### 2. Se connecter (TOTP)

L'entreprise se connecte par e-mail et mot de passe. Le TOTP (Google Authenticator, Authy…) est obligatoire : aucun jeton complet n'est jamais renvoyé directement, la réponse indique l'étape suivante.

#### Se connecter

**POST** `/auth/login` — Public

Corps de la requête :

```json
{
  "email": "contact@mon-entreprise.cm",
  "password": "<mot-de-passe>"
}
```

Cas 1 : TOTP pas encore configuré (première connexion) :

```json
{
  "success": true,
  "data": {
    "requires_totp_setup": true,
    "setup_token": "..."
  },
  "meta": {
    "request_id": "...",
    "timestamp": "..."
  }
}
```

Cas 2 : TOTP déjà configuré :

```json
{
  "success": true,
  "data": {
    "requires_totp": true,
    "challenge_token": "550e8400-e29b-41d4-a716-446655440000"
  },
  "meta": {
    "request_id": "...",
    "timestamp": "..."
  }
}
```

| Code | Quand |
| --- | --- |
| 422 | Identifiants incorrects, compte désactivé, ou compte particulier (utiliser la connexion par téléphone). |

- Mot de passe oublié : `POST /auth/password/forgot` puis `POST /auth/password/reset`.

#### Première connexion : activer le TOTP

**POST** `/auth/totp/setup` — setup_token (Bearer)

Avec `Authorization: Bearer {setup_token}` (valable 15 min). Renvoie le secret à scanner dans Google Authenticator ou Authy.

Réponse 200 :

```json
{
  "success": true,
  "data": {
    "secret": "JBSWY3DPEHPK3PXP",
    "qr_url": "otpauth://totp/KratosPay:...",
    "manual_entry_key": "JBSWY3DPEHPK3PXP"
  },
  "meta": {
    "request_id": "...",
    "timestamp": "..."
  }
}
```

| Code | Quand |
| --- | --- |
| 401 | `setup_token` expiré : se reconnecter. |

#### Confirmer le TOTP et recevoir le jeton

**POST** `/auth/totp/confirm` — setup_token (Bearer)

Saisissez le premier code à 6 chiffres généré par l'application. La configuration expire après 10 minutes.

Corps de la requête :

```json
{
  "code": "123456"
}
```

Réponse 200 :

```json
{
  "success": true,
  "data": {
    "merchant": {
      "...": "..."
    },
    "token": "<jeton-de-session>",
    "token_type": "Bearer",
    "recovery_codes": [
      "XXXX-XXXX-XXXX",
      "... 10 codes au total"
    ]
  },
  "meta": {
    "request_id": "...",
    "timestamp": "..."
  }
}
```

| Code | Quand |
| --- | --- |
| 422 | Code invalide ou session expirée. |

> **Conservez les 10 codes de récupération en lieu sûr : ils ne seront plus jamais affichés.** Chacun remplace un code TOTP, à usage unique.

#### Connexions suivantes : répondre au défi TOTP

**POST** `/auth/totp/challenge` — Public

À utiliser après une connexion qui a renvoyé `requires_totp`. Le `challenge_token` est à usage unique, valable 5 minutes.

Corps de la requête :

```json
{
  "challenge_token": "550e8400-e29b-41d4-a716-446655440000",
  "code": "123456"
}
```

Réponse 200 :

```json
{
  "success": true,
  "data": {
    "requires_totp": false,
    "merchant": {
      "...": "..."
    },
    "token": "<jeton-de-session>",
    "token_type": "Bearer"
  },
  "meta": {
    "request_id": "...",
    "timestamp": "..."
  }
}
```

| Code | Quand |
| --- | --- |
| 422 | Code invalide, challenge expiré ou déjà consommé (un code de récupération `XXXX-XXXX-XXXX` est aussi accepté). |

### 3. Valider le compte (KYC)

Sans KYC validé, l'entreprise reste en **test** : les clés `sk_live_` n'encaissent pas (422 « compte non validé KYC »). Le KYC se déroule en trois appels par document, les fichiers partant directement vers le stockage sécurisé.

#### État du dossier

**GET** `/kyc` — Jeton de session

État du dossier et pièces encore manquantes.

Réponse 200 :

```json
{
  "success": true,
  "data": {
    "status": "none",
    "status_label": "Non vérifié",
    "has_live_access": false,
    "can_submit_documents": true,
    "requirements": {
      "identity": [
        "id_front",
        "passport"
      ],
      "...": "..."
    },
    "missing_groups": [
      "identity",
      "niu"
    ],
    "documents": []
  },
  "meta": {
    "request_id": "...",
    "timestamp": "..."
  }
}
```

Statuts du compte : **none** (rien soumis), **pending** (en cours de vérification), **verified** (accès live), **suspended**. La liste exacte des pièces exigées est dans `requirements` / `missing_groups`.

#### Envoyer une pièce (3 appels)

**POST** `/kyc/upload-url` — Jeton de session

1. Demande une adresse d'envoi temporaire (valable 15 min) pour une pièce.

Corps de la requête :

```json
{
  "document_type": "business_registration",
  "content_type": "application/pdf"
}
```

Réponse 200 :

```json
{
  "success": true,
  "data": {
    "upload_url": "https://...",
    "document_key": "kyc/2/business_registration/550e...pdf",
    "expires_in": 900
  },
  "meta": {
    "request_id": "...",
    "timestamp": "..."
  }
}
```

2. Envoyez le fichier : `PUT {upload_url}` avec le même `Content-Type`, le corps étant le fichier.

**POST** `/kyc/documents` — Jeton de session

3. Confirmez l'envoi :

Corps de la requête :

```json
{
  "document_type": "business_registration",
  "document_key": "kyc/2/business_registration/550e...pdf",
  "expires_at": "2030-05-14"
}
```

Réponse 201 :

```json
{
  "success": true,
  "data": {
    "document_type": "business_registration",
    "document_type_label": "Registre de commerce",
    "status": "pending",
    "status_label": "En attente de vérification",
    "rejection_reason": null,
    "requires_expiry": false,
    "created_at": "2026-11-02T14:30:00Z"
  },
  "meta": {
    "request_id": "...",
    "timestamp": "..."
  }
}
```

#### Règles des pièces

- Types de pièces : `id_front`, `id_back`, `passport`, `location_plan`, `niu_certificate` (particulier) ; `business_registration`, `tax_compliance`, `manager_id_front`, `manager_id_back`, `manager_passport` (entreprise) ; `merchant_contract`, `bank_domiciliation_certificate` (communs) ; `address_proof`, `residence_proof`, `additional_financial_documents` (facultatifs).
- Formats : `image/jpeg`, `image/png`, `application/pdf`. `expires_at` n'est demandé que pour les pièces d'identité (`requires_expiry: true`).
- État de chaque pièce : **pending**, **approved**, **rejected** (avec `rejection_reason`). Une pièce approuvée ne peut pas être écrasée ; une pièce en attente est remplacée par un nouvel envoi. Supprimer une pièce en attente : `DELETE /kyc/documents/{id}`.
- Erreurs : 422 si le type ou le format est invalide, si le fichier n'a pas été envoyé avant la confirmation, ou si la pièce est déjà approuvée.

### 4. Créer une application

Une application = une intégration distincte (site, application mobile, terminal). Chacune a ses clés, son webhook, son solde et ses liens de paiement. Son identifiant se passe dans `X-Application-ID`.

#### Créer une application

**POST** `/applications` — Jeton de session

Corps de la requête :

```json
{
  "name": "Boutique en ligne",
  "webhook_url": "https://mon-site.cm/webhooks/kratospay",
  "country_id": 1
}
```

Réponse 201 :

```json
{
  "success": true,
  "data": {
    "application": {
      "id": 12,
      "name": "Boutique en ligne",
      "webhook_url": "https://mon-site.cm/webhooks/kratospay",
      "has_webhook": true,
      "is_active": true,
      "created_at": "2026-11-02T08:00:00Z"
    },
    "webhook_secret": "whsec_<secret>"
  },
  "meta": {
    "request_id": "...",
    "timestamp": "..."
  }
}
```

| Code | Quand |
| --- | --- |
| 401 | Jeton expiré. |
| 422 | Nom manquant ou URL de webhook invalide. |

- **`webhook_secret` n'est affiché qu'une fois**, même sans `webhook_url`. Le régénérer : `POST /applications/{id}/webhook/rotate-secret`.
- Gérer : `GET /applications`, `GET /applications/{id}`, `PATCH /applications/{id}` (`name`, `webhook_url`, `is_active`), `DELETE /applications/{id}`. Pays ayant transigé : `GET /applications/{id}/countries`.

### 5. Générer ses clés API

Même appel que pour le particulier : une clé `test` pour développer, une clé `live` une fois le KYC validé. La valeur n'est affichée qu'une fois.

#### Générer ses clés API

**POST** `/auth/keys` — Jeton de session

Corps de la requête :

```json
{
  "name": "Serveur boutique",
  "environment": "live"
}
```

Réponse 201 :

```json
{
  "success": true,
  "data": {
    "key": "sk_live_<votre-cle>",
    "meta": {
      "id": 7,
      "environment": "live",
      "...": "..."
    }
  },
  "meta": {
    "request_id": "...",
    "timestamp": "..."
  }
}
```

| Code | Quand |
| --- | --- |
| 422 | `environment` ou nom invalide. |

Détails complets : Marchand particulier, étape 4.

### 6. Encaisser et envoyer

Les appels sont identiques à ceux du particulier (encaisser, suivre, annuler, rembourser) : `POST /transactions`, `GET /transactions`, `GET /transactions/{uuid}`, `POST /transactions/{uuid}/cancel`, `POST /transactions/{uuid}/refund`, avec `Authorization: Bearer sk_live_…` et `X-Application-ID: {id}`.

#### Spécificités entreprise

- **`X-Application-ID` est indispensable** : choisissez l'application à laquelle rattacher l'opération.
- Tant que le KYC n'est pas validé, un paiement avec une clé live est refusé en 422 (erreur `kyc`).
- Envoi d'argent depuis le solde : `POST /transactions` avec `"type": "disbursement"`. En production, le montant est retenu sur le solde avant l'appel au réseau ; solde insuffisant = 422 `INSUFFICIENT_BALANCE`, rien n'est créé. En test, aucun solde n'est requis.
- Filtres de liste : `status`, `type`, `provider`, `date_from`, `date_to`, `amount_min`, `amount_max`, `application_id`, `per_page`. Réponse : `data.items` + `data.pagination`.
- Depuis le dashboard (sans clé API) : `GET /merchant/transactions` et `GET /merchant/transactions/{uuid}` avec le jeton de session.
- Export CSV asynchrone : `POST /transactions/export`, puis `GET /transactions/exports/{uuid}` jusqu'à « prêt », et téléchargement par le lien reçu par e-mail.
- Carte bancaire : utilisez `provider: "mastercard"` pour Mastercard comme pour Visa, sans `phone_number`; la création renvoie un `checkout_url` sécurisé. Le KYC de l'entreprise doit être validé avant les paiements live.

Corps, réponses et erreurs : Marchand particulier, étapes 5, 6 et 9.

#### Envoyer de l'argent (disbursement)

**POST** `/transactions` — Clé API + X-Application-ID

Envoie un montant depuis le solde de l'application vers le portefeuille Mobile Money du bénéficiaire. `phone_number` est requis, sans indicatif pays, et doit correspondre au pays de `provider`. En production, le montant est réservé avant l'appel au réseau ; un solde insuffisant renvoie 422 `INSUFFICIENT_BALANCE`. Utilisez une `idempotency_key` unique et conservez l'`uuid` de la réponse.

Corps de la requête :

```json
{
  "type": "disbursement",
  "provider": "mtn_momo_cm",
  "amount": 2000,
  "currency": "XAF",
  "phone_number": "677895412",
  "idempotency_key": "paiement-fournisseur-42"
}
```

Réponse 201 :

```json
{
  "success": true,
  "data": {
    "uuid": "018f1a2b-3c4d-7e5f-a6b7-c8d9e0f1a2b4",
    "type": "disbursement",
    "provider": "mtn_momo",
    "status": "pending",
    "amount": "2000.00",
    "currency": "XAF",
    "reference": "MP240101.1235.B12345",
    "phone_number": "677895412",
    "created_at": "2026-11-02T08:05:00Z",
    "updated_at": "2026-11-02T08:05:00Z"
  },
  "meta": {
    "request_id": "...",
    "timestamp": "..."
  }
}
```

| Code | Quand |
| --- | --- |
| 422 | `INSUFFICIENT_BALANCE`, montant/devise invalide, réseau indisponible ou numéro non conforme au pays de l'opérateur. |
| 401 | Clé API invalide, inactive ou d'un autre environnement. |

- Le statut initial est généralement `pending`. Attendez le webhook `transaction.success` ou `transaction.failed`, ou consultez `GET /transactions/{uuid}` ; ne déduisez pas le succès de la seule réponse de création.
- En test (`sk_test_`), aucun solde réel n'est requis et aucun fonds ne bouge.

### 7. Liens de paiement

Pour encaisser sans développement. Même page publique que pour le particulier, avec Mobile Money ou carte selon les moyens autorisés ; la création est rattachée à une application.

#### Créer un lien de paiement

**POST** `/payment-links` — Jeton de session

Corps de la requête :

```json
{
  "application_id": 12,
  "title": "Abonnement mensuel",
  "description": "Accès illimité pendant 30 jours",
  "amount": 5000,
  "currency": "XAF",
  "is_amount_fixed": true,
  "allowed_providers": [
    "orange_money_cm",
    "mtn_momo_cm"
  ],
  "redirect_url": "https://monsite.com/merci",
  "expires_at": "2026-12-31T23:59:59Z",
  "max_uses": 100,
  "slug": "abonnement-mensuel"
}
```

Réponse 201 :

```json
{
  "success": true,
  "data": {
    "uuid": "b2c3d4e5-...",
    "slug": "abonnement-mensuel",
    "title": "Abonnement mensuel",
    "amount": "5000.00",
    "currency": "XAF",
    "is_amount_fixed": true,
    "status": "active",
    "use_count": 0,
    "public_url": "https://payment.kratospay.com/abonnement-mensuel",
    "images": []
  },
  "meta": {
    "request_id": "...",
    "timestamp": "..."
  }
}
```

- Erreurs : 400 (montant manquant pour un lien fixe), 422 (application inconnue ou d'un autre compte, réseau inexistant dans `allowed_providers`).
- Autres routes : `GET /payment-links` (liste), `GET` / `PATCH` / `DELETE /payment-links/{uuid}`, `GET /payment-links/static` (lien permanent + QR), galerie d'images en deux temps : `POST /payment-links/{uuid}/images/upload-url` puis `POST /payment-links/{uuid}/images/confirm`.
- Un paiement par lien exige que l'entreprise soit validée KYC : sinon 422 à la création du paiement.

### 8. Solde et retraits

Solde : `GET /merchant/wallet` (global par devise) et `GET /applications/{id}/balance` (par application). Détails et réponses : Marchand particulier, étape 8.

#### Retrait instantané

**POST** `/withdrawals` — Jeton de session

Pour une entreprise, `application_id` est **obligatoire**. Même vérification préalable (`POST /withdrawals/account-inquiry`), mêmes corps et réponses que pour le particulier. Historique : `GET /withdrawals`.

- Pour un virement bancaire, ajoutez `destination_type: "bank"` et l'`application_id`. L'IBAN du profil entreprise est utilisé ; consultez la section « Retirer par virement bancaire » du parcours particulier pour les conditions, exemples et erreurs.

#### Retrait programmé

**POST** `/scheduled-withdrawals` — Jeton de session

Réservé aux entreprises : le montant est retenu au déclenchement de chaque occurrence.

Corps de la requête :

```json
{
  "application_id": 12,
  "amount": 15000,
  "destination_provider": "orange_money_cm",
  "destination_phone_number": "696246686",
  "frequency": "monthly",
  "day_of_month": 1,
  "time_of_day": "12:30",
  "starts_from": "2026-12-01",
  "currency": "XAF"
}
```

Réponse 201 (extrait) :

```json
{
  "success": true,
  "data": {
    "application_id": 12,
    "amount": 15000,
    "frequency": "monthly",
    "day_of_month": 1,
    "time_of_day": "12:30:00",
    "next_execution_at": "2026-12-01T12:30:00Z",
    "is_active": true,
    "status": "active",
    "currency": "XAF"
  },
  "meta": {
    "request_id": "...",
    "timestamp": "..."
  }
}
```

- `frequency` : `once`, `daily`, `weekly` (exige `day_of_week`, 1 = lundi à 7 = dimanche), `monthly` (exige `day_of_month`, 1 à 31 ; ramené au dernier jour d'un mois plus court).
- Statuts : active, paused, completed (fréquence `once` terminée), cancelled. Modifier / mettre en pause : `PATCH /scheduled-withdrawals/{id}`. Annuler : `DELETE /scheduled-withdrawals/{id}`. Historique des occurrences : `GET /scheduled-withdrawals/executions`.
- Erreurs : 403 (compte non entreprise), 422 (données invalides).

### 9. Paiements de masse

Salaires, fournisseurs, remboursements groupés. Trois temps : (1) créer le lot (aucun argent ne bouge), (2) le déclencher, (3) suivre son avancement.

#### Créer un lot

**POST** `/mass-payments` — Jeton de session

Corps de la requête :

```json
{
  "application_id": 12,
  "reference": "Salaire_Juin_2026",
  "currency": "XAF",
  "beneficiaries": [
    {
      "nom": "Bénéficiaire 1",
      "telephone": "691028372",
      "montant": 10000,
      "provider": "orange_money_cm"
    },
    {
      "nom": "Bénéficiaire 2",
      "telephone": "677895412",
      "montant": 10000,
      "provider": "mtn_momo_cm"
    }
  ]
}
```

Réponse 201 (extrait) :

```json
{
  "success": true,
  "data": {
    "uuid": "...",
    "reference": "Salaire_Juin_2026",
    "currency": "XAF",
    "total_amount": 20000,
    "beneficiary_count": 2,
    "statut": "en_attente",
    "beneficiaries": [
      "..."
    ]
  },
  "meta": {
    "request_id": "...",
    "timestamp": "..."
  }
}
```

#### Déclencher et suivre

- **Tout ou rien** : un seul bénéficiaire invalide et rien n'est enregistré (422 avec les erreurs indexées par bénéficiaire).
- Import depuis un fichier : `POST /mass-payments/preview-csv` renvoie un aperçu avant création.
- `POST /mass-payments/{uuid}/trigger` (202) lance l'exécution en arrière-plan, une seule fois par lot (422 si déjà déclenché). Statut : `en_attente` → `en_cours` → `valide` / `partiellement_reussi` / `echoue`.
- Suivi : `GET /mass-payments/{uuid}` ; liste : `GET /mass-payments`.

### 10. Équipe et autres modules

Invitez des collaborateurs avec des droits limités, et découvrez les autres modules disponibles pour les entreprises.

#### Inviter un collaborateur

**POST** `/merchant/team/members` — Jeton de session

Le collaborateur reçoit un e-mail pour définir son mot de passe (aucun mot de passe n'est transmis en clair).

Corps de la requête :

```json
{
  "name": "Nom Prénom",
  "email": "collaborateur@exemple.cm",
  "role_id": 3,
  "application_ids": [
    12
  ]
}
```

`role_id` absent : le membre reste en attente de configuration. `application_ids` absent ou `null` : accès à toutes les applications. Erreurs : 422 (plafond de membres atteint, membre déjà actif).

| Route | Rôle |
| --- | --- |
| `GET /merchant/team/members` | Lister les membres |
| `PATCH /merchant/team/members/{uuid}` | Changer le profil ou les applications accessibles |
| `POST /merchant/team/members/{uuid}/resend-invitation` | Renvoyer l'invitation |
| `DELETE /merchant/team/members/{uuid}` | Retirer un membre |
| `GET /merchant/team/roles`, `GET /merchant/team/permissions` | Catalogue des profils et des permissions |
| `POST /merchant/team/roles`, `PATCH` / `DELETE /merchant/team/roles/{id}`, `POST /merchant/team/roles/{id}/fork` | Créer, modifier, archiver ou dériver un profil personnalisé |

#### Autres modules disponibles

Documentation détaillée sur demande.

| Module | Routes principales |
| --- | --- |
| Suivi du tableau de bord | `GET /merchant/insights`, `/merchant/insights/timeseries`, `/merchant/insights/payment-links`, `/merchant/insights/flows` ; `GET /merchant/activity-logs` |
| Fournisseurs | `GET` / `POST /fournisseurs`, `GET` / `PUT` / `DELETE /fournisseurs/{id}`, activate / deactivate, `GET` / `POST /fournisseurs/{id}/payments` |
| Conversion de devises | `GET /exchange-rates`, `POST /conversions/quote`, `POST /conversions/{uuid}/execute`, `GET /conversions` |
| Parrainage | `GET /merchant/referral-code`, `GET /merchant/referral/commissions`, `POST /merchant/commissions/transfer`, `POST /merchant/commissions/withdraw` |
| Annonces | `GET /announcements`, `POST /announcements/{id}/read`, `POST /announcements/read-all` |
| Référentiels | `GET /countries`, `/currencies`, `/activity-types`, `/payment-providers` |

## Encaissement par QR GIMAC

### Code marchand et points QR

Le QR GIMAC permet aux clients de la zone CEMAC de payer en scannant un code marchand depuis l'application de leur banque ou opérateur. Les routes de points QR permettent de créer un code distinct pour chaque caisse, station, agence ou pompiste.

#### Parcours de paiement

- Le client scanne le QR, saisit le montant en XAF puis valide dans son application de paiement. GIMAC interroge Kratos Pay en temps réel.
- Si le code marchand, le compte et le point sont actifs et que le KYC requis est validé, le paiement est accepté immédiatement : la transaction est créée au statut `success`, le solde est crédité et le webhook est envoyé.
- En cas de refus, le client n'est pas débité. Le paiement peut venir d'un autre pays de la zone CEMAC ; le montant reste en `XAF`.
- Les paiements QR sont statiques : le client saisit le montant. Un compte possède déjà un code marchand attribué automatiquement ; demandez au support sa carte imprimable « Scannez & Payez ».

#### Routes des points QR

| Route | Rôle |
| --- | --- |
| `POST /gimac/qr-points` | Créer un point avec son code marchand, `qr_payload` et `qr_svg`. |
| `GET /gimac/qr-points` | Lister les points ; filtres facultatifs `external_ref` et `is_active`. L'image QR n'est pas incluse. |
| `GET /gimac/qr-points/{uuid}` | Lire un point et son image `qr_svg`. |
| `PATCH /gimac/qr-points/{uuid}` | Modifier le nom, la ville ou l'application, ou activer/désactiver le point. Le code reste inchangé. |
| `GET /gimac/qr-points/{uuid}/card` | Ouvrir la carte A5 imprimable avec le nom, le code et le QR. |

Pour les routes ci-dessus, utilisez une clé API et `X-Application-ID`. Les mêmes opérations sont disponibles depuis le dashboard avec un jeton de session sous `/merchant/gimac/qr-points`.

#### Créer un point QR

**POST** `/gimac/qr-points` — Clé API + X-Application-ID

Crée un code pour une station, un pompiste, une agence ou une caisse. Les paiements sont crédités au solde de l'application indiquée, ou à celle de `X-Application-ID` si `application_id` n'est pas fourni.

Corps de la requête :

```json
{
  "name": "STATION TOTAL BONAMOUSSADI",
  "city": "Douala",
  "external_ref": "station-42",
  "application_id": 12
}
```

Réponse 201 :

```json
{
  "success": true,
  "data": {
    "uuid": "0190b6f0-7a52-7e0a-9b5c-2f6d1c3a8e44",
    "code": "612345",
    "name": "STATION TOTAL BONAMOUSSADI",
    "city": "Douala",
    "external_ref": "station-42",
    "application_id": 12,
    "is_active": true,
    "qr_payload": "00020101021135370014A000000656101001051402302066123455204...",
    "qr_svg": "<svg>…</svg>",
    "created_at": "2026-11-11T09:00:00Z",
    "updated_at": "2026-11-11T09:00:00Z"
  },
  "meta": {
    "request_id": "...",
    "timestamp": "..."
  }
}
```

| Code | Quand |
| --- | --- |
| 200 | `external_ref` existe déjà : le point existant est renvoyé (création idempotente). |
| 401 | Clé API invalide. |
| 422 | `name` manquant ou supérieur à 25 caractères, `city` supérieur à 15 caractères, ou `application_id` appartenant à un autre compte. |

- `external_ref` est votre identifiant métier stable ; renvoyez la même valeur après un timeout pour éviter un doublon. Le code marchand à 6 chiffres ne change pas.
- `name` (25 caractères max.) et `city` (15 max.) sont imprimés dans le QR. Les accents sont retirés ; par défaut, `city` reprend l'adresse du profil.
- `qr_svg` est l'image complète à afficher ou enregistrer ; `qr_payload` est la chaîne à encoder si vous dessinez vous-même le QR. Ils ne sont pas inclus dans la liste.

#### Gérer un point et traiter un paiement

**PATCH** `/gimac/qr-points/{uuid}` — Clé API + X-Application-ID

Corps de la requête :

```json
{
  "is_active": false
}
```

La désactivation fait refuser les paiements scannés sur ce QR ; envoyez `is_active: true` pour le réactiver. La réponse reprend la structure de création avec les valeurs modifiées. Une erreur 404 indique un point absent ou rattaché à un autre compte. Un changement de nom ou de ville ne modifie pas les QR déjà imprimés.

Webhook `transaction.success` pour un paiement sur un point :

```json
{
  "event": "transaction.success",
  "data": {
    "uuid": "018f1a2b-3c4d-7e5f-a6b7-c8d9e0f1a2b3",
    "type": "collection",
    "provider": "gimac",
    "status": "success",
    "amount": 5000,
    "currency": "XAF",
    "external_id": "C1452HG78511",
    "metadata": {
      "gimac_intent": "merchant_purchase",
      "gimac_frommember": "32001",
      "qr_point_uuid": "0190b6f0-7a52-7e0a-9b5c-2f6d1c3a8e44",
      "qr_point_ref": "station-42",
      "qr_point_name": "STATION TOTAL BONAMOUSSADI"
    }
  }
}
```

- Utilisez `metadata.qr_point_ref` (votre `external_ref`) pour attribuer le paiement à la station ou au pompiste. Pour le QR de compte, les champs `qr_point_*` sont absents.
- `external_id` identifie le transfert GIMAC ; un même transfert n'est jamais crédité deux fois. Les paiements QR ne sont pas remboursables par API.

#### Conditions et limites

- Le marchand entreprise doit être validé KYC pour recevoir des paiements QR ; le particulier en est dispensé.
- Disponible en XAF dans la zone CEMAC. Un compte ou un point inactif est refusé et le client n'est pas débité.
- Le client saisit le montant lui-même. Les montants fixes par point QR ne sont pas disponibles.

## Annexes

### Webhooks

Kratos Pay envoie un `POST` JSON sur le `webhook_url` de votre application à chaque changement de statut d'une transaction. C'est la méthode recommandée pour connaître le résultat d'un paiement.

#### Événements

| event | Moment |
| --- | --- |
| `transaction.success` | Le paiement a abouti (Mobile Money, carte ou QR GIMAC). |
| `transaction.failed` | Le paiement a échoué (refus, solde insuffisant du client, délai dépassé…). |
| `transaction.cancelled` | Le paiement a été annulé (par vous, le client ou l'abonné). |
| `transaction.awaiting_confirmation` | Un envoi attend encore la confirmation du réseau ; le montant reste réservé. |
| `transaction.refunded` | Le remboursement a été confirmé. |

#### Requête reçue

En-têtes :

```http
POST https://mon-site.cm/webhooks/kratospay
Content-Type: application/json
X-Timestamp: 1751731200
X-Signature: a3f8c9...          (HMAC-SHA256 hexadécimal)
X-Request-ID: 7d2a1c3e-....      (unique par tentative)
User-Agent: KratosPay-Webhooks/1.0
```

Corps :

```json
{
  "event": "transaction.success",
  "api_version": "2026-06-17",
  "created_at": "2026-11-02T14:30:00Z",
  "data": {
    "uuid": "018f1a2b-3c4d-7e5f-a6b7-c8d9e0f1a2b3",
    "type": "collection",
    "provider": "orange_money",
    "status": "success",
    "amount": 5000,
    "currency": "XAF",
    "reference": "MP240101.1234.A56789",
    "external_id": "TXN-987654",
    "phone_number": "696246686",
    "metadata": {
      "description": "Commande 1234"
    }
  }
}
```

#### Vérifier la signature

Calculez `HMAC-SHA256(clé = webhook_secret, message = X-Timestamp + "." + corps brut)` et comparez-le à `X-Signature` avec une comparaison à temps constant. Rejetez toute requête dont `X-Timestamp` s'écarte de plus de 5 minutes de l'heure courante (anti-rejeu). Utilisez le corps **brut**, avant tout décodage JSON.

PHP :

```php
$expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $webhookSecret);
if (!hash_equals($expected, $signature) || abs(time() - (int)$timestamp) > 300) {
  http_response_code(400);
  exit;
}
```

Node.js :

```javascript
const expected = crypto.createHmac('sha256', secret).update(timestamp + '.' + rawBody).digest('hex');
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
```

#### Livraison et nouvelles tentatives

- Répondez par un statut **2xx** pour accuser réception, en moins de 10 secondes. Toute autre réponse (ou l'absence de réponse) est un échec de livraison.
- En cas d'échec, jusqu'à **5 tentatives** au total, espacées de 1, 5, 15 puis 30 minutes.
- Un même événement peut vous être livré plus d'une fois : rendez votre traitement **idempotent** (par exemple en mémorisant `data.uuid` + `event`).
- Ne considérez jamais un paiement comme encaissé sans avoir vérifié la signature ; en cas de doute, confirmez par `GET /transactions/{uuid}`.
- Pour les paiements QR GIMAC, `transaction.success` est envoyé dès la création. Les paiements sur un point QR incluent `metadata.qr_point_ref` (voir Code marchand et points QR).

### Statuts d'une transaction

Les statuts possibles d'une transaction, et lesquels sont définitifs.

#### Statuts

| status | Signification | Définitif ? |
| --- | --- | --- |
| `pending` | Demande envoyée, en attente de la confirmation du client sur son téléphone ou la page carte, ou du réseau. | Non |
| `awaiting_confirmation` | Envoi dont le réseau n'a pas encore confirmé l'issue ; le montant reste réservé jusqu'à la réponse. | Non |
| `success` | Paiement abouti. | Oui (selon le réseau) |
| `failed` | Paiement refusé ou échoué. | Oui |
| `cancelled_by_customer` | Annulé par le marchand ou le client. | Oui |
| `cancelled_by_subscriber` | Refusé ou annulé par l'abonné sur son téléphone. | Oui |
| `expired` | Délai de validation dépassé (par exemple, le client n'est pas revenu de la page carte). | Oui |
| `refunded` | Remboursé. | Oui |

#### Types de transaction

`collection` (encaissement Mobile Money, carte ou QR GIMAC), `disbursement` (envoi), `cashout` (retrait, y compris par virement bancaire), `airtime`, `tv`, `internet`.

### Erreurs fréquentes

Les erreurs les plus fréquentes et la conduite à tenir.

#### Erreurs fréquentes et conduite à tenir

| Situation | Réponse | Conduite à tenir |
| --- | --- | --- |
| Clé API absente ou mal formée | 401 | Le préfixe doit être `sk_test_` ou `sk_live_` ; la clé doit être active et non expirée. |
| Clé test utilisée en production (ou l'inverse) | 401 | Utiliser la clé de l'environnement visé. |
| `X-Application-ID` inconnu ou d'un autre compte | 422 `APPLICATION_NOT_FOUND` | Vérifier l'identifiant (`GET /applications`). |
| Compte entreprise non validé KYC, clé live | 422 (champ `kyc`) | Terminer le KYC (Marchand entreprise, étape 3). |
| Retrait ou envoi supérieur au solde | 422 `INSUFFICIENT_BALANCE` | Consulter `GET /merchant/wallet` ; rien n'a été débité. |
| Virement sans IBAN ou IBAN invalide | 422 (`destination_type`) | Enregistrer un IBAN CEMAC valide dans le profil ; contactez le support pour le modifier. |
| Banque non prise en charge ou devise incorrecte pour un virement | 422 | La banque doit être un participant GIMAC actif et la devise doit être XAF. |
| Paiement par carte non activé sur le compte | 422 (champ `provider`) | Contacter le support pour activer le paiement par carte. |
| Le client revient de la page carte sans payer | `pending` puis `expired` | Ne validez pas la commande ; proposez un nouveau paiement. |
| Numéro d'un autre pays que le réseau | 422 (champ `phone_number`) | Utiliser un numéro du pays du réseau. |
| Réseau désactivé ou indisponible | 422 (champ `provider`) | Réessayer plus tard ou utiliser un autre réseau. |
| Point QR inactif ou compte non validé KYC | Refus côté GIMAC | Réactiver le point ou terminer le KYC requis ; le client n'est pas débité. |
| Timeout sur la création d'un paiement ou d'un point QR | — | Renvoyer le paiement avec la même `idempotency_key` ou le point avec le même `external_ref`. |
| Remboursement MTN, carte ou QR GIMAC | 422 | Non pris en charge par API : seul Orange Money rembourse. |
| Trop de requêtes | 429 | Ralentir et réessayer. |

> Support : communiquez toujours le `meta.request_id` de la réponse concernée.
