v3.4
API Reference

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.

Integration APIhttps://business.myropay.com/api/ Checkout APIhttps://checkout.myropay.com/api/
🧩
Building a third-party integration (a plugin, marketplace, or another platform accepting payments through MyroPay)? You only ever need two things: a pk_live_... / sk_live_... key pair from business.myropay.com → API & Apps, and the whsec_... webhook secret generated alongside them. That's the entire integration surface — deposits, escrow, payouts, subscriptions, and invoicing all sit behind those two keys. See Authentication below.
🚫
app.myropay.com is the personal consumer wallet app — not an integration surface. It's where an individual manages their own MyroPay balance. Nothing there (login, personal PIN) is meant to be shared with, or referenced by, a third-party integration. If an integration guide or AI agent asks you to point at app.myropay.com or use a personal mrp_... key to power a platform integration, that's a documentation bug — use the Business API instead.
ℹ️
All responses are JSON. Timestamps are UTC ISO 8601. Amounts are in the major currency unit (dollars, not cents). HTTPS is always required. Endpoints below live on business.myropay.com unless otherwise labeled.
Security

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.

CredentialPrefixWho it's forWhere to get it
User sessionBearer eyJ...An individual logged into the personal or business app in their own browser/app sessionLogin flow (JWT, 15 min, refreshable)
Merchant App keyspk_live_... / sk_live_...Third-party integrations — plugins, marketplaces, social-commerce platforms, anything accepting payments on behalf of a MyroPay businessbusiness.myropay.com → API & Apps
If you're integrating MyroPay into another platform, use the Merchant App keys — nothing else. One sk_live_... key now authenticates every money-movement endpoint on this site: deposits, escrow, payouts, wallet transfers, invoices, and subscriptions. You don't need a separate credential per feature.
Sending a JWT
# User session — logged-in individual only
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
Tokens expire in 15 minutes. Call POST /auth.php?action=refresh with your refresh token to get a new pair. Tokens rotate on every refresh.
Sending an API key
# Merchant App secret key — this is what a third-party integration sends
X-API-Key: sk_live_...
🔑
Both sk_live_.../sk_test_... merchant app keys go in the same X-API-Key header — never Authorization, which is reserved for JWTs. MyroPay tells them apart by prefix, so you never need to specify which kind of key you're sending.
Scopes

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.

ScopeGrants
charges:create / charges:readCreate and read deposits, checkout sessions, and payment links
payouts:create / payouts:readInitiate withdrawals and manage saved banks / read payout history
wallet:read / wallet:transferRead balances / send a P2P wallet transfer or fulfill a payment request
escrow:manageCreate, release, dispute, and respond to escrow
invoices:manageCreate, send, and manage invoices
subscriptions:create / subscriptions:readManage recurring plans and subscriptions
transactions:readRead transaction history
webhooks:manageCreate and manage webhook endpoints for this app
🔒
Neither key type can create, rotate, reveal, or delete other API keys, or manage team members — that always requires a real signed-in session on the dashboard. This is intentional: an API key that could mint other API keys would let a leaked key escalate itself.
Reference

Errors & Rate Limiting

HTTP CodeMeaningCommon cause
200SuccessAll good
400Bad RequestMissing or malformed fields
401UnauthorizedMissing, expired or invalid token
403ForbiddenKYC required, wrong account type, wrong PIN
404Not FoundResource not found or doesn't belong to you
409ConflictDuplicate idempotency key or already exists
429Rate LimitedToo many requests — see retry_after
503UnavailableDatabase 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.

Client Library

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>
Option 1 — Modal popup (recommended)
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),
});
Option 2 — Server-side redirect session
🔒
This call creates a real charge session and must run on your server — it uses your secret key, which should never reach a browser.
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);
Option 3 — Inline embed
const { destroy } = await MyroPay.inline({
  container: '#checkout-box',
  publicKey: 'pk_live_...',
  amount: 2500, currency: 'NGN',
});
Option 4 — Pre-built button
MyroPay.createButton({
  container: '#pay-here',
  label: 'Pay with MyroPay',
  publicKey: 'pk_live_...',
  amount: 5000, currency: 'NGN',
});
Verify a payment (always server-side)
const result = await MyroPay.verify(sessionId, 'sk_live_...');
if (result.status === 'completed' && result.amount === expectedAmount) {
  // fulfil order
}
Getting Started

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.

