# Documentation développeur — XPay API v1

_Mise à jour : 10 juillet 2026_

XPay est une API de paiement mobile-first pour l'Afrique. Vous pouvez :

- Créer des demandes de paiement en USD ou CDF côté client
- Rediriger vos clients vers une page de checkout sécurisée hébergée par XPay
- Recevoir une notification (webhook) HTTP dès qu'un paiement est confirmé
- Poller le statut d'une transaction via un endpoint public
- Consulter soldes et historique dans votre tableau de bord

**Base URL de production :** `https://x-pay.dev`
**Format :** JSON UTF-8. Montants en **centimes** (1 USD = 100, 1 CDF = 100).
**Devises supportées :** `USD`, `CDF`.
**Fuseau horaire :** UTC, format ISO 8601.

---

## 1. Règle d'or des wallets

> **Seul le wallet USD est crédité, quelle que soit la devise payée par le client.**

Votre client peut être facturé en `USD` ou en `CDF`. Dans les deux cas, XPay règle le paiement en **USD uniquement** :

- **Wallet USD** : crédité par *tous* les paiements confirmés, en dollars. C'est votre solde réel de règlement.
- **Wallet CDF** : jamais crédité par un paiement provenant d'une intégration API ou d'un checkout. Il ne sert que pour les conversions et retraits manuels gérés directement par XPay.
- **Conversion automatique** : un paiement de 5 000 CDF est collecté en CDF côté client, puis converti au taux du jour et crédité en USD sur votre wallet.

Dans le webhook de confirmation, les champs `amount` et `currency` indiquent ce qui a été facturé au client. Les champs `settled_amount` et `settled_currency` représentent ce qui a été réellement crédité sur votre wallet USD — c'est ce que vous devez utiliser pour votre comptabilité et la livraison de votre service.

**Règlement marchand :** toujours `USD`. Un paiement CDF est converti au taux du jour et crédite uniquement le wallet USD.

### Frais & minimums (v1)

- **Dépôts : 0 % de frais.** Le montant réglé est crédité intégralement sur votre wallet USD (net = brut). Minimum 1 USD (ou équivalent CDF).
- **Retraits :** frais de 15 %, minimum 80 USD ou 5 500 FC.
- Pour un dépôt, le webhook renvoie `settled_amount` — c'est exactement ce qui est crédité sur votre wallet USD. Aucun `fee_amount` n'est déduit en v1.
- Les frais de dépôt sont **verrouillés à 0 %** dans l'administration XPay.

---

## 2. Démarrage rapide

1. Créez votre compte sur https://x-pay.dev et confirmez votre email.
2. Générez une clé API dans **Plus → Intégrations API**. Copiez-la immédiatement.
3. Configurez votre URL webhook (HTTPS obligatoire).
4. Appelez `POST /api/public/payments` avec le montant et la devise.
5. Redirigez votre client vers la `payment_url` reçue.
6. Traitez le webhook `POST` reçu à la confirmation.

---

## 3. Authentification

Chaque requête doit inclure votre clé secrète dans :

- `X-API-Key: xpk_live_...`
- ou `Authorization: Bearer xpk_live_...`

Format : `xpk_live_` + 32 caractères.

```bash
curl https://x-pay.dev/api/public/payments \
  -H "X-API-Key: xpk_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "amount": 500, "currency": "USD" }'
```

> ⚠️ Ne jamais exposer votre clé côté client (mobile, navigateur). Appelez toujours depuis votre backend.

---

## 4. Créer un paiement

**`POST https://x-pay.dev/api/public/payments`**

| Champ | Type | Requis | Description |
|---|---|---|---|
| `amount` | integer | oui | Montant en centimes (min : 1) |
| `currency` | string | oui | `USD` ou `CDF` |
| `description` | string | non | Libellé affiché au client (max 200 car.) |
| `customer_reference` | string | non | Identifiant interne, renvoyé dans le webhook |
| `customer_email` | string | non | Email du client (pré-remplissage) |
| `webhook_url` | string | non | Surcharge de l'URL webhook globale, pour ce paiement |
| `metadata` | object | non | JSON libre renvoyé tel quel dans le webhook |

**Réponse `201` :**

```json
{
  "order_id": "xpay_lz3k9x_a1b2c3d4",
  "transaction_id": "9f0e...",
  "status": "pending",
  "payment_url": "https://x-pay.dev/checkout/xpay_lz3k9x_a1b2c3d4",
  "amount": 500,
  "currency": "USD"
}
```

