Accept payments with PAYRYTE
One API for Visa/Mastercard, MTN Mobile Money, Orange Money, PAYRYTE wallet and bank transfer, in 8 currencies. You create a payment, send your customer to our secure checkout page, and we tell you the result. Card details never touch your servers.
Quick start
- Ask your PAYRYTE administrator to create your merchant account and give you a test key (starts with
sk_test_). - Create a payment from your server with
POST /payments. - Redirect your customer to the
checkout_urlin the response. - When they come back to your
return_url, check the payment withGET /payments/:idbefore delivering the order. - When everything works, switch to your live key (
sk_live_).
Base URL: https://payryte.lovable.app/api/public/v1
Authentication
Send your secret key in the Authorization header on every request:
Authorization: Bearer sk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx- Keys are shown once when created. We only store a scrambled version, so we cannot show it again — create a new one if lost.
- Keep keys on your server only. Never put them in a website, mobile app or public code repository.
- If a key leaks, ask your administrator to revoke it; it stops working immediately.
Test & live mode
Test mode — sk_test_
No real money moves. Every payment method is simulated on the checkout page so you can try successful payments end to end. Use card 4242 4242 4242 4242, any future expiry, any CVC.
Live mode — sk_live_
Real customers, real money. A method only works once PAYRYTE has connected that payment partner; otherwise it is refused rather than faked.
Test and live data are fully separate: a test key never sees live payments and vice versa.
Amounts & currencies
Amounts are whole numbers in the smallest unit of the currency. No decimals.
| Currency | You send | Means |
|---|---|---|
| XAF, XOF | 15000 | 15,000 FCFA (no sub-unit) |
| EUR, USD, GBP, NGN, GHS, KES | 1999 | 19.99 |
Payments are settled to your account in your settlement currency (USD by default). The converted figure appears as settlement_amount.
Create a payment
| Field | Required | Description |
|---|---|---|
| amount | Yes | Whole number in minor units, above the currency minimum. |
| currency | Yes | XAF, XOF, EUR, USD, GBP, NGN, GHS or KES. |
| description | No | Shown to the customer. Max 200 characters. |
| customer_email | No | Customer's email address. |
| return_url | No | Where the customer goes after paying. Must be a full https URL. |
| metadata | No | Your own key/value notes (e.g. order_id). Returned unchanged. |
Idempotency-Key header (recommended): send a unique value per order, such as your order number. If the request is repeated (network retry, double click), you get the same payment back instead of a duplicate.
Response 201: the payment object plus checkout_url and available_methods.
Retrieve a payment
Returns the current payment object. Always use this to confirm a payment before delivering goods.
List payments
Your most recent payments, newest first. limit is 1–100 (default 20). Response: { "data": [ ... ] }
Cancel a payment
Cancels a payment that hasn't been paid yet. The checkout link stops working. Already paid payments return 409.
Currencies & methods
No key needed. Lists supported currencies, decimals, reference rates, and which methods work for each currency:
| Method ID | Name | Currencies |
|---|---|---|
| card | Visa / Mastercard | All 8 |
| mtn_momo | MTN Mobile Money | XAF, XOF, GHS |
| orange_money | Orange Money | XAF, XOF |
| payryte_wallet | PAYRYTE wallet | USD |
| bank_transfer | Bank transfer | XAF, EUR, USD |
| crypto | Crypto (BTC, ETH, USDT on Ethereum or Tron) — confirmed on the blockchain | All 8 |
| cashapp | Cash App (test mode simulated; live mode pending a collection channel) | USD |
| zelle | Zelle (test mode simulated; live mode pending a collection channel) | USD |
| gift_card | Gift card (test mode simulated; live mode pending a collection channel) | USD |
Your customer only sees methods that both support the currency and are turned on for your merchant account.
Payment object
{
"id": "6f1c2a0e-8b1d-4c55-9a0b-2f9e5d1c7a33",
"mode": "test",
"amount": 1999,
"currency": "USD",
"settlement_amount": 1999,
"settlement_currency": "USD",
"method": null,
"status": "requires_payment",
"description": "Order #1042",
"customer_email": "ama@example.com",
"metadata": { "order_id": "1042" },
"return_url": "https://shop.example/thanks?order=1042",
"failure_reason": null,
"checkout_url": "https://payryte.lovable.app/hosted/6f1c2a0e-...",
"created_at": "2026-11-06T02:40:00Z",
"updated_at": "2026-11-06T02:40:00Z"
}| status | Meaning |
|---|---|
| requires_payment | Created, waiting for the customer to pay. |
| succeeded | Paid. Safe to deliver. method shows how they paid. |
| failed | The payment was refused. See failure_reason. |
| canceled | You canceled it before it was paid. |
Payment updates (webhooks)
PAYRYTE sends a signed POST request to your server the moment a payment succeeds or fails, so you don't have to keep checking.
1. Set up
- Create a page on your server that accepts
POSTrequests, e.g.https://shop.example/payryte/webhook. It must use https. - Send the address to your PAYRYTE administrator. Test and live mode each have their own address.
- You receive a signing secret (starts with
whsec_). It is shown once. Store it on your server like a password. - Ask for a test event to check that everything works.
2. Events
| type | When |
|---|---|
| payment.succeeded | The customer paid. Deliver the order. |
| payment.failed | The payment was refused or expired. |
| webhook.test | Sent on request to check your setup. Ignore it. |
POST /payryte/webhook
Content-Type: application/json
Payryte-Event-Id: evt_3b1f...
Payryte-Signature: t=1762396800,v1=5f2c8a...
{
"id": "evt_3b1f...",
"type": "payment.succeeded",
"mode": "live",
"created": 1762396800,
"data": { "object": { /* the payment object, see above */ } }
}3. Verify the signature (required)
Anyone can send a request to your address, so always check it came from PAYRYTE:
- Read the raw request body exactly as received (before turning it into JSON).
- From
Payryte-Signature, taket(time) andv1(signature). - Compute HMAC-SHA256 of
`${t}.${rawBody}`with your signing secret, as hex. - Compare with
v1using a constant-time comparison. Reject if different, or iftis more than 5 minutes old.
import crypto from "node:crypto";
import express from "express";
const app = express();
app.post("/payryte/webhook", express.raw({ type: "application/json" }), (req, res) => {
const raw = req.body.toString("utf8");
const parts = Object.fromEntries((req.get("Payryte-Signature") || "").split(",").map((p) => p.split("=")));
const expected = crypto.createHmac("sha256", process.env.PAYRYTE_WEBHOOK_SECRET)
.update(`${parts.t}.${raw}`).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
const ok = parts.v1 && parts.v1.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
if (!ok || !fresh) return res.status(400).send("bad signature");
const event = JSON.parse(raw);
res.sendStatus(200); // answer first, then work
if (event.type === "payment.succeeded") markOrderPaid(event.data.object.metadata.order_id);
});4. Answer quickly, handle repeats
- Reply with any 2xx status within 10 seconds. Do slow work (emails, stock updates) afterwards.
- Anything else — an error, a redirect, a timeout — is retried after about 5 min, then 30 min, 2 h, 6 h, 12 h and 24 h (8 tries over about 2 days). After that it is marked failed; your administrator can resend it.
- The same event may arrive more than once. Save each
Payryte-Event-Idyou've handled and skip repeats. - Events may arrive out of order. If in doubt, confirm with
GET /payments/:id.
Backup: still check your return_url visits with GET /payments/:id before delivering, in case your server was down.
Errors
Errors use standard HTTP codes and always have the same shape:
{ "error": { "message": "currency: Unsupported currency" } }| Code | Name | What to do |
|---|---|---|
| 400 | Bad request | A field is missing or wrong (for example an unsupported currency, a decimal amount, or an amount below the minimum). The message names the field. |
| 401 | Invalid API key | The key is missing, mistyped, revoked, or the merchant account is not active. Check the Authorization header. |
| 404 | Payment not found | The ID doesn't exist, belongs to another merchant, or was created in the other mode (test vs live). |
| 409 | No longer cancelable | You tried to cancel a payment that is already paid, failed or canceled. |
| 500 | Server error | Something failed on our side. Retry with the same Idempotency-Key — you will not create a duplicate. |
Code examples
Create a $19.99 payment and send the customer to checkout:
curl -X POST https://payryte.lovable.app/api/public/v1/payments \
-H "Authorization: Bearer sk_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order_1042" \
-d '{"amount": 1999, "currency": "USD", "description": "Order #1042",
"return_url": "https://shop.example/thanks?order=1042"}'Confirm a payment:
curl https://payryte.lovable.app/api/public/v1/payments/PAYMENT_ID \
-H "Authorization: Bearer sk_test_YOUR_KEY"