PAYRYTEDeveloper Guide

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

  1. Ask your PAYRYTE administrator to create your merchant account and give you a test key (starts with sk_test_).
  2. Create a payment from your server with POST /payments.
  3. Redirect your customer to the checkout_url in the response.
  4. When they come back to your return_url, check the payment with GET /payments/:id before delivering the order.
  5. 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.

CurrencyYou sendMeans
XAF, XOF1500015,000 FCFA (no sub-unit)
EUR, USD, GBP, NGN, GHS, KES199919.99

Payments are settled to your account in your settlement currency (USD by default). The converted figure appears as settlement_amount.

Create a payment

POST/payments
FieldRequiredDescription
amountYesWhole number in minor units, above the currency minimum.
currencyYesXAF, XOF, EUR, USD, GBP, NGN, GHS or KES.
descriptionNoShown to the customer. Max 200 characters.
customer_emailNoCustomer's email address.
return_urlNoWhere the customer goes after paying. Must be a full https URL.
metadataNoYour 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

GET/payments/:id

Returns the current payment object. Always use this to confirm a payment before delivering goods.

List payments

GET/payments?limit=20

Your most recent payments, newest first. limit is 1–100 (default 20). Response: { "data": [ ... ] }

Cancel a payment

DELETE/payments/:id

Cancels a payment that hasn't been paid yet. The checkout link stops working. Already paid payments return 409.

Currencies & methods

GET/currencies

No key needed. Lists supported currencies, decimals, reference rates, and which methods work for each currency:

Method IDNameCurrencies
cardVisa / MastercardAll 8
mtn_momoMTN Mobile MoneyXAF, XOF, GHS
orange_moneyOrange MoneyXAF, XOF
payryte_walletPAYRYTE walletUSD
bank_transferBank transferXAF, EUR, USD
cryptoCrypto (BTC, ETH, USDT on Ethereum or Tron) — confirmed on the blockchainAll 8
cashappCash App (test mode simulated; live mode pending a collection channel)USD
zelleZelle (test mode simulated; live mode pending a collection channel)USD
gift_cardGift 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"
}
statusMeaning
requires_paymentCreated, waiting for the customer to pay.
succeededPaid. Safe to deliver. method shows how they paid.
failedThe payment was refused. See failure_reason.
canceledYou 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

  1. Create a page on your server that accepts POST requests, e.g. https://shop.example/payryte/webhook. It must use https.
  2. Send the address to your PAYRYTE administrator. Test and live mode each have their own address.
  3. You receive a signing secret (starts with whsec_). It is shown once. Store it on your server like a password.
  4. Ask for a test event to check that everything works.

2. Events

typeWhen
payment.succeededThe customer paid. Deliver the order.
payment.failedThe payment was refused or expired.
webhook.testSent 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:

  1. Read the raw request body exactly as received (before turning it into JSON).
  2. From Payryte-Signature, take t (time) and v1 (signature).
  3. Compute HMAC-SHA256 of `${t}.${rawBody}` with your signing secret, as hex.
  4. Compare with v1 using a constant-time comparison. Reject if different, or if t is 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-Id you'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" } }
CodeNameWhat to do
400Bad requestA field is missing or wrong (for example an unsupported currency, a decimal amount, or an amount below the minimum). The message names the field.
401Invalid API keyThe key is missing, mistyped, revoked, or the merchant account is not active. Check the Authorization header.
404Payment not foundThe ID doesn't exist, belongs to another merchant, or was created in the other mode (test vs live).
409No longer cancelableYou tried to cancel a payment that is already paid, failed or canceled.
500Server errorSomething 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"