MyroPay API Reference
The MyroPay API is a RESTful HTTP API. It uses JSON request bodies, returns JSON responses, and uses standard HTTP status codes. All endpoints require HTTPS.
Base URLs
# Checkout API (SDK-facing — auth via checkout key)
https://checkout.myropay.com/api/
# Business API (auth via JWT or business key)
https://business.myropay.com/api/
# Personal App API (auth via JWT)
https://app.myropay.com/api/
ck_test_* or sk_test_* keys to test against our sandbox at https://sandbox.myropay.com/api/ (same paths as the live Checkout API; live keys are rejected there and test keys are rejected on checkout.myropay.com). No real money moves. Card and wallet are simulated; bank transfer, mobile money and PayPal are live-only.
Authentication
MyroPay supports two authentication methods:
1. Checkout app key (server-side)
Pass your checkout secret key in the X-Checkout-Key header for all Checkout API calls.
X-Checkout-Key: ck_sec_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
2. JWT Bearer token (dashboard / business API)
Obtain a JWT via POST /api/auth.php?action=login then pass in the Authorization header:
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOjEsImFjdG9yX3R5cGUiOiJidXNpbmVzcyJ9.xxx
ck_sec_*, sk_live_*) in client-side code, browser JavaScript, or mobile apps. Use publishable keys (ck_pub_*) on the client side only.
Key types
| Key prefix | Type | Use case | Expiry |
|---|---|---|---|
| ck_pub_ | Publishable test | Client-side JS only | Never |
| ck_sec_ | Secret test | Server-side only | Never (rotatable) |
| pk_live_ | Publishable live | Client-side (KYB required) | Never |
| sk_live_ | Secret live | Server-side only (KYB required) | Never (rotatable) |
| ck_wh_ | Webhook secret | HMAC-SHA256 signature verification | Never (rotatable) |
Errors & responses
All API responses share a consistent JSON envelope:
{
"success": true,
"message": "Session created",
"data": { /* response payload */ }
}
{
"success": false,
"message": "amount must be greater than 0",
"error_code": "VALIDATION_ERROR",
"status_code": 422
}
| HTTP code | Meaning | Retry? |
|---|---|---|
| 200 | Success | — |
| 201 | Created | — |
| 400 | Bad request / unknown action | No |
| 401 | Authentication failed | No |
| 403 | Forbidden (KYB/KYC required) | No |
| 404 | Resource not found | No |
| 409 | Conflict (duplicate) | No |
| 410 | Gone (session expired) | No |
| 422 | Validation failed | No (fix params) |
| 429 | Rate limit exceeded | Yes (after retry-after) |
| 500 | Internal server error | Yes (with backoff) |
| 502 | Upstream gateway error | Yes (with backoff) |
Pagination
List endpoints return paginated results. Pass page and per_page as query parameters:
GET /api/invoices.php?action=list&page=2&per_page=20
{
"success": true,
"data": {
"data": [ /* array of items */ ],
"total": 143,
"page": 2,
"per_page": 20,
"total_pages": 8,
"has_next": true,
"has_prev": true
}
}
Rate limits
| Endpoint group | Default limit | Window |
|---|---|---|
| Authentication | 10 requests | 10 minutes |
| Checkout session create | 100 requests | 1 minute |
| Transfers / withdrawals | 20 requests | 1 minute |
| General API | 300 requests | 1 minute |
| Analytics endpoints | 60 requests | 1 minute |
Rate limit headers returned on every response: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset. On 429, check the Retry-After header.
Idempotency
For POST requests that create resources or execute transactions, include an idempotency_key in the request body. If the same key is submitted within 24 hours, the original response is returned without re-executing. Prevents duplicate charges from network retries.
{
"amount": 5000,
"currency_code": "NGN",
"idempotency_key": "order_9876_attempt_1"
}
Webhooks
Configure a webhook URL under API & Apps. MyroPay sends signed POST requests to your URL when events occur. Always verify the X-MyroPay-Signature header before processing. The header is sha256= followed by the hex HMAC-SHA256 of the raw request body, keyed with your whsec_ secret.
Every delivery is wrapped in the same envelope: { "event": "payment.success", "data": { ... }, "timestamp": 1790586932 }. Event names include payment.success, payment.failed and subscription_invoice.paid; the full list and every payload's fields are on the Webhook Events page.
const crypto = require('crypto');
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['x-myropay-signature'] || '';
const expected = 'sha256=' + crypto.createHmac('sha256', process.env.MYROPAY_WEBHOOK_SECRET).update(req.body).digest('hex');
const ok = sig.length === expected.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
if (!ok) return res.status(401).send('Unauthorized');
const { event, data } = JSON.parse(req.body);
if (event === 'payment.success') fulfillOrder(data.session_id, data.order_ref);
res.json({ received: true });
});
$payload = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_MYROPAY_SIGNATURE'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $payload, $_ENV['MYROPAY_WEBHOOK_SECRET']);
if (!hash_equals($expected, $sig)) {
http_response_code(401); exit;
}
$event = json_decode($payload, true);
if ($event['event'] === 'payment.success') {
fulfillOrder($event['data']['session_id'], $event['data']['order_ref']);
}
http_response_code(200);
import hmac, hashlib, os
@app.route('/webhook', methods=['POST'])
def webhook():
payload = request.get_data()
sig = request.headers.get('X-MyroPay-Signature', '')
expected = 'sha256=' + hmac.new(os.environ['MYROPAY_WEBHOOK_SECRET'].encode(), payload, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, sig):
return 'Unauthorized', 401
event = request.get_json()
if event['event'] == 'payment.success':
fulfill_order(event['data']['session_id'], event['data']['order_ref'])
return '', 200
SDKs
npm install myropay-jscomposer require myropay/php-sdkpip install myropaynpm install @myropay/reactCheckout — Create session
Creates a new checkout session. Returns a checkout_url to redirect your customer to. Sessions expire after 30 minutes.
Request body
| Parameter | Type | Required | Description |
|---|---|---|---|
| amount | number | required | Amount to charge. In the smallest unit (kobo for NGN, cents for USD, pence for GBP) |
| currency_code | string | required | ISO 4217 code: NGN, USD, GBP, EUR, GHS, KES… |
| description | string | optional | Shown on the checkout page to the customer |
| customer_email | string | optional | Pre-fills email on the checkout form |
| customer_name | string | optional | Pre-fills name on the checkout form |
| success_url | string | optional | Redirect URL after successful payment (overrides app default) |
| cancel_url | string | optional | Redirect URL if customer cancels |
| metadata | object | optional | Key-value pairs returned in the webhook payload. Max 50 keys, 500 chars per value. |
| idempotency_key | string | optional | Prevents duplicate sessions on network retries |
import MyroPay from 'myropay-js';
const client = new MyroPay('ck_sec_live_xxxx');
const session = await client.checkout.create({
amount: 500000, // ₦5,000.00 in kobo
currency_code: 'NGN',
description: 'Order #1234',
customer_email: 'customer@example.com',
success_url: 'https://mystore.com/success',
cancel_url: 'https://mystore.com/cancel',
metadata: { order_id: 'ORD-9876' },
idempotency_key: 'order-9876-attempt-1',
});
// Redirect customer to payment page
window.location.href = session.checkout_url;
use MyroPay\Client;
$client = new Client('ck_sec_live_xxxx');
$session = $client->checkout->create([
'amount' => 500000,
'currency_code' => 'NGN',
'description' => 'Order #1234',
'customer_email' => 'customer@example.com',
'success_url' => 'https://mystore.com/success',
'cancel_url' => 'https://mystore.com/cancel',
'metadata' => ['order_id' => 'ORD-9876'],
'idempotency_key' => 'order-9876-attempt-1',
]);
header('Location: ' . $session['checkout_url']);
import myropay
client = myropay.Client("ck_sec_live_xxxx")
session = client.checkout.create(
amount=500000,
currency_code="NGN",
description="Order #1234",
customer_email="customer@example.com",
success_url="https://mystore.com/success",
cancel_url="https://mystore.com/cancel",
metadata={"order_id": "ORD-9876"},
idempotency_key="order-9876-attempt-1",
)
return redirect(session["checkout_url"])
curl -X POST https://checkout.myropay.com/api/session.php?action=create \
-H "X-Checkout-Key: ck_sec_live_xxxx" \
-H "Content-Type: application/json" \
-d '{
"amount": 500000,
"currency_code": "NGN",
"description": "Order #1234",
"customer_email": "customer@example.com",
"success_url": "https://mystore.com/success",
"cancel_url": "https://mystore.com/cancel",
"metadata": { "order_id": "ORD-9876" },
"idempotency_key": "order-9876-attempt-1"
}'
{
"success": true,
"data": {
"session_id": 42,
"session_token": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"checkout_url": "https://checkout.myropay.com/pay/a1b2c3d4e5f6g7h8i9j0",
"expires_at": "2025-01-15 13:04:56",
"amount": 500000,
"currency": "NGN"
}
}
Retrieves the current status of a checkout session by its token.
| Parameter | Type | Description |
|---|---|---|
| token | string | The session_token returned by create |
Immediately expires a pending session (e.g. if the customer cancels in your own UI before redirecting).
{ "token": "a1b2c3d4..." }Invoices
Returns paginated list of invoices for the authenticated business.
| Query param | Type | Description |
|---|---|---|
| page | integer | Page number (default: 1) |
| per_page | integer | Results per page (default: 20, max: 50) |
| status | string | Filter by status: draft | sent | paid | overdue | void |
| search | string | Full-text search on invoice number, client name, email |
Creates a new invoice. Returns the invoice ID and a public payment link.
| Parameter | Type | Required | Description |
|---|---|---|---|
| client_name | string | required | Client display name |
| client_email | string | required | Client email (where invoice email is sent) |
| items | array | required | Array of line item objects (see below) |
| items[].description | string | required | Line item description |
| items[].quantity | number | required | Quantity |
| items[].unit_price | number | required | Price per unit |
| items[].tax_rate | number | optional | Tax percentage (e.g. 7.5 for 7.5%) |
| currency_code | string | optional | Default: business wallet currency |
| due_date | string | optional | Due date in YYYY-MM-DD format |
| notes | string | optional | Payment terms / notes shown on invoice |
| discount_percent | number | optional | Whole-invoice discount (0–100) |
| allow_partial | boolean | optional | Allow partial online payments (default: false) |
Returns full invoice detail including line items and payment history.
Sends the invoice to the client's email. Transitions status from draft to sent.
{ "id": 42 }Voids an invoice. Cannot be undone. Cannot void a paid invoice.
Wallets
Returns all active wallets for the authenticated business, ordered by primary first.
{
"wallets": [{
"id": 1,
"currency_code": "NGN",
"currency_symbol": "₦",
"balance": "125000.00",
"locked_balance": "0.00",
"is_primary": 1,
"flw_account_number":"1234567890",
"flw_bank_name": "Wema Bank"
}]
}
Looks up a user or business by Myrotag or email. Use before initiating a transfer.
Returns the permanent NGN virtual bank account for the wallet, issued by Flutterwave. If none exists yet it is created instantly (the account owner needs a verified BVN, otherwise the call returns 412 with needs_bvn: true). Transfers into it credit the wallet automatically.
FX Rates
Returns live exchange rates for a base currency. Rates update every 30 minutes from our liquidity providers.
Preview a currency conversion with fee breakdown. Does not execute the conversion.
| Parameter | Type | Description |
|---|---|---|
| from | string | Source currency code (e.g. USD) |
| to | string | Target currency code (e.g. NGN) |
| amount | number | Amount to convert in source currency |
Analytics
Returns KPI summary for the specified time period.
| Query param | Values | Description |
|---|---|---|
| period | 7d 30d 90d 12m custom | Time window. Use custom with from and to params. |
| from | string | Start date YYYY-MM-DD (required when period=custom) |
| to | string | End date YYYY-MM-DD (required when period=custom) |
Returns time-series revenue data for charting.
| Query param | Values | Description |
|---|---|---|
| granularity | hour day week month | Data point interval |
Webhook events
Every delivery uses the envelope { "event", "data", "timestamp" }. data.session_id is the session_id returned when you created the session, and data.order_ref is the order_ref you supplied. Full field tables and the remaining events: Webhook Events.
payment.success
{
"event": "payment.success",
"data": {
"session_id": "cs_live_9f2c4e7a1b3d5f60718293a4b5c6d7e8f9a0b1c2",
"order_ref": "ORD-9876",
"status": "success",
"amount": 15000.0,
"currency": "NGN",
"gateway": "flutterwave",
"customer_email": "customer@example.com",
"escrow": false,
"is_test": false,
"metadata": { "cart_id": "c_123" },
"paid_at": "2026-09-28T10:15:32+01:00"
},
"timestamp": 1790586932
}payment.failed
{
"event": "payment.failed",
"data": {
"session_id": "cs_live_9f2c4e7a1b3d5f60718293a4b5c6d7e8f9a0b1c2",
"order_ref": "ORD-9876",
"status": "failed",
"amount": 15000.0,
"currency": "NGN",
"gateway": "flutterwave",
"customer_email": "customer@example.com",
"escrow": false,
"is_test": false,
"metadata": { "cart_id": "c_123" },
"gateway_status": "failed"
},
"timestamp": 1790586932
}subscription_invoice.paid
subscription_invoice.created and subscription_invoice.payment_failed use the same fields (status is created / failed; failures add attempt, max_attempts, will_retry, next_retry_at, subscription_status and reason; this event fires on every failed attempt, so check will_retry). Also new in v3.8: subscription.wallet_authorized and subscription.resumed carry subscription_id.
{
"event": "subscription_invoice.paid",
"data": {
"invoice_id": "inv_live_4d1e...",
"subscription_id": "sub_live_7b3a...",
"plan_id": "plan_live_2c9f...",
"status": "paid",
"amount": 15000.0,
"currency": "NGN",
"billing_mode": "card_token",
"customer_email": "customer@example.com",
"checkout_session_id": null,
"due_at": "2026-10-28 10:00:04",
"paid_at": "2026-10-28 10:00:06"
},
"timestamp": 1793178006
}transfer.received
{
"event": "transfer.received",
"data": { "amount": 10000.0, "currency": "NGN", "sent_amount": 10000.0, "sent_currency": "NGN", "from": "@adaobi" },
"timestamp": 1790586932
}payout.completed
Sent once, when a withdrawal is actually delivered: the Flutterwave transfer or PayPal item succeeded, or MyroPay's periodic check with Flutterwave confirmed it, or finance marked a manual (mobile money) payout completed. It is not sent when the withdrawal is merely queued. transaction_id is the withdrawal's uuid, as returned by initiate.
{
"event": "payout.completed",
"data": { "transaction_id": "6f1c0c52-9b1e-4c1a-8e0d-2a5f6c0b7a11", "amount": 50000.0, "fee": 500.0, "currency": "NGN", "method": "bank", "status": "completed" },
"timestamp": 1790586932
}payout.failed
The payout was rejected or returned. The full amount plus the fee has already been refunded to the wallet when this arrives.
{
"event": "payout.failed",
"data": { "transaction_id": "6f1c0c52-9b1e-4c1a-8e0d-2a5f6c0b7a11", "amount": 50000.0, "fee": 500.0, "currency": "NGN", "method": "bank", "status": "failed", "reason": "Account closed" },
"timestamp": 1790586932
}subscription.autopay_stopped
The customer turned off automatic wallet payment in their MyroPay app. The subscription switches to invoice billing: expect a hosted checkout link each cycle instead of a silent debit.
{
"event": "subscription.autopay_stopped",
"data": { "subscription_id": "sub_live_7b3a...", "billing_mode": "invoice" },
"timestamp": 1793178006
}subscription.cancel_scheduled
The customer chose to cancel from their MyroPay app. Access and billing continue until ends_at, then subscription.cancelled follows.
{
"event": "subscription.cancel_scheduled",
"data": { "subscription_id": "sub_live_7b3a...", "cancelled_by": "customer", "ends_at": "2026-11-28 10:00:04" },
"timestamp": 1793178006
}subscription.cancelled
The subscription ended. reason is cancel_at_period_end (scheduled cancel reached its date), payment_failed (all retries exhausted) or customer_cancelled (cancelled immediately by the customer; a cancel from the MyroPay app is scheduled, so it ends with cancel_at_period_end). No further invoices or charges follow.
{
"event": "subscription.cancelled",
"data": { "subscription_id": "sub_live_7b3a...", "reason": "payment_failed" },
"timestamp": 1793178006
}subscription.resumed
A scheduled cancel-at-period-end was undone with resume; the subscription keeps renewing.
{
"event": "subscription.resumed",
"data": { "subscription_id": "sub_live_7b3a..." },
"timestamp": 1793178006
}payroll.completed
{
"event": "payroll.completed",
"data": { "batch_id": "0b8f0a1e-...", "total": 1200000.0, "count": 24 },
"timestamp": 1790586932
}Live playground
Test API calls directly. Use your test secret key — no real transactions.