🔒
Every SDK below uses your secret key. Run this code on your server only — never in a browser, mobile app bundle, or anywhere a user could view-source it.
💵
Amounts are your currency's own whole units — 500000 for NGN means ₦500,000, not kobo. This differs from some other African payment APIs that expect subunits — double-check if you're porting integration code.
All five SDKs are now published and installable via their package managers (npm, PyPI, RubyGems, Packagist, Maven Central) — the install commands below are live, not placeholders.
Fixed in v3.3: createSession() and verify() have been removed from the Checkout SDK — both required a secret key from browser JS, which no header fix can make safe against a secret-key-only backend. Create and verify sessions from your own backend using a Server SDK instead. checkout() / inline() / createButton() were never affected.
Install
// npm install myropay-node
Create a charge
const myropay = require('myropay-node')('YOUR_SECRET_KEY');

const charge = await myropay.charges.create({
  email: "customer@email.com",
  amount: 500000,
});

res.redirect(charge.checkoutUrl);
Install
# pip install myropay
Create a charge
import myropay
myropay.api_key = 'YOUR_SECRET_KEY'

charge = myropay.Charge.create(
    email="customer@email.com",
    amount=500000
)

print(charge.checkout_url)
Install
// composer require myropay/myropay-php
Create a charge
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']);
Install
# gem install myropay
Create a charge
require 'myropay'
Myropay.api_key = 'YOUR_SECRET_KEY'

charge = Myropay::Charge.create(
    email: 'customer@email.com',
    amount: 500000
)
Install
<!-- 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'
Create a charge
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);
Playground — try a real test-mode charge

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.


  
Integrations

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.

PlatformStatusNotes
WooCommerceAvailableStandard WC_Payment_Gateway, HPOS-compatible
OpenCartAvailableOpenCart 3.0.x–3.x, installable via Extension Installer or manual copy
BigCommercePlannedRequires app-marketplace registration/review
MagentoPlannedMost involved build of the group — full module + admin DI config
WixPlannedRequires app-marketplace registration/review
Weebly / EcwidPlanned
ℹ️
Subscriptions aren't wired into either plugin yet — both handle one-time checkout only, pending the recurring-billing session type documented below.
Integrations

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.

iOS — Swift Package (ASWebAuthenticationSession)
MyroPayCheckout.shared.present(
    checkoutURL: checkoutURL,
    callbackURLScheme: "myshop-checkout",
    presentationContext: self
) { result in /* verify session_id against your backend */ }
Android — Kotlin module (Custom Tabs)
MyroPayCheckout.redirectListener = MyroPayRedirectListener { uri ->
    // verify uri.myroPaySessionId() against your backend
}
MyroPayCheckout.present(context, checkoutUrl)
🔒
Never call sessions.php directly from a mobile app — your secret key would ship inside the app bundle. Always create the session from your own backend first.
ℹ️
This is a hosted-checkout wrapper, not a native card-entry SDK. A "customer never leaves your app's native UI" experience would require card tokenization at entry and pulls your app into PCI scope — a separate, larger build.
Integrations

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.

Create a plan
POST /api/subscriptions.php?action=create-plan
{
  "name": "Pro Monthly",
  "amount": 15000,
  "currency": "NGN",
  "interval": "monthly",
  "trial_days": 7
}
Subscribe a customer
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 modeHow it charges
invoice (default)New checkout session each cycle — works with every existing payment method (card, bank transfer, USSD, wallet)
wallet_autoSilent debit from a MyroPay wallet balance each cycle — reuses the existing wallet-transfer bookkeeping
🔒
There is currently no "auto-charge a saved card" mode. That requires card tokenization, which this API doesn't implement anywhere yet — building it is a separate, security-sensitive project, not something bolted onto this release.
ℹ️
New scopes required on merchant apps: subscriptions:create, subscriptions:read.
Core API

Auth Endpoints

POST/auth.php?action=registerPublic

Create a personal or business account. Returns access + refresh tokens immediately.

POST/auth.php?action=loginPublic

Authenticate with email + password. Include totp_code if 2FA enabled.

POST/auth.php?action=refreshPublic

Exchange refresh token for new access + refresh pair. Tokens rotate on every use.

GET/auth.php?action=meJWT

Returns authenticated user profile: balance, KYC status, pin_set, referral code.

POST/auth.php?action=verify-email-otpJWT

Verify email with 6-digit OTP. Body: { "otp": "123456" }

POST/auth.php?action=forgot-passwordPublic

