Aller au contenu

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. 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. 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. 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
read pour lire ; payments:write pour 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.

Requête
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… ou 08…).
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
CDF ou USD. 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-Key obligatoire, 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é.
Réponse
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, SUCCEEDED ou FAILED (avec failure_reason).
GET /payments
Liste, plus récents d’abord : limit = 10, 25, 50 ou 100 ; page ; filtres status et reference. 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 id déjà traité. Un paiement d’abord échoué peut réussir plus tard (confirmation tardive de l’opérateur) : vous recevez alors aussi payment.succeeded.
Adresse
https, port 443, adresse publique (les adresses privées ou locales sont refusées).
Exemple d’envoi
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é.

Node.js (Express)
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
<?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.

HTTPcodeQuand
400invalid_json · idempotency_key_requiredCorps illisible, ou Idempotency-Key absente (8 à 64 caractères : lettres, chiffres, . _ : -).
401invalid_api_keyClé absente, inconnue, révoquée ou expirée.
402payment_refusedL’opérateur a refusé la demande (numéro, solde, service). Le rejeu de la même clé renvoie la même réponse.
403insufficient_scope · plan_not_allowedClé en lecture seule pour un POST, ou formule sans l’API.
404link_not_found · payment_not_foundIntrouvable sur VOTRE compte (un objet d’un autre compte répond pareil).
409idempotency_key_reused · idempotency_in_progressMême clé avec un autre corps, ou même requête encore en cours.
413payload_too_largeCorps de plus de 16 Ko.
422invalid_request · invalid_phone · amount_below_minimum · amount_mismatchChamp manquant ou invalide, lien inactif ou expiré, montant hors bornes du lien.
429rate_limited · phone_rate_limitedTrop de requêtes (en-tête Retry-After), ou trop de demandes vers le même numéro en 10 min.
503maintenanceRapyoPay 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.