Payroon
ENFR Sign in Get started Get the app
Developer documentation

Accept Payroon in minutes

A simple API: you create a payment, the customer pays on the Payroon page, you receive a signed webhook. Examples in 8 languages.

Open the merchant portal Payroon Merchant

1Quickstart

Create your spaceOpen the merchant portal with your Payroon account. Your test keys are created instantly.
Create a paymentCall the API from your server with your secret key.
Send the customerRedirect them to checkout_url. They pay with their PIN.
Fulfil the orderYour server receives payment.succeeded: confirm the order.

2Authentication

Send your secret key in the Authorization header. Test keys start with sk_test_ and never move money. Live keys start with sk_live_ and switch on once your business is approved. Never put a secret key in a mobile app or a web page: it stays on your server.

Authorization: Bearer sk_test_a1b2c3...   (test key)
Authorization: Bearer sk_live_d4e5f6...   (live key)
A secret key is shown only once, when it is created. If you lose it, replace it from the merchant portal (PIN required): the old one stops working.

3Create a payment

The amount is in whole units of your merchant account currency (for example 5000 for 5,000 XAF). Add an Idempotency-Key header: if your request is repeated you get the same payment back, never a duplicate.

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"}'

Response

{ "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 } }

4Checkout page and return

Redirect your customer to checkout_url. After paying they return to your return_url with ?payroon_payment=pay_… added. On that page the customer picks Payroon (wallet), MTN MoMo or Orange Money (no Payroon account needed). MTN and Orange payments land in your merchant balance. A fee per payment method applies; in your settings (or per payment with fee_payer) you choose whether the customer or you pays it. You can cash out to MTN or Orange straight from your merchant balance, for free. Never fulfil an order on that redirect alone: wait for the webhook or call get_payment, because a customer can close the page before coming back.

5Webhooks

Save your https webhook URL in the merchant portal. Payroon sends a JSON POST for every event. Reply with a 2xx status. Otherwise we retry automatically: after 1 min, 5 min, 30 min, 2 h, 6 h and 24 h. You can also resend any event by hand from the portal.

Events

payment.createdA payment was just created.
payment.succeededThe customer paid: fulfil the order.
payment.expiredThe link expired without payment.
payment.cancelledThe payment was cancelled.

Example event

{ "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" } } }

6Verify the signature

Every request carries the header Payroon-Signature: t=<timestamp>,v1=<signature>. Compute HMAC-SHA256 of "t.body" with your webhook secret (whsec_…), compare in constant time and reject timestamps older than 5 minutes. Use the raw body, not re-encoded JSON.

$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);
Make your handler idempotent: the same event (same id) can arrive more than once.

7API reference

All routes live under /api/gateway.php?action=… and use the same key.

POST/api/gateway.php?action=create_payment

Creates a payment and returns the checkout link.

ParameterTypeRequiredDescription
amountnumberyesAmount in whole units of your currency.
currencystringnoMust equal your account currency (optional).
referencestringnoYour order number (100 characters max).
descriptionstringnoText shown to the customer.
customer_emailstringnoCustomer email address (optional).
return_urlstringnohttps address to return to after payment.
cancel_urlstringnohttps address if the customer cancels.
metadataobjectnoUp to 20 keys, 1000 characters: returned in your webhooks.
expires_inintegernoLink lifetime in minutes (5 up to the limit Payroon sets).
fee_payerstringno"customer": the customer pays the price plus the fee; "merchant": the fee is taken from your sale. Default: your account setting.
GET/api/gateway.php?action=get_payment&id=pay_…

Returns one payment.

ParameterTypeRequiredDescription
idstringyesThe payment id (pay_…).
GET/api/gateway.php?action=list_payments

Lists your payments (for your key’s mode).

ParameterTypeRequiredDescription
limitintegernoNumber of results, 1 to 100.
statusstringnopending, paid, expired or cancelled.
referencestringnoYour order number (100 characters max).
POST/api/gateway.php?action=cancel_payment

Cancels a payment that is still waiting.

ParameterTypeRequiredDescription
idstringyesThe payment id (pay_…).
GET/api/gateway.php?action=ping

Checks your key and returns your profile.

8Errors

Errors always have the same shape and a matching HTTP status.

{ "success": false, "error": { "code": "invalid_amount", "message": "amount must be a number, in whole currency units." } }
401Key missing, wrong or replaced.
403Merchant account not approved (live key) or suspended.
404Payment not found for this key.
409Not possible in the payment’s current state.
422Invalid data: read the message field.
429Too many requests: slow down.

9Test mode and live mode

With a test key the checkout page offers “Simulate successful payment” and “Simulate cancelled payment”. No money moves and your webhooks carry mode=test. Switch between Test and Live whenever you like in the merchant portal. Live payments land in your merchant balance, which you then move to your main balance for free.

Need another language? The API is JSON over HTTPS: any HTTP library works.