Trigger password reset email. Always returns 200 to prevent enumeration.

POST/auth.php?action=reset-passwordPublic

Complete password reset. Body: { "token": "...", "password": "..." }

POST/auth.php?action=update-myrotagJWT

Change @myrotag. Max 3 changes per year. Body: { "myrotag": "newhandle" }

POST/auth.php?action=setup-2faJWT

Generate TOTP secret and QR code URI for authenticator app setup.

POST/auth.php?action=confirm-2faJWT

Activate 2FA. Returns 10 recovery codes — store immediately.

POST/auth.php?action=disable-2faJWT

Disable 2FA with current TOTP code. Body: { "code": "123456" }

POST/auth.php?action=logoutJWT

Revoke refresh token. Body: { "refresh_token": "..." }

Core API

Wallet & Transfers

GET/wallet.php?action=balanceJWT / Key

Returns balance, locked funds, currency, monthly in/out totals. Scope: wallet:read.

POST/wallet.php?action=transferJWT / Key

Send money to @myrotag, email, or phone. Requires PIN. Fee: 0.1%. Supports X-Idempotency-Key. Scope: wallet:transfer.

GET/wallet.php?action=lookup-user&q=@handleJWT / Key

Find user by @myrotag, email, or phone. Scope: wallet:read.

POST/wallet.php?action=requestJWT / Key

Request payment from another user. Scope: wallet:read.

POST/wallet.php?action=fulfill-requestJWT / Key

Pay a received payment request. Body: { "request_id": 14, "pin": "1234" }. Scope: wallet:transfer.

GET/wallet.php?action=historyJWT / Key

Paginated transaction history. Params: page, limit, type, q, from, to. Scope: wallet:read.

Core API

Deposits

Use action=init for all deposits. Gateway is selected automatically: Flutterwave hosted for all countries, or MyroPay Checkout as a fallback. Paystack was removed from the deposit path — third-party integrators should not pass method=paystack.
POST/deposit.php?action=initJWT / Key

Smart gateway init. Pass method to force: auto | flutterwave | checkout. Scope: charges:create.

POST/deposit.php?action=charge-card-flwJWT / Key

Flutterwave direct card charge — preserves your own card form UI. Returns auth mode: pin, otp, or redirect. Scope: charges:create.

POST/deposit.php?action=validate-charge-flwJWT / Key

Validate PIN or OTP for a pending FLW charge. Body: { "flw_ref": "...", "otp": "..." }. Scope: charges:create.

POST/deposit.php?action=verify-flutterwaveJWT / Key

Verify FLW payment after redirect. Body: { "tx_ref": "...", "transaction_id": "..." }. Scope: charges:create.

POST/deposit.php?action=flutterwave-webhookSig-verified

Server-to-server FLW event. Validated via VERIF-HASH header.

Core API

Payouts

GET/payout.php?action=list-banks&country=NGJWT / Key

Supported banks. NG uses Paystack; other countries use Flutterwave. Scope: payouts:read.

GET/payout.php?action=resolve-accountJWT / Key

Verify account number before adding. Params: account_number, bank_code. Nigeria only. Scope: payouts:read.

POST/payout.php?action=add-bankJWT / Key

Save bank account. Numbers encrypted AES-256-CBC at rest. Max 5 accounts. Scope: payouts:create.

GET/payout.php?action=my-banksJWT / Key

List saved bank accounts. Account numbers are masked in response. Scope: payouts:read.

POST/payout.php?action=initiateJWT / Key

Withdraw to bank, PayPal, or mobile money. KYC required. Fee: max($1, 1%). Scope: payouts:create.

GET/payout.php?action=historyJWT / Key

Withdrawal history for current user. Scope: payouts:read.

Core API

Transactions

GET/transactions.php?action=listJWT / Key

Paginated transaction list. Params: page, limit, type, q, from, to. Scope: transactions:read.

GET/transactions.php?action=detail&uuid={uuid}JWT / Key

Single transaction with full counterpart details. Scope: transactions:read.

GET/transactions.php?action=exportJWT / Key

Export up to 5,000 transactions. Params: from, to. Scope: transactions:read.

GET/transactions.php?action=summaryJWT / Key

Lifetime and monthly totals by type for dashboard stats. Scope: transactions:read.

Core API

Notifications

GET/notifications.php?action=listJWT

Paginated notifications with unread count. Param: page.

GET/notifications.php?action=unread-countJWT

Returns { "count": 4 } — fast endpoint for polling badge counts.

