MyroPay API v3.4
Complete payments infrastructure for Africa and beyond — send money, collect payments, hold funds in escrow, pay bills, run payroll, and build full checkout experiences through a unified REST API.
Authentication
There are three credentials in MyroPay. Only one of them is for building a third-party integration — the other two are for a person managing their own account.
| Credential | Prefix | Who it's for | Where to get it |
|---|---|---|---|
| User session | Bearer eyJ... | An individual logged into the personal or business app in their own browser/app session | Login flow (JWT, 15 min, refreshable) |
| Merchant App keys | pk_live_... / sk_live_... | Third-party integrations — plugins, marketplaces, social-commerce platforms, anything accepting payments on behalf of a MyroPay business | business.myropay.com → API & Apps |
# User session — logged-in individual only
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
# Merchant App secret key — this is what a third-party integration sends
X-API-Key: sk_live_...
Each Merchant App key can optionally be restricted to a list of scopes at creation (or left unscoped for full access to everything below). Unscoped is the default so a first integration isn't blocked by a permission it forgot to request.
| Scope | Grants |
|---|---|
| charges:create / charges:read | Create and read deposits, checkout sessions, and payment links |
| payouts:create / payouts:read | Initiate withdrawals and manage saved banks / read payout history |
| wallet:read / wallet:transfer | Read balances / send a P2P wallet transfer or fulfill a payment request |
| escrow:manage | Create, release, dispute, and respond to escrow |
| invoices:manage | Create, send, and manage invoices |
| subscriptions:create / subscriptions:read | Manage recurring plans and subscriptions |
| transactions:read | Read transaction history |
| webhooks:manage | Create and manage webhook endpoints for this app |
Errors & Rate Limiting
| HTTP Code | Meaning | Common cause |
|---|---|---|
| 200 | Success | All good |
| 400 | Bad Request | Missing or malformed fields |
| 401 | Unauthorized | Missing, expired or invalid token |
| 403 | Forbidden | KYC required, wrong account type, wrong PIN |
| 404 | Not Found | Resource not found or doesn't belong to you |
| 409 | Conflict | Duplicate idempotency key or already exists |
| 429 | Rate Limited | Too many requests — see retry_after |
| 503 | Unavailable | Database momentarily unavailable |
// All errors follow this shape
{ "success": false, "message": "Insufficient balance.", "retry_after": 42 }
Idempotency. Financial endpoints accept X-Idempotency-Key header. Send a UUID — repeat calls within 24h return the original response.
Rate limits. Login: 10/5 min · Register: 5/5 min · OTP resend: 3/5 min · General: 300/min.
Checkout SDK
One script tag. Four integration modes. No gateway keys ever touch your frontend.
Install
Add to any HTML page. The global MyroPay object is available immediately after the script loads.
<script src="https://dev.myropay.com/myropay.js"></script>
MyroPay.checkout({
publicKey: 'pk_live_...',
amount: 5000,
currency: 'NGN',
orderRef: 'ORD-001',
description: 'Premium plan',
customer: { email: 'buyer@email.com', name: 'John Doe' },
escrow: true,
escrowCondition: 'Software delivered and tested',
onSuccess: (data) => console.log('Paid:', data),
onClose: () => console.log('Closed'),
onError: (msg) => console.error(msg),
});
const session = await MyroPay.createSession({
secretKey: 'sk_live_...',
amount: 15000, currency: 'NGN',
orderRef: 'ORD-002',
successUrl: 'https://yoursite.com/thanks',
cancelUrl: 'https://yoursite.com/cart',
});
res.redirect(session.checkout_url);
const { destroy } = await MyroPay.inline({
container: '#checkout-box',
publicKey: 'pk_live_...',
amount: 2500, currency: 'NGN',
});
MyroPay.createButton({
container: '#pay-here',
label: 'Pay with MyroPay',
publicKey: 'pk_live_...',
amount: 5000, currency: 'NGN',
});
const result = await MyroPay.verify(sessionId, 'sk_live_...');
if (result.status === 'completed' && result.amount === expectedAmount) {
// fulfil order
}
Server SDKs
Official libraries for creating and verifying charges from your own backend — thin wrappers around the same hosted-checkout session API the JS Checkout SDK uses. Every method below maps 1:1 onto POST /api/sessions.php?action=create and GET /api/sessions.php?session_id=... on checkout.myropay.com.
// npm install myropay-node
const myropay = require('myropay-node')('YOUR_SECRET_KEY');
const charge = await myropay.charges.create({
email: "customer@email.com",
amount: 500000,
});
res.redirect(charge.checkoutUrl);
# pip install myropay
import myropay
myropay.api_key = 'YOUR_SECRET_KEY'
charge = myropay.Charge.create(
email="customer@email.com",
amount=500000
)
print(charge.checkout_url)
// composer require myropay/myropay-php
require_once('vendor/autoload.php');
\Myropay\Myropay::setApiKey('YOUR_SECRET_KEY');
$charge = \Myropay\Charge::create([
'email' => 'customer@email.com',
'amount' => 500000
]);
header('Location: ' . $charge['checkout_url']);
# gem install myropay
require 'myropay'
Myropay.api_key = 'YOUR_SECRET_KEY'
charge = Myropay::Charge.create(
email: 'customer@email.com',
amount: 500000
)
<!-- Maven: pom.xml -->
<dependency>
<groupId>com.myropay</groupId>
<artifactId>myropay-java</artifactId>
<version>1.0.0</version>
</dependency>
// Gradle
implementation 'com.myropay:myropay-java:1.0.0'
import com.myropay.Myropay;
import com.myropay.model.Charge;
import java.util.HashMap;
import java.util.Map;
Myropay.apiKey = "YOUR_SECRET_KEY";
Map<String, Object> params = new HashMap<>();
params.put("email", "customer@email.com");
params.put("amount", 500000);
Charge charge = Charge.create(params);
This calls the real POST /sessions.php?action=create endpoint in test mode using a demo merchant account — no card details, no real money, no account needed. It's the same request the Node.js snippet above makes, just run for you.
E-commerce Plugins
Drop-in payment gateway extensions for the platforms merchants already run on. Each one redirects to hosted checkout, then confirms the order two independent ways — a fast return-redirect for UX, and a webhook as the authoritative source of truth — so a closed browser tab can never strand an order as unpaid.
| Platform | Status | Notes |
|---|---|---|
| WooCommerce | Available | Standard WC_Payment_Gateway, HPOS-compatible |
| OpenCart | Available | OpenCart 3.0.x–3.x, installable via Extension Installer or manual copy |
| BigCommerce | Planned | Requires app-marketplace registration/review |
| Magento | Planned | Most involved build of the group — full module + admin DI config |
| Wix | Planned | Requires app-marketplace registration/review |
| Weebly / Ecwid | Planned |
Mobile SDKs
Thin hosted-checkout wrappers for iOS and Android — no card data, secret keys, or payment logic lives on-device. Your own backend creates the session server-side (same X-API-Key call the server SDKs make); the mobile SDK just presents the resulting checkout_url in a native in-app browser and reports when it redirects back into your app.
MyroPayCheckout.shared.present(
checkoutURL: checkoutURL,
callbackURLScheme: "myshop-checkout",
presentationContext: self
) { result in /* verify session_id against your backend */ }
MyroPayCheckout.redirectListener = MyroPayRedirectListener { uri ->
// verify uri.myroPaySessionId() against your backend
}
MyroPayCheckout.present(context, checkoutUrl)
Subscriptions
Recurring billing on top of Checkout Sessions — fills in the session_type: subscription value that already existed in the session schema. Create a plan, subscribe a customer, and each billing cycle produces an invoice.
POST /api/subscriptions.php?action=create-plan
{
"name": "Pro Monthly",
"amount": 15000,
"currency": "NGN",
"interval": "monthly",
"trial_days": 7
}
POST /api/subscriptions.php?action=subscribe
{
"plan_id": "plan_live_...",
"customer": { "email": "customer@email.com" },
"billing_mode": "invoice",
"success_url": "https://yourshop.com/thanks"
}
| Billing mode | How it charges |
|---|---|
| invoice (default) | New checkout session each cycle — works with every existing payment method (card, bank transfer, USSD, wallet) |
| wallet_auto | Silent debit from a MyroPay wallet balance each cycle — reuses the existing wallet-transfer bookkeeping |
Auth Endpoints
Create a personal or business account. Returns access + refresh tokens immediately.
Authenticate with email + password. Include totp_code if 2FA enabled.
Exchange refresh token for new access + refresh pair. Tokens rotate on every use.
Returns authenticated user profile: balance, KYC status, pin_set, referral code.
Verify email with 6-digit OTP. Body: { "otp": "123456" }
Trigger password reset email. Always returns 200 to prevent enumeration.
Complete password reset. Body: { "token": "...", "password": "..." }
Change @myrotag. Max 3 changes per year. Body: { "myrotag": "newhandle" }
Generate TOTP secret and QR code URI for authenticator app setup.
Activate 2FA. Returns 10 recovery codes — store immediately.
Disable 2FA with current TOTP code. Body: { "code": "123456" }
Revoke refresh token. Body: { "refresh_token": "..." }
Wallet & Transfers
Returns balance, locked funds, currency, monthly in/out totals. Scope: wallet:read.
Send money to @myrotag, email, or phone. Requires PIN. Fee: 0.1%. Supports X-Idempotency-Key. Scope: wallet:transfer.
Find user by @myrotag, email, or phone. Scope: wallet:read.
Request payment from another user. Scope: wallet:read.
Pay a received payment request. Body: { "request_id": 14, "pin": "1234" }. Scope: wallet:transfer.
Paginated transaction history. Params: page, limit, type, q, from, to. Scope: wallet:read.
Deposits
Smart gateway init. Pass method to force: auto | flutterwave | checkout. Scope: charges:create.
Flutterwave direct card charge — preserves your own card form UI. Returns auth mode: pin, otp, or redirect. Scope: charges:create.
Validate PIN or OTP for a pending FLW charge. Body: { "flw_ref": "...", "otp": "..." }. Scope: charges:create.
Verify FLW payment after redirect. Body: { "tx_ref": "...", "transaction_id": "..." }. Scope: charges:create.
Server-to-server FLW event. Validated via VERIF-HASH header.
Payouts
Supported banks. NG uses Paystack; other countries use Flutterwave. Scope: payouts:read.
Verify account number before adding. Params: account_number, bank_code. Nigeria only. Scope: payouts:read.
Save bank account. Numbers encrypted AES-256-CBC at rest. Max 5 accounts. Scope: payouts:create.
List saved bank accounts. Account numbers are masked in response. Scope: payouts:read.
Withdraw to bank, PayPal, or mobile money. KYC required. Fee: max($1, 1%). Scope: payouts:create.
Withdrawal history for current user. Scope: payouts:read.
Transactions
Paginated transaction list. Params: page, limit, type, q, from, to. Scope: transactions:read.
Single transaction with full counterpart details. Scope: transactions:read.
Export up to 5,000 transactions. Params: from, to. Scope: transactions:read.
Lifetime and monthly totals by type for dashboard stats. Scope: transactions:read.
Notifications
Paginated notifications with unread count. Param: page.
Returns { "count": 4 } — fast endpoint for polling badge counts.
Mark all notifications as read.
Mark a single notification as read. Body: { "id": 42 }
Escrow
Lock funds until delivery conditions are met. Either party can raise a dispute. MyroPay resolves within 48 hours. Fee: 1.5%.
Create and fund escrow. Amount + 1.5% fee debited and locked immediately. Requires PIN. Scope: escrow:manage.
Release funds to beneficiary. Initiator only. Body: { "uuid": "...", "pin": "..." }. Scope: escrow:manage.
Raise a dispute. Either party. Body: { "uuid": "...", "reason": "...", "evidence": "https://..." }. Scope: escrow:manage.
Submit response to open dispute. Body: { "dispute_uuid": "...", "response": "..." }. Scope: escrow:manage.
All escrows where you are initiator or beneficiary.
Full escrow details with counterpart info.
KYC Verification
| Level | Label | Daily limit | Requirement |
|---|---|---|---|
| 0 | Starter | $200 | Email verified |
| 1 | Standard | $5,000 | Government ID + selfie |
| 2 | Verified | $50,000 | Address proof + BVN (NG) |
Current KYC level, document status per tier, daily limits.
Multipart form-data. Fields: document (file), doc_type, tier. Max 5MB. JPEG/PNG/PDF.
Nigerian BVN verification. Hashed SHA-256 immediately — never stored plain.
Invoices Business accounts
Create invoice with line items and tax. Returns invoice number and payment link. Scope: invoices:manage.
Email invoice PDF to client. Changes status draft → sent. Scope: invoices:manage.
All invoices. Filter by status: draft | sent | paid | overdue. Scope: invoices:manage.
Full invoice with parsed line items array. Scope: invoices:manage.
Payroll
Bulk transfer to multiple MyroPay users in one request. Recipients credited instantly.
Payroll batches with totals, recipient count, and completion status.
Analytics
KPI summary: revenue, spending, net, transaction count, invoice stats, payroll totals.
Daily inflow/outflow series for charting.
Top 10 users by amount sent to your account in last 90 days.
Breakdown by transaction type for pie/donut charts.
Checkout API
Create a checkout session. Returns session_id and checkout_url.
Verify a session. Always call server-side before fulfilling orders. Check status === "completed" and amount.
Pay a session with MyroPay wallet balance. Body: { "session_id": "...", "pin": "..." }
Pay a session via Flutterwave direct card charge. Returns auth mode for 3DS/OTP challenge.
Submit OTP/PIN for pending card payment. Body: { "flw_ref": "...", "otp": "...", "session_id": "..." }
API Keys
A Merchant App is one pk_live_.../sk_live_... key pair. Create it once and it authenticates deposits, escrow, payouts, wallet, transactions, invoices, subscriptions, and checkout — this is the single integration surface for a third-party platform.
Body: { "app_name", "mode": "live"|"test", "scopes"?, "allowed_ips"?, "rate_limit_per_min"?, "webhook_url"?, "webhook_events"? }. Returns public_key, secret_key, webhook_id, webhook_secret — store the secret key and webhook secret immediately, neither is shown again. mode: "live" requires KYB verification; test keys are unrestricted.
Every app on the account with scopes, IP allow-list, and rate limit — secret keys are never included.
Update scopes / allowed_ips / rate_limit_per_min in one call. Requires PIN.
Issues a new secret key immediately; the old one keeps working for grace_hours (default 24, max 72, set 0 to kill it instantly). Requires PIN.
Permanently destroys the key pair — cannot be undone. Requires PIN.
Webhook Management
Manage the webhook(s) attached to a specific app. See Webhook Events for the event catalogue and signature verification.
Body: { "app_id", "url", "events": [...] }. app_id is required — a webhook must belong to a specific app. Returns the whsec_... secret once. Scope: webhooks:manage.
List webhooks; optionally filter to one app. Scope: webhooks:manage.
Sends a signed test.ping event to the endpoint and returns the HTTP status it responded with. Scope: webhooks:manage.
Removes a webhook endpoint. Scope: webhooks:manage.
Settings
Update name and phone. Body: { "first_name", "last_name", "phone" }
Change password. Revokes all other sessions. Body: { "current_password", "new_password" }
Set or change 4-digit transaction PIN. Body: { "pin", "confirm_pin", "current_pin?" }
Toggle preference. Body: { "key": "push_notifications", "value": 1 }
Active login sessions with device, IP, last used time.
Log out all other sessions. No body required.
Webhook Events
Every webhook secret starts with whsec_ and belongs to exactly one app — you get it once, in the same response as that app's pk_/sk_ pair (see API Keys). Store it as MYROPAY_WEBHOOK_SECRET next to MYROPAY_SECRET_KEY — see Environment Variables below for the full naming reference.
// PHP — always verify before processing
$rawBody = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_MYROPAY_SIGNATURE'] ?? '';
// $_ENV['MYROPAY_WEBHOOK_SECRET'] — the whsec_... value returned when
// you created this app (or its webhook) under API Keys. Not the sk_/pk_
// key itself — a distinct secret, one per webhook endpoint.
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $_ENV['MYROPAY_WEBHOOK_SECRET']);
if (!hash_equals($expected, $sig)) {
http_response_code(401); exit;
}
http_response_code(200); // ack fast, process async
$event = json_decode($rawBody, true);
| Event | Trigger |
|---|---|
| payment.success | Checkout session paid and completed |
| payment.failed | Payment attempt abandoned or declined |
| deposit.confirmed | Wallet deposit credited (any gateway) |
| transfer.received | P2P transfer credited to wallet |
| payout.completed | Withdrawal processed and sent |
| payout.failed | Withdrawal failed — funds returned |
| escrow.funded | Escrow created and funds locked |
| escrow.released | Initiator releases to beneficiary |
| escrow.disputed | Either party raises a dispute |
| escrow.resolved | Admin resolves dispute |
| kyc.approved | KYC document approved |
| kyc.rejected | KYC document rejected |
| invoice.paid | Invoice paid via checkout link |
| invoice.overdue | Invoice past due date |
| payroll.completed | Payroll batch fully processed |
| subscription.created | A subscription is created |
| subscription.cancelled | Cancelled immediately or at period end |
| subscription_invoice.created | A subscription billing cycle's invoice is generated |
| subscription_invoice.paid | Subscription invoice paid (invoice or wallet_auto mode) |
| subscription_invoice.payment_failed | Wallet debit declined, or invoice unpaid past its due window |
Environment Variables
The exact variable names to use when wiring MyroPay into a codebase or briefing an AI coding agent. Copy this table verbatim — it's the single source of truth this whole site should agree with.
| Variable | Value | Notes |
|---|---|---|
| MYROPAY_PUBLIC_KEY | pk_live_... / pk_test_... | Safe in frontend code. From API & Apps. |
| MYROPAY_SECRET_KEY | sk_live_... / sk_test_... | Server-side only. This is the one credential that authenticates everything — deposits, escrow, payouts, wallet, invoices, subscriptions, checkout. |
| MYROPAY_WEBHOOK_SECRET | whsec_... | Server-side only. Generated together with the key pair above, or via Webhook Management. Used only to verify X-MyroPay-Signature. |
There is also no separate "platform API key." MYROPAY_PLATFORM_API_KEY isn't a MyroPay concept — there's only MYROPAY_SECRET_KEY per Merchant App, scoped to the merchant that created it. If an AI agent invents a *_PLATFORM_* variable, it's guessing at a naming convention MyroPay doesn't have; point it back at this table.
Changelog
- Fixed: Merchant App secret keys (sk_live_/sk_test_) were only ever accepted by the Checkout API — every other endpoint (deposits, escrow, payouts, wallet, transactions, invoices) silently rejected them and fell back to checking a personal mrp_ key instead. A single sk_ key now authenticates the entire API surface, with scopes, IP allow-list, and rate limit enforced everywhere.
- New scopes: invoices:manage, wallet:transfer.
- Webhook secrets (whsec_...) are now generated together with a Merchant App's key pair instead of as a separate, unscoped step — pass webhook_url when creating the app. Standalone webhook creation now requires app_id.
- API keys (merchant) can no longer create, rotate, reveal, or revoke other API keys, or manage team members — that always requires a real dashboard session now, closing a self-escalation gap.
- Docs: clarified that app.myropay.com (personal wallet) is unrelated to building an integration, added the missing API Keys / Webhook Management pages, and added an Environment Variables reference.
- Checkout SDK (myropay.js): createSession() and verify() removed — both required a secret key in browser JS, which the current secret-key-only backend can't support safely from a client. Use a Server SDK instead. checkout()/inline()/createButton() unaffected.
- All 5 server SDKs (Node.js, Python, Java, Ruby, PHP) now actually published to their package registries via CI — install commands on this page are live
- WooCommerce and OpenCart (3.0.x–3.x) payment gateway plugins released — dual return-redirect + webhook confirmation
- Mobile SDKs released for iOS (Swift Package, ASWebAuthenticationSession) and Android (Kotlin, Custom Tabs) — hosted-checkout wrappers, no card data on-device
- Subscriptions API released: plans, recurring invoices, wallet auto-billing, dunning/retry — fills in the previously-unused session_type: subscription value
- New webhook events: subscription.created, subscription.cancelled, subscription_invoice.created, subscription_invoice.paid, subscription_invoice.payment_failed
- Known issue flagged: Checkout SDK (myropay.js) sends the wrong auth header for createSession()/verify() — fix pending, see Server SDKs section
- Planned next: BigCommerce, Magento, Wix, Weebly, Ecwid plugins; card tokenization for true auto-charge subscriptions
- Flutterwave Direct Charge API integrated — preserves custom card form UI, no modal redirect needed
- Checkout SDK (myropay.js) published with 4 integration modes: modal, redirect, inline, button
- Escrow now supported in Checkout sessions — escrow: true param locks funds on payment
- Admin dashboard: dispute resolution, withdrawal queue, KYC review, FX rate management, audit log
- Business analytics API: summary, volume chart, top senders, transaction type breakdown
- Team management: invite by email, role-based access (admin/finance/developer/support)
- Auto-save cron for savings goals — daily/weekly/monthly frequencies
- Token refresh rotation — refresh tokens invalidated on each use
- WebAuthn credential columns added to users table (implementation ready)
- Rate limiting now DB-backed with sliding window per identifier+action
- Multi-subdomain architecture: app, business, checkout, admin, dev
- Business account type with payroll, invoicing, merchant apps
- Escrow system with dispute resolution workflow
- Virtual cards with AES-256-CBC encryption at rest
- Webhook delivery with exponential backoff retry
- Mono bank linking integration for Nigerian accounts
- Multi-currency wallet display