**Exemple :**

```bash
curl -X POST https://x-pay.dev/api/public/payments \
  -H "X-API-Key: xpk_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 1500,
    "currency": "USD",
    "description": "Commande #1042",
    "customer_reference": "ORDER-1042",
    "customer_email": "client@example.com",
    "metadata": { "user_id": "u_123", "plan": "pro" }
  }'
```

---

## 5. Page de paiement (Checkout)

Ouvrez la `payment_url` reçue. Trois intégrations :

- **Nouvel onglet** — `window.open(payment_url, '_blank')`. L'onglet tente `window.close()` après succès (bloqué par certains navigateurs, prévoir fallback).
- **Redirection** — `window.location.href = payment_url`. Retour via `document.referrer` ou `return_url`.
- **Iframe** — XPay émet un `postMessage` à la page parente à la confirmation.

### Détection côté client (postMessage)

```js
window.addEventListener("message", (event) => {
  if (event.origin !== "https://x-pay.dev") return;
  const data = event.data;
  if (data && data.type === "xpay:payment" && data.status === "success") {
    // data.order_id contient l'identifiant
    // Rafraîchir votre UI / rediriger le client
  }
});
```

### Vérifier le statut (polling, non authentifié)

**`GET https://x-pay.dev/api/public/checkout-session/:order_id`**

```json
{
  "order_id": "xpay_lz3k9x_a1b2c3d4",
  "status": "pending",
  "amount": 1500,
  "currency": "USD",
  "checkout_url": "https://...",
  "created_at": "2026-07-10T12:30:00.000Z"
}
```

> Cet endpoint est **sans authentification** — ne renvoie aucune donnée sensible.

---

## 6. Webhooks marchand

Dès qu'un paiement est confirmé, XPay envoie une requête **POST JSON**. Votre endpoint doit répondre **HTTP 2xx en moins de 8 secondes**. Toute réponse hors 2xx ou hors délai est marquée `failed`.

### En-têtes envoyés

```
POST /votre/endpoint HTTP/1.1
Host: votre-domaine.com
Content-Type: application/json
X-XPay-Event: payment.success
```

### Payload

```json
{
  "event": "payment.success",
  "status": "success",
  "order_id": "xpay_lz3k9x_a1b2c3d4",
  "transaction_id": "9f0e2a10-7c8f-4b2e-9c1d-3a0f5e6d7b12",
  "customer_reference": "ORDER-1042",
  "amount": 50000,
  "currency": "CDF",
  "settled_amount": 22,
  "settled_currency": "USD",
  "exchange_rate": 2283.29,
  "metadata": { "user_id": "u_123" },
  "confirmed_at": "2026-07-10T12:34:56.000Z"
}
```

> **Règle métier XPay :** tous les paiements sont réglés en **USD**.
> Quand vous facturez en `CDF`, XPay collecte le montant côté client en CDF
> puis convertit au taux du jour et crédite uniquement votre wallet USD.
> Le wallet CDF n'est jamais crédité par un paiement d'intégration. Utilisez
> `settled_amount` / `settled_currency` pour votre comptabilité — c'est ce
> qui a réellement été crédité au wallet.

### Champs