POST/notifications.php?action=mark-all-readJWT

Mark all notifications as read.

POST/notifications.php?action=mark-readJWT

Mark a single notification as read. Body: { "id": 42 }

Features

Escrow

Lock funds until delivery conditions are met. Either party can raise a dispute. MyroPay resolves within 48 hours. Fee: 1.5%.

Initiator creates + funds
Funds locked
Work delivered
Release or dispute
POST/escrow.php?action=createJWT / Key

Create and fund escrow. Amount + 1.5% fee debited and locked immediately. Requires PIN. Scope: escrow:manage.

POST/escrow.php?action=releaseJWT / Key

Release funds to beneficiary. Initiator only. Body: { "uuid": "...", "pin": "..." }. Scope: escrow:manage.

POST/escrow.php?action=disputeJWT / Key

Raise a dispute. Either party. Body: { "uuid": "...", "reason": "...", "evidence": "https://..." }. Scope: escrow:manage.

POST/escrow.php?action=dispute-respondJWT / Key

Submit response to open dispute. Body: { "dispute_uuid": "...", "response": "..." }. Scope: escrow:manage.

GET/escrow.php?action=listJWT / Key

All escrows where you are initiator or beneficiary.

GET/escrow.php?action=detail&uuid={uuid}JWT / Key

Full escrow details with counterpart info.

Compliance

KYC Verification

LevelLabelDaily limitRequirement
0Starter$200Email verified
1Standard$5,000Government ID + selfie
2Verified$50,000Address proof + BVN (NG)
GET/kyc.php?action=statusJWT

Current KYC level, document status per tier, daily limits.

POST/kyc.php?action=uploadJWT

Multipart form-data. Fields: document (file), doc_type, tier. Max 5MB. JPEG/PNG/PDF.

POST/kyc.php?action=submit-bvnJWT

Nigerian BVN verification. Hashed SHA-256 immediately — never stored plain.

Business

Invoices Business accounts

POST/business.php?section=invoices&action=createJWT / Key

Create invoice with line items and tax. Returns invoice number and payment link. Scope: invoices:manage.

POST/business.php?section=invoices&action=sendJWT / Key

Email invoice PDF to client. Changes status draft → sent. Scope: invoices:manage.

GET/business.php?section=invoices&action=listJWT / Key

All invoices. Filter by status: draft | sent | paid | overdue. Scope: invoices:manage.

GET/business.php?section=invoices&action=detail&uuid={uuid}JWT / Key

Full invoice with parsed line items array. Scope: invoices:manage.

Business

Payroll

POST/business.php?section=payroll&action=createJWT

Bulk transfer to multiple MyroPay users in one request. Recipients credited instantly.

GET/business.php?section=payroll&action=listJWT

Payroll batches with totals, recipient count, and completion status.

Business

Analytics

GET/analytics.php?action=summary&days=30JWT

KPI summary: revenue, spending, net, transaction count, invoice stats, payroll totals.

GET/analytics.php?action=volume-chart&days=30JWT

Daily inflow/outflow series for charting.

GET/analytics.php?action=top-sendersJWT

Top 10 users by amount sent to your account in last 90 days.

GET/analytics.php?action=transaction-types&days=30JWT

Breakdown by transaction type for pie/donut charts.

Platform

Checkout API

Checkout API Basehttps://checkout.myropay.com/api/
POST/sessions.phpSecret Key

Create a checkout session. Returns session_id and checkout_url.

GET/sessions.php?session_id={id}Secret Key

Verify a session. Always call server-side before fulfilling orders. Check status === "completed" and amount.

POST/sessions.php?action=pay-walletJWT

Pay a session with MyroPay wallet balance. Body: { "session_id": "...", "pin": "..." }

POST/sessions.php?action=pay-cardPublic

Pay a session via Flutterwave direct card charge. Returns auth mode for 3DS/OTP challenge.

POST/sessions.php?action=validate-cardPublic

Submit OTP/PIN for pending card payment. Body: { "flw_ref": "...", "otp": "...", "session_id": "..." }

🔒
Never put your secret key (sk_live_) in frontend JavaScript. Only your public key (pk_live_) is safe for the browser.
Platform

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.

🔔
Pass webhook_url (and optionally webhook_events) when you create the app, and its whsec_... webhook secret is generated and returned in the same response as the key pair — a webhook is a property of the key that owns it, not a separate freestanding thing you set up later.
POST/business.php?section=apps&action=createJWT only

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.

