Encaissez depuis votre site, confirmez sans attendre
Le client paie, ferme l’onglet avant la redirection… et votre site ne sait pas si la commande est payée. Avec l’API, votre serveur lance la demande Mobile Money lui-même et RapyoPay le prévient, signature à l’appui, dès que le paiement réussit ou échoue.
Formules Pro et Entreprise · CDF et USD · Aucun retrait possible par l’API
Un appel, une demande
Votre serveur envoie le numéro et le montant ; le client confirme sur son téléphone.
Confirmation signée
Un webhook part à votre serveur dès que le paiement réussit, échoue ou est remboursé.
Jamais d’argent qui sort
L’API encaisse sur vos liens. Elle ne retire rien et ne touche pas à vos coordonnées.
Démarrer en trois étapes
- 1. Un lien de paiement. Créez-le dans le tableau de bord : il fixe la devise, le montant (fixe, ou libre avec bornes) et l’organisation qui encaisse. L’API ne paie que sur vos liens.
- 2. Une clé d’API avec « Autoriser la création de paiements ». Dans Paramètres → Applications connectées. Gardez-la sur votre serveur, jamais dans une page web ou une application mobile.
- 3. Une adresse de webhook. Même page, section Webhooks : copiez le secret affiché une seule fois, puis « Envoyer un événement de test ».
Pas d’environnement de test séparé : faites un premier essai réel au montant minimum (150 CDF ou 1 USD) sur votre propre numéro.
Authentification
- En-tête
Authorization: Bearer rpk_live_…sur chaque requête, en HTTPS.- Portées
readpour lire ;payments:writepour créer un paiement, accordée seulement si vous cochez « Autoriser la création de paiements » à la création de la clé. Une clé en lecture seule reçoit 403.- Serveur à serveur
- Aucun en-tête CORS : un navigateur ne peut pas appeler l’API. C’est voulu.
- Révocation
- Immédiate, depuis vos paramètres. La formule est vérifiée à chaque appel.
Créer un paiement
POST /api/v1/payments envoie la demande Mobile Money au téléphone du client et répond aussitôt avec le statut PENDING.
curl -X POST https://pay.rapyogo.com/api/v1/payments \
-H "Authorization: Bearer rpk_live_VOTRE_CLE" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: commande-1042" \
-d '{
"link_id": "VOTRE_LIEN",
"amount": "15000",
"currency": "CDF",
"phone": "+243812345678",
"reference": "CMD-1042",
"metadata": { "panier": "A17" }
}'- link_id *
- Identifiant de votre lien (celui de l’adresse /pay/… ou l’identifiant interne).
- phone *
- Numéro Mobile Money RDC (
+243…ou08…). - amount
- Montant en texte décimal. Obligatoire sur un lien à montant libre ; sur un lien à montant fixe, omettez-le ou envoyez exactement ce montant.
- currency
CDFouUSD. Par défaut : la devise du lien. L’autre devise n’est acceptée que si le lien l’accepte (taux RapyoPay du jour).- reference
- Votre numéro de commande (100 caractères). Renvoyé dans le statut et les webhooks.
- metadata
- Objet de textes courts : 20 clés, 2 Ko au plus. Ne mettez pas de données sensibles.
- payer_name, payer_email
- Facultatifs, sauf si le lien exige l’e-mail (reçu, fichier livré).
- Minimum
- 150 CDF ou 1 USD : en dessous, l’opérateur refuse (
422 amount_below_minimum). - Idempotence
Idempotency-Keyobligatoire, une par paiement (votre numéro de commande convient). Rejouée avec le même corps pendant 24 h, elle renvoie la même réponse sans nouvelle demande au client ; avec un autre corps : 409. Après une coupure réseau, réessayez toujours avec la même clé.
HTTP/1.1 201 Created
{
"id": "PAY_3f9c…",
"object": "payment",
"status": "PENDING",
"amount": "15000.00",
"currency": "CDF",
"link_id": "clx…",
"reference": "CMD-1042",
"metadata": { "panier": "A17" },
"payer_phone": "+243 •• ••• 56 78",
"failure_reason": null,
"created_at": "2026-11-09T10:00:00.000Z",
"updated_at": "2026-11-09T10:00:00.000Z"
}Statut et liste
- GET /payments/:id
- Statut d’un paiement de votre compte :
PENDING,SUCCEEDEDouFAILED(avecfailure_reason). - GET /payments
- Liste, plus récents d’abord :
limit= 10, 25, 50 ou 100 ;page; filtresstatusetreference. Réponse{ object: "list", data, page, limit, total, has_more }. - Délai
- Le client a quelques minutes pour confirmer sur son téléphone ; sans réponse de l’opérateur, le paiement passe en
FAILED. Préférez le webhook à l’interrogation en boucle.
Webhooks
RapyoPay envoie un POST JSON à votre adresse https à chaque changement définitif d’un paiement sur vos liens, qu’il ait été lancé par l’API, par /pay ou en caisse.
- Événements
payment.succeeded,payment.failed,payment.refunded(réclamation du payeur acceptée),test(bouton de vos paramètres).- Réponse attendue
- Un code 2xx sous 5 s. Les redirections ne sont pas suivies.
- Reprise
- Sinon, nouvel essai après 1 min, 5 min, 30 min, 2 h, 6 h, 12 h, 24 h : 8 essais au total. Le journal des dernières livraisons est visible dans vos paramètres.
- Au moins une fois
- Un événement peut arriver deux fois : ignorez un
iddéjà traité. Un paiement d’abord échoué peut réussir plus tard (confirmation tardive de l’opérateur) : vous recevez alors aussipayment.succeeded. - Adresse
- https, port 443, adresse publique (les adresses privées ou locales sont refusées).
POST https://votre-site.cd/webhooks/rapypay
RapyPay-Signature: t=1762682400,v1=5d41402abc4b2a76b9719d911017c592…
RapyPay-Event-Id: evt_9f2c1a7b3e5d4c6f8a0b1c2d
RapyPay-Event-Type: payment.succeeded
{
"id": "evt_9f2c1a7b3e5d4c6f8a0b1c2d",
"object": "event",
"type": "payment.succeeded",
"created": 1762682400,
"data": { "payment": { "id": "PAY_3f9c…", "status": "SUCCEEDED", "amount": "15000.00", "currency": "CDF", "reference": "CMD-1042", … } }
}Vérifier la signature
RapyPay-Signature: t=<horodatage>,v1=<signature> où la signature est le HMAC-SHA256, en hexadécimal, de t + "." + corps brut avec votre secret whsec_…. Refusez un horodatage de plus de 5 minutes : un message rejoué plus tard est rejeté.
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const SECRET = process.env.RAPYPAY_WEBHOOK_SECRET; // whsec_…
// Corps BRUT : la signature porte sur les octets reçus, pas sur un JSON relu.
app.post('/webhooks/rapypay', express.raw({ type: 'application/json' }), (req, res) => {
const body = req.body.toString('utf8');
const parts = Object.fromEntries(
String(req.get('RapyPay-Signature') || '').split(',').map((p) => p.split('=')),
);
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return res.sendStatus(400);
const expected = crypto.createHmac('sha256', SECRET).update(`${t}.${body}`).digest('hex');
const ok = /^[0-9a-f]{64}$/.test(parts.v1 || '') &&
crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(parts.v1, 'hex'));
if (!ok) return res.sendStatus(400);
const event = JSON.parse(body);
// Livraison « au moins une fois » : ignorez un event.id déjà traité.
if (event.type === 'payment.succeeded') {
// Confirmez la commande event.data.payment.reference
}
res.sendStatus(200); // répondez vite (5 s au plus), traitez ensuite
});<?php
$secret = getenv('RAPYPAY_WEBHOOK_SECRET'); // whsec_…
$payload = file_get_contents('php://input'); // corps brut
$header = $_SERVER['HTTP_RAPYPAY_SIGNATURE'] ?? '';
$parts = [];
foreach (explode(',', $header) as $part) {
[$k, $v] = array_pad(explode('=', $part, 2), 2, '');
$parts[trim($k)] = trim($v);
}
$t = (int) ($parts['t'] ?? 0);
$expected = hash_hmac('sha256', $t . '.' . $payload, $secret);
if (abs(time() - $t) > 300 || !hash_equals($expected, $parts['v1'] ?? '')) {
http_response_code(400);
exit;
}
$event = json_decode($payload, true);
// Ignorez un $event['id'] déjà traité (livraison « au moins une fois »).
if ($event['type'] === 'payment.succeeded') {
$reference = $event['data']['payment']['reference'];
// Confirmez la commande $reference
}
http_response_code(200);Après « Régénérer le secret », l’ancien cesse aussitôt de signer : mettez votre serveur à jour juste après.
Erreurs et limites
Toute erreur a la même forme : { "error": { "code": "…", "message": "…" } }. Le code est stable, le message peut changer.
| HTTP | code | Quand |
|---|---|---|
| 400 | invalid_json · idempotency_key_required | Corps illisible, ou Idempotency-Key absente (8 à 64 caractères : lettres, chiffres, . _ : -). |
| 401 | invalid_api_key | Clé absente, inconnue, révoquée ou expirée. |
| 402 | payment_refused | L’opérateur a refusé la demande (numéro, solde, service). Le rejeu de la même clé renvoie la même réponse. |
| 403 | insufficient_scope · plan_not_allowed | Clé en lecture seule pour un POST, ou formule sans l’API. |
| 404 | link_not_found · payment_not_found | Introuvable sur VOTRE compte (un objet d’un autre compte répond pareil). |
| 409 | idempotency_key_reused · idempotency_in_progress | Même clé avec un autre corps, ou même requête encore en cours. |
| 413 | payload_too_large | Corps de plus de 16 Ko. |
| 422 | invalid_request · invalid_phone · amount_below_minimum · amount_mismatch | Champ manquant ou invalide, lien inactif ou expiré, montant hors bornes du lien. |
| 429 | rate_limited · phone_rate_limited | Trop de requêtes (en-tête Retry-After), ou trop de demandes vers le même numéro en 10 min. |
| 503 | maintenance | RapyoPay en maintenance : rien n’est débité, réessayez plus tard. |
- Par clé
- 120 requêtes par minute ; plafond quotidien de votre formule (partagé avec la connexion IA).
- Par compte
- 60 créations de paiement par 10 minutes, toutes clés confondues.
- Par adresse IP
- 120 requêtes par minute avant authentification.
- Par numéro de payeur
- 5 demandes en 10 minutes, tous marchands confondus (anti-harcèlement).
Pour lire votre compte avec un assistant IA, voir aussi la connexion IA (MCP).
Prompt prêt à coller dans votre assistant IA
Vous avez une page de vente et un assistant de code (Claude Code, Cursor, ChatGPT…) ? Copiez ce prompt : il décrit tout ce qu’il faut construire (formulaire, route serveur, écran de confirmation, webhook signé) et les pièges à éviter. Ni clé ni secret n’y figurent : vous les mettrez vous-même dans les variables d’environnement de votre serveur.
Aussi disponible dans les prompts de la connexion IA.