| Champ | Type | Description |
|---|---|---|
| `event` | string | Toujours `payment.success` en v1 |
| `status` | string | `success` (v1 n'envoie pas encore `failed`) |
| `order_id` | string | Identifiant XPay unique — **clé d'idempotence** |
| `transaction_id` | string (uuid) | UUID interne XPay |
| `customer_reference` | string \| null | Votre référence à la création |
| `amount` | integer | Montant facturé au client, en centimes de la devise d'origine |
| `currency` | string | Devise facturée (`USD` ou `CDF`) |
| `settled_amount` | integer | Montant réellement crédité au wallet, en centimes USD |
| `settled_currency` | string | Toujours `USD` en v1 |
| `exchange_rate` | number \| null | Taux USD→CDF appliqué si `currency=CDF` ; `null` si `currency=USD` |
| `metadata` | object \| null | JSON libre transmis à la création |
| `confirmed_at` | string | Timestamp ISO 8601 UTC |


### Réponse attendue

```
HTTP/1.1 200 OK
Content-Type: application/json

{ "received": true }
```

> **Idempotence obligatoire.** Utilisez `order_id` comme clé unique et répondez `200 OK` pour tout événement déjà traité.

### Retry & livraisons échouées

- **v1** : une tentative automatique à la confirmation. En cas d'échec, la livraison est stockée avec le code HTTP et la réponse.
- Rejeu manuel disponible dans le dashboard (Transactions → détail).
- **v2 (à venir)** : retry exponentiel (1 min, 5 min, 30 min, 2 h, 12 h) jusqu'à 24 h.

### Exemple de handler (Node.js / Express)

```js
app.post("/webhooks/xpay", express.json(), async (req, res) => {
  const {
    event, status, order_id, customer_reference, amount, currency, metadata,
  } = req.body;

  if (event !== "payment.success" || status !== "success") {
    return res.status(200).end();
  }

  const already = await db.orders.findOne({ xpay_order_id: order_id });
  if (already?.paid) return res.status(200).end();

  const order = await db.orders.findOne({ reference: customer_reference });
  if (!order || order.amount !== amount || order.currency !== currency) {
    return res.status(200).end();
  }

  await db.orders.updateOne(
    { _id: order._id },
    { $set: { paid: true, xpay_order_id: order_id, paid_at: new Date() } },
  );

  return res.status(200).json({ received: true });
});
```

---

## 7. Sécurité

- **HTTPS obligatoire** pour l'URL webhook.
- **Clés secrètes côté serveur uniquement** — jamais dans du JS client ou une app mobile.
- **Idempotence** par `order_id`.
- **Vérifiez `amount` et `currency`** reçus avant de livrer.
- **Rotation** : régénérez la clé en cas de compromission, l'ancienne est révoquée immédiatement.

---

## 8. Codes d'erreur

Format :

```json
{ "error": { "code": "invalid_api_key", "message": "API key invalide ou révoquée" } }
```

| Code | HTTP | Description |
|---|---|---|
| `missing_api_key` | 401 | En-tête `X-API-Key` absent |
| `invalid_api_key` | 401 | Clé inconnue ou révoquée |
| `invalid_json` | 400 | Body JSON malformé |
| `invalid_payload` | 400 | Paramètre requis manquant ou invalide |
| `invalid_amount` | 400 | Montant doit être un entier positif |
| `invalid_currency` | 400 | Seuls `USD` et `CDF` sont supportés |
| `checkout_url_failed` | 500 | Impossible d'initialiser le paiement |
| `internal_error` | 500 | Erreur interne — réessayer |

---

## 9. Exemples de code

### Node.js

```js
import fetch from "node-fetch";

const res = await fetch("https://x-pay.dev/api/public/payments", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.XPAY_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    amount: 1500,
    currency: "USD",
    description: "Commande #1042",
    customer_reference: "ORDER-1042",
  }),
});
const { payment_url } = await res.json();
return { redirect: payment_url };
```

### PHP

```php
<?php
$ch = curl_init("https://x-pay.dev/api/public/payments");
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_POST => true,
  CURLOPT_HTTPHEADER => [
    "X-API-Key: " . getenv("XPAY_API_KEY"),
    "Content-Type: application/json",
  ],
  CURLOPT_POSTFIELDS => json_encode([
    "amount" => 1500,
    "currency" => "USD",
    "customer_reference" => "ORDER-1042",
  ]),
]);
$data = json_decode(curl_exec($ch), true);
header("Location: " . $data["payment_url"]);
```

### Python

```python
import os, requests

r = requests.post(
    "https://x-pay.dev/api/public/payments",
    headers={"X-API-Key": os.environ["XPAY_API_KEY"]},
    json={
        "amount": 1500,
        "currency": "USD",
        "customer_reference": "ORDER-1042",
    },
    timeout=10,
)
r.raise_for_status()
payment_url = r.json()["payment_url"]
```

---

## 10. Tests & Go-Live

1. Effectuez un paiement d'un petit montant (ex. 100 CDF).
2. Vérifiez que la transaction apparaît en statut **Réussi** dans le dashboard.
3. Contrôlez la réception du webhook (logs) — livraison en **200 OK**.
4. Confirmez l'exécution de votre logique métier (activation, réduction, livraison…).
5. En cas d'échec, rejouez manuellement depuis le dashboard.

**Support :** WhatsApp : +243 997 532 529

---

_Documentation XPay v1 — toute évolution rétrocompatible est ajoutée sans changement de version._
