Aller au contenu

Échange entre portefeuilles

Une personne paie X F CFA depuis un portefeuille et reçoit X − frais sur son autre portefeuille, sur le même numéro de téléphone : Orange Money → Wave, ou Wave → Orange Money. Même monnaie, aucune conversion : Xaalis encaisse sur un réseau et verse sur l’autre.

Ce sont des endpoints publics (sans clé d’API), comme la page de paiement hébergée : ils sont derrière la page d’échange Xaalis. Chaque échange est protégé par son propre secret client cs, renvoyé une seule fois à la création.

sequenceDiagram
    participant P as Personne (page d'échange)
    participant X as API Xaalis
    P->>X: POST /v1/public/exchanges/quotes (from, montant, téléphone)
    X-->>P: devis exq_… (frais, receive_amount, valable 5 min)
    P->>X: POST /v1/public/exchanges/otp (téléphone)
    P->>X: POST /v1/public/exchanges (quote, otp) + Idempotency-Key
    X-->>P: échange exc_… + checkout_url + cs
    P->>X: paie sur la page hébergée (portefeuille source)
    X-->>P: versement sur l'autre portefeuille, même numéro
    P->>X: GET /v1/public/exchanges/{id}?cs=… jusqu'au statut final
Fenêtre de terminal
curl -s -X POST https://sandbox-api.xaalis.org/v1/public/exchanges/quotes \
-H "Content-Type: application/json" \
-d '{ "from": "orange_money", "send_amount": 10000, "phone": "+221771234567" }'
{ "object": "exchange_quote", "id": "exq_…", "from": "orange_money", "to": "wave",
"send_amount": 10000, "fee": 150, "receive_amount": 9850, "currency": "XOF", "expires_at": "…" }
  • Donnez send_amount (ce qui quitte le portefeuille source) ou receive_amount (ce qui doit arriver). Pour un montant reçu, Xaalis cherche le plus petit montant envoyé qui donne exactement ce montant ; s’il n’existe pas, vous obtenez 422 limit_exceeded.
  • to est toujours l’autre réseau ; le même réseau des deux côtés est refusé.
  • Un devis dure 5 minutes et ne peut financer qu’un échange.
Fenêtre de terminal
curl -s -X POST https://sandbox-api.xaalis.org/v1/public/exchanges/otp \
-H "Content-Type: application/json" -d '{ "phone": "+221771234567" }'

En mode test, aucun SMS n’est envoyé : la réponse contient "test_code": "000000", et 000000 est le seul code accepté. Au plus 5 codes par numéro et par heure (429 rate_limited au-delà).

Fenêtre de terminal
curl -s -X POST https://sandbox-api.xaalis.org/v1/public/exchanges \
-H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
-d '{ "quote": "exq_…", "otp": "000000" }'
{ "object": "exchange", "id": "exc_…", "status": "awaiting_payment", "from": "orange_money", "to": "wave",
"send_amount": 10000, "fee": 150, "receive_amount": 9850, "currency": "XOF", "livemode": false,
"created_at": "…", "checkout_url": "https://sandbox.xaalis.org/…", "cs": "…" }
  • L’Idempotency-Key est obligatoire ; relancer avec la même clé renvoie le même échange.
  • Envoyez la personne sur checkout_url pour payer depuis le portefeuille source. Elle a 15 minutes ; ensuite l’échange passe à expired et rien ne bouge.
  • Gardez cs côté serveur ou dans la mémoire de la page uniquement : c’est le seul moyen de lire l’échange.
Fenêtre de terminal
curl -s "https://sandbox-api.xaalis.org/v1/public/exchanges/exc_…?cs=$CS"

Interrogez tant que le statut n’est pas final. La réponse ne contient jamais de détail du grand livre, du marchand ou de l’opérateur, et elle est envoyée avec Cache-Control: no-store.

Statut Ce que cela veut dire pour la personne
awaiting_payment on attend son paiement sur la page hébergée
paid paiement reçu ; le versement va démarrer
payout_processing l’argent est en route vers l’autre portefeuille
completed final — receive_amount est arrivé sur l’autre portefeuille
expired final — pas payé en 15 minutes ; rien n’a été prélevé
refunding le versement a échoué ; le send_amount complet est en cours de restitution
refunded final — le send_amount complet est revenu sur le portefeuille source (frais compris)
needs_review un opérateur Xaalis vérifie l’échange — l’argent est en sécurité et la personne sera contactée

Seul un paiement confirmé par l’opérateur compte. Arriver sur une page « succès » ne prouve rien.

frais = max(100, ⌈send_amount × 1,5 %⌉) F CFA, arrondis au franc supérieur ; receive_amount = send_amount − frais.

Envoie Frais Reçoit
1 000 100 900
10 000 150 9 850
33 333 500 32 833
200 000 3 000 197 000

Si le versement échoue, les frais sont restitués avec le reste : un échange remboursé ne coûte rien à la personne.

Par défaut
Par échange 1 000 – 200 000 F CFA
Par numéro, sur 24 h glissantes 500 000 F CFA
Les 7 premiers jours d’un numéro 50 000 F CFA par échange, 100 000 F CFA par 24 h
Échanges par numéro et par heure 5

Au-delà d’une limite → 422 limit_exceeded (vérifié au devis, puis de nouveau à la création). Si le portefeuille de destination n’a pas assez de réserve pour le versement → 503 liquidity_unavailable : réessayez plus tard ou avec un montant plus petit.

Le numéro qui paie est le numéro qui reçoit : le versement part toujours vers le numéro du devis, sur l’autre réseau. Il est impossible d’envoyer un échange à quelqu’un d’autre.

Déclenchez l’encaissement sans portefeuille, avec le cs de l’échange :

Fenêtre de terminal
curl -s -X POST "https://sandbox-api.xaalis.org/v1/public/exchanges/exc_…/simulate?cs=$CS" \
-H "Content-Type: application/json" -d '{ "outcome": "succeeded" }'

succeeded mène l’échange jusqu’à completed ; failed refuse l’encaissement (l’échange ne verse jamais rien).

HTTP code Quand
400 validation_error champ invalide, numéro qui n’est pas +2217XXXXXXXX, mauvais code OTP
403 forbidden appel sur une API où le mode test est coupé (production) — utilisez sandbox-api.xaalis.org
404 not_found échange inconnu, ou mauvais cs
409 invalid_state le devis a expiré ou a déjà servi
422 limit_exceeded montant hors des limites ci-dessus
429 rate_limited trop de requêtes ou de codes OTP — attendez Retry-After secondes
503 liquidity_unavailable pas assez de réserve sur le réseau de destination pour l’instant

Les schémas complets sont dans la référence de l’API, rubrique Wallet exchange.