Acceptez Payroon en quelques minutes
Une API simple : vous créez un paiement, le client paie sur la page Payroon, vous recevez un webhook signé. Exemples en 8 langages.
1Démarrage rapide
2Authentification
Envoyez votre clé secrète dans l’en-tête Authorization. Les clés de test commencent par sk_test_ et ne déplacent jamais d’argent. Les clés réelles commencent par sk_live_ et s’activent après approbation de votre activité. Ne mettez jamais une clé secrète dans une application mobile ou une page web : elle reste sur votre serveur.
Authorization: Bearer sk_test_a1b2c3... (test key)
Authorization: Bearer sk_live_d4e5f6... (live key)3Créer un paiement
Le montant est en unités entières de la devise de votre compte marchand (par exemple 5000 pour 5 000 XAF). Ajoutez un en-tête Idempotency-Key : si votre requête est répétée, vous obtenez le même paiement, jamais un doublon.
curl https://payroon.com/api/gateway.php?action=create_payment \
-H "Authorization: Bearer sk_test_YOUR_KEY" \
-H "Idempotency-Key: order-1042-attempt-1" \
-H "Content-Type: application/json" \
-d '{"amount": 5000, "reference": "order-1042", "description": "2 x T-shirt",
"return_url": "https://yourshop.com/thanks", "cancel_url": "https://yourshop.com/cart"}'$ch = curl_init('https://payroon.com/api/gateway.php?action=create_payment');
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('PAYROON_SECRET_KEY'), 'Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode(['amount' => 5000, 'reference' => 'order-1042',
'return_url' => 'https://yourshop.com/thanks', 'cancel_url' => 'https://yourshop.com/cart'])]);
$pay = json_decode(curl_exec($ch), true)['data'];
header('Location: ' . $pay['checkout_url']);const res = await fetch('https://payroon.com/api/gateway.php?action=create_payment', {
method: 'POST',
headers: { Authorization: 'Bearer ' + process.env.PAYROON_SECRET_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ amount: 5000, reference: 'order-1042',
return_url: 'https://yourshop.com/thanks', cancel_url: 'https://yourshop.com/cart' })
});
const { data } = await res.json();
response.redirect(data.checkout_url);import os, requests
r = requests.post('https://payroon.com/api/gateway.php?action=create_payment',
headers={'Authorization': 'Bearer ' + os.environ['PAYROON_SECRET_KEY']},
json={'amount': 5000, 'reference': 'order-1042',
'return_url': 'https://yourshop.com/thanks', 'cancel_url': 'https://yourshop.com/cart'})
checkout_url = r.json()['data']['checkout_url']require 'net/http'; require 'json'
uri = URI('https://payroon.com/api/gateway.php?action=create_payment')
req = Net::HTTP::Post.new(uri, 'Authorization' => "Bearer #{ENV['PAYROON_SECRET_KEY']}", 'Content-Type' => 'application/json')
req.body = { amount: 5000, reference: 'order-1042', return_url: 'https://yourshop.com/thanks' }.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
checkout_url = JSON.parse(res.body)['data']['checkout_url']body := strings.NewReader(`{"amount":5000,"reference":"order-1042","return_url":"https://yourshop.com/thanks"}`)
req, _ := http.NewRequest("POST", "https://payroon.com/api/gateway.php?action=create_payment", body)
req.Header.Set("Authorization", "Bearer "+os.Getenv("PAYROON_SECRET_KEY"))
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
var out struct{ Data struct{ CheckoutURL string `json:"checkout_url"` } `json:"data"` }
json.NewDecoder(resp.Body).Decode(&out)HttpRequest req = HttpRequest.newBuilder(URI.create("https://payroon.com/api/gateway.php?action=create_payment"))
.header("Authorization", "Bearer " + System.getenv("PAYROON_SECRET_KEY"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("{\"amount\":5000,\"reference\":\"order-1042\",\"return_url\":\"https://yourshop.com/thanks\"}"))
.build();
String json = HttpClient.newHttpClient().send(req, HttpResponse.BodyHandlers.ofString()).body();
// parse json -> data.checkout_url with your JSON libraryvar http = new HttpClient();
http.DefaultRequestHeaders.Add("Authorization", "Bearer " + Environment.GetEnvironmentVariable("PAYROON_SECRET_KEY"));
var body = new StringContent("{\"amount\":5000,\"reference\":\"order-1042\",\"return_url\":\"https://yourshop.com/thanks\"}", Encoding.UTF8, "application/json");
var res = await http.PostAsync("https://payroon.com/api/gateway.php?action=create_payment", body);
var checkoutUrl = JsonDocument.Parse(await res.Content.ReadAsStringAsync()).RootElement.GetProperty("data").GetProperty("checkout_url").GetString();Réponse
{ "success": true, "data": {
"id": "pay_9f2c1a7e5b3d4c6a8e0f1b2d", "object": "payment", "mode": "test", "status": "pending",
"amount": 5000, "currency": "XAF", "reference": "order-1042",
"checkout_url": "https://payroon.com/pay?id=pay_9f2c1a7e5b3d4c6a8e0f1b2d",
"expires_at": "2026-11-01T10:30:00+00:00", "paid_at": null } }4Page de paiement et retour
Redirigez votre client vers checkout_url. Après le paiement il revient sur votre return_url avec ?payroon_payment=pay_… ajouté. Le client choisit sur cette page : Payroon (portefeuille), MTN MoMo ou Orange Money (sans compte Payroon). Les paiements MTN et Orange arrivent dans votre solde marchand. Des frais par moyen de paiement s’appliquent ; vous choisissez dans vos réglages (ou par paiement avec fee_payer) si c’est le client ou vous qui les payez. Vous pouvez retirer vers MTN ou Orange directement depuis votre solde marchand, gratuitement. Ne livrez jamais une commande sur la seule foi de cette redirection : attendez le webhook ou interrogez get_payment, car un client peut fermer la page avant de revenir.
5Webhooks
Enregistrez l’URL https de votre webhook dans l’espace marchand. Payroon y envoie un POST JSON à chaque événement. Répondez avec un code 2xx. Sinon, nous réessayons automatiquement : après 1 min, 5 min, 30 min, 2 h, 6 h puis 24 h. Vous pouvez aussi renvoyer un événement à la main depuis l’espace marchand.
Événements
payment.createdUn paiement vient d’être créé.payment.succeededLe client a payé : livrez la commande.payment.expiredLe lien a expiré sans paiement.payment.cancelledLe paiement a été annulé.Exemple d’événement
{ "id": "evt_4a1b...", "object": "event", "type": "payment.succeeded", "created": 1793000000, "mode": "live",
"data": { "payment": { "id": "pay_9f2c...", "status": "paid", "amount": 5000, "currency": "XAF", "reference": "order-1042", "paid_at": "2026-11-01T10:12:09+00:00" } } }6Vérifier la signature
Chaque requête contient l’en-tête Payroon-Signature : t=<horodatage>,v1=<signature>. Calculez HMAC-SHA256 de « t.corps » avec votre secret de webhook (whsec_…), comparez en temps constant et refusez les horodatages de plus de 5 minutes. Utilisez le corps brut, pas un JSON ré-encodé.
$body = file_get_contents('php://input');
preg_match('/t=(\d+),v1=([a-f0-9]+)/', $_SERVER['HTTP_PAYROON_SIGNATURE'] ?? '', $m);
$ok = $m && abs(time() - (int)$m[1]) < 300
&& hash_equals(hash_hmac('sha256', $m[1] . '.' . $body, getenv('PAYROON_WEBHOOK_SECRET')), $m[2]);
if (!$ok) { http_response_code(400); exit; }
$event = json_decode($body, true);
if ($event['type'] === 'payment.succeeded') { /* fulfil order $event['data']['payment']['reference'] once */ }
http_response_code(200);const crypto = require('crypto'); // use the RAW body
app.post('/payroon-webhook', express.raw({ type: 'application/json' }), (req, res) => {
const m = /t=(\d+),v1=([a-f0-9]+)/.exec(req.get('Payroon-Signature') || '');
if (!m || Math.abs(Date.now() / 1000 - Number(m[1])) > 300) return res.sendStatus(400);
const expected = crypto.createHmac('sha256', process.env.PAYROON_WEBHOOK_SECRET).update(m[1] + '.' + req.body).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(m[2]))) return res.sendStatus(400);
const event = JSON.parse(req.body);
if (event.type === 'payment.succeeded') { /* fulfil event.data.payment.reference once */ }
res.sendStatus(200);
});import hmac, hashlib, os, re, time, json
from flask import Flask, request, abort
@app.post('/payroon-webhook')
def webhook():
body = request.get_data()
m = re.match(r't=(\d+),v1=([a-f0-9]+)', request.headers.get('Payroon-Signature', ''))
if not m or abs(time.time() - int(m[1])) > 300: abort(400)
expected = hmac.new(os.environ['PAYROON_WEBHOOK_SECRET'].encode(), m[1].encode() + b'.' + body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, m[2]): abort(400)
event = json.loads(body)
if event['type'] == 'payment.succeeded': pass # fulfil the order once
return '', 200require 'openssl'; require 'json'
post '/payroon-webhook' do
body = request.body.read
m = /t=(\d+),v1=([a-f0-9]+)/.match(request.env['HTTP_PAYROON_SIGNATURE'].to_s)
halt 400 unless m && (Time.now.to_i - m[1].to_i).abs < 300
expected = OpenSSL::HMAC.hexdigest('SHA256', ENV['PAYROON_WEBHOOK_SECRET'], "#{m[1]}.#{body}")
halt 400 unless Rack::Utils.secure_compare(expected, m[2])
event = JSON.parse(body)
# event['type'] == 'payment.succeeded' -> fulfil the order once
status 200
endfunc webhook(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
m := regexp.MustCompile(`t=(\d+),v1=([a-f0-9]+)`).FindStringSubmatch(r.Header.Get("Payroon-Signature"))
if m == nil { http.Error(w, "bad", 400); return }
ts, _ := strconv.ParseInt(m[1], 10, 64)
if math.Abs(float64(time.Now().Unix()-ts)) > 300 { http.Error(w, "old", 400); return }
mac := hmac.New(sha256.New, []byte(os.Getenv("PAYROON_WEBHOOK_SECRET")))
mac.Write([]byte(m[1] + "." + string(body)))
if !hmac.Equal([]byte(hex.EncodeToString(mac.Sum(nil))), []byte(m[2])) { http.Error(w, "bad", 400); return }
// parse body; if type == "payment.succeeded" fulfil the order once
w.WriteHeader(200)
}// ASP.NET minimal API
app.MapPost("/payroon-webhook", async (HttpRequest req) => {
var body = await new StreamReader(req.Body).ReadToEndAsync();
var m = Regex.Match(req.Headers["Payroon-Signature"].ToString(), @"t=(\d+),v1=([a-f0-9]+)");
if (!m.Success || Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - long.Parse(m.Groups[1].Value)) > 300) return Results.BadRequest();
using var h = new HMACSHA256(Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable("PAYROON_WEBHOOK_SECRET")!));
var expected = Convert.ToHexString(h.ComputeHash(Encoding.UTF8.GetBytes(m.Groups[1].Value + "." + body))).ToLower();
if (!CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(expected), Encoding.UTF8.GetBytes(m.Groups[2].Value))) return Results.BadRequest();
// parse body; if type == "payment.succeeded" fulfil the order once
return Results.Ok();
});7Référence de l’API
Toutes les routes sont sous /api/gateway.php?action=… et utilisent la même clé.
Crée un paiement et renvoie le lien de paiement.
| Paramètre | Type | Requis | Description |
|---|---|---|---|
amount | number | oui | Montant en unités entières de votre devise. |
currency | string | non | Doit être la devise de votre compte (facultatif). |
reference | string | non | Votre numéro de commande (100 caractères max). |
description | string | non | Texte affiché au client. |
customer_email | string | non | Adresse email du client (facultatif). |
return_url | string | non | Adresse https vers laquelle revenir après paiement. |
cancel_url | string | non | Adresse https si le client annule. |
metadata | object | non | Jusqu’à 20 clés, 1000 caractères : vous les retrouvez dans les webhooks. |
expires_in | integer | non | Durée de vie du lien en minutes (5 à la limite fixée par Payroon). |
fee_payer | string | non | « customer » : le client paie le prix + les frais ; « merchant » : les frais sont prélevés sur votre vente. Par défaut : le réglage de votre compte. |
Retourne un paiement.
| Paramètre | Type | Requis | Description |
|---|---|---|---|
id | string | oui | Identifiant du paiement (pay_…). |
Liste vos paiements (mode de votre clé).
| Paramètre | Type | Requis | Description |
|---|---|---|---|
limit | integer | non | Nombre de résultats, 1 à 100. |
status | string | non | pending, paid, expired ou cancelled. |
reference | string | non | Votre numéro de commande (100 caractères max). |
Annule un paiement encore en attente.
| Paramètre | Type | Requis | Description |
|---|---|---|---|
id | string | oui | Identifiant du paiement (pay_…). |
Vérifie votre clé et renvoie votre profil.
8Erreurs
Les erreurs ont toujours la même forme et un code HTTP adapté.
{ "success": false, "error": { "code": "invalid_amount", "message": "amount must be a number, in whole currency units." } }401Clé manquante, incorrecte ou remplacée.403Compte marchand non approuvé (clé réelle) ou suspendu.404Paiement introuvable pour cette clé.409Action impossible dans l’état actuel du paiement.422Données invalides : lisez le champ message.429Trop de requêtes : ralentissez.9Mode test et mode réel
Avec une clé de test, la page de paiement propose « Simuler un paiement réussi » et « Simuler un paiement annulé ». Aucun argent ne bouge et vos webhooks portent mode=test. Basculez entre Test et Réel à tout moment dans l’espace marchand. Les paiements réels arrivent dans votre solde marchand : vous les passez ensuite gratuitement vers votre solde principal.