GET/business.php?section=apps&action=listJWT only

Every app on the account with scopes, IP allow-list, and rate limit — secret keys are never included.

POST/business.php?section=apps&action=update-configJWT only

Update scopes / allowed_ips / rate_limit_per_min in one call. Requires PIN.

POST/business.php?section=apps&action=rotate-secretJWT only

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.

POST/business.php?section=apps&action=revokeJWT only

Permanently destroys the key pair — cannot be undone. Requires PIN.

🔒
Every action on this page is JWT only — an API key can never create, rotate, reveal, or revoke another API key, even its own. That must always happen from a signed-in dashboard session, so a leaked key can't be used to mint itself broader access.
Platform

Webhook Management

Manage the webhook(s) attached to a specific app. See Webhook Events for the event catalogue and signature verification.

POST/business.php?section=webhooks&action=createJWT / Key

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.

GET/business.php?section=webhooks&action=list&app_id=JWT / Key

List webhooks; optionally filter to one app. Scope: webhooks:manage.

POST/business.php?section=webhooks&action=testJWT / Key

Sends a signed test.ping event to the endpoint and returns the HTTP status it responded with. Scope: webhooks:manage.

DELETE/business.php?section=webhooks&action=deleteJWT / Key

Removes a webhook endpoint. Scope: webhooks:manage.

Platform

Settings

POST/settings.php?action=update-profileJWT

Update name and phone. Body: { "first_name", "last_name", "phone" }

POST/settings.php?action=change-passwordJWT

Change password. Revokes all other sessions. Body: { "current_password", "new_password" }

POST/settings.php?action=set-pinJWT

Set or change 4-digit transaction PIN. Body: { "pin", "confirm_pin", "current_pin?" }

POST/settings.php?action=setJWT

Toggle preference. Body: { "key": "push_notifications", "value": 1 }

GET/settings.php?action=sessionsJWT

Active login sessions with device, IP, last used time.

POST/settings.php?action=revoke-all-sessionsJWT

Log out all other sessions. No body required.

Platform

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.

Verify a delivery
// 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);
EventTrigger
payment.successCheckout session paid and completed
payment.failedPayment attempt abandoned or declined
deposit.confirmedWallet deposit credited (any gateway)
transfer.receivedP2P transfer credited to wallet
payout.completedWithdrawal processed and sent
payout.failedWithdrawal failed — funds returned
escrow.fundedEscrow created and funds locked
escrow.releasedInitiator releases to beneficiary
escrow.disputedEither party raises a dispute
escrow.resolvedAdmin resolves dispute
kyc.approvedKYC document approved
kyc.rejectedKYC document rejected
invoice.paidInvoice paid via checkout link
invoice.overdueInvoice past due date
payroll.completedPayroll batch fully processed
subscription.createdA subscription is created
subscription.cancelledCancelled immediately or at period end
subscription_invoice.createdA subscription billing cycle's invoice is generated
subscription_invoice.paidSubscription invoice paid (invoice or wallet_auto mode)
subscription_invoice.payment_failedWallet debit declined, or invoice unpaid past its due window
♻️
Retry schedule. Failed deliveries retried: 1 min → 5 min → 30 min → 2 hours → 24 hours. Always respond with 200 immediately and process async.
Reference

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.

VariableValueNotes
MYROPAY_PUBLIC_KEYpk_live_... / pk_test_...Safe in frontend code. From API & Apps.
MYROPAY_SECRET_KEYsk_live_... / sk_test_...Server-side only. This is the one credential that authenticates everything — deposits, escrow, payouts, wallet, invoices, subscriptions, checkout.
MYROPAY_WEBHOOK_SECRETwhsec_...Server-side only. Generated together with the key pair above, or via Webhook Management. Used only to verify X-MyroPay-Signature.
🚫
There is no "platform PIN" and it should never be requested by, or given to, an integration. A transaction PIN belongs to a human account holder and is entered by that person inside the MyroPay app to authorize their own action — it is never a server-side credential, never an environment variable, and no legitimate integration flow asks for it. If something asks you to set a MYROPAY_PLATFORM_PIN or similar, that's wrong — stop and remove it.

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.
Release Notes

Changelog

v3.4
Current Release
  • 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.
v3.3
Previous Release
  • 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.
v3.2
Previous Release
  • 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
v3.1
Previous Release
  • 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
v3.0
Previous Release
  • 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