Mopay logoMopay
FeaturesFor CreatorsOur TeamHow It WorksPricing
Sign inSign Up
API Documentation

MoPay Integration Guide

Contents

Overview

Quick Overview

The MoPay flow is intentionally short: create a session, redirect the customer, then verify the payment result when they return.

Your backend owns the session creation, MoPay owns the payment experience, and your app regains control once the customer is redirected back.

Your App → Create Payment Session → Redirect to MoPay → User Pays → Redirect Back

API Service

https://mopay.co.ls

Create and manage payment sessions from your backend.

Payment UI

https://mopay.co.ls

Hosted customer-facing checkout for mobile money and card payments.

Setup

Getting Started

Start by creating a MoPay account and generating the API key your server will use for payment session creation.

1

Get Your API Key

  1. Go to https://mopay.co.ls
  2. Create an account and log in
  3. Create a new Project
  4. Copy your API Key from the project settings

Security: Keep your API key secure. Store it in environment variables, never in client-side code.

Core Flow

Integration Steps

Build the integration in three predictable moves: create the session, redirect the customer to MoPay, then verify what happened on return.

Step 1: Create a Payment Session

Make a POST request to create a payment session:

POST https://mopay.co.ls/api/external/payment

Headers:
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json

Body:
{
  "amount": "100.00",
  "reference": "ORDER12345",
  "redirectUrl": "https://yourwebsite.com/payment-complete",
  "description": "Order #12345",
  "customerEmail": "customer@example.com",
  "customerName": "John Doe",
  "notificationPhoneNumber": "+26650123456",
  "allocations": [
    { "recipientEmail": "producer@example.com", "percentage": 100 }
  ]
}

Required Fields

FieldTypeDescription
amountstringPayment amount (e.g., "100.00")

Reference Format: When provided, references must contain only letters and numbers. Spaces, hyphens, underscores, and other special characters are rejected. MoPay does not enforce uniqueness, so use a different reference for each order.

Valid: ORDER12345Invalid: ORDER-12345

Optional Fields

FieldTypeDescription
referencestringUnique alphanumeric order reference
redirectUrlstringURL to redirect after payment
descriptionstringPayment description shown to user
customerEmailstringCustomer's email address
customerNamestringCustomer's name
paymentFrequencystringONCE or a supported recurring frequency
productNamestringProduct name (for product payments)
productDetailsstringProduct or payment description
productImagestringURL to product image
payWhatYouWantbooleanAllow custom amounts
minimumAmountstringMinimum amount if payWhatYouWant is true
notificationPhoneNumberstringMerchant payment SMS number (production projects only)
allocationsarrayAccounting split: recipientId or recipientEmail plus percentage

Merchant payment SMS notifications

  • Available only for projects in LIVE (production) mode.
  • The number receives a merchant notification after payment is confirmed. It is separate from the customer's payment number and receipt.
  • Use an international number such as +26650123456, or an 8-digit Lesotho number such as 50123456.
  • Sandbox requests containing this field return HTTP 400 and do not send an SMS.

Payment-specific allocations

Use allocationsto override the project's default split for this payment. Each recipient must already be the owner or an accepted project team member. Percentages may total up to 100%; any remainder stays visibly unallocated. This records who the payment is meant for and does not move funds or alter payouts.

Note on paymentFrequency:

  • ONCE - One-time payment only
  • DAILY, WEEKLY, MONTHLY, QUARTERLY, HALF_YEARLY, ANNUALLY - M-Pesa Direct Debit subscriptions

Recurring sessions create a Direct Debit mandate during M-Pesa checkout. M-Pesa is the only available payment method for recurring sessions.

For recurring card checkout and MIT renewals, follow the separate Subscription Setup section for invoice-backed billing, or the Card Subscriptions guide and prompt for guided pay-now/trial signup. External recurring sessions support M-Pesa Direct Debit; they do not create automatic card subscriptions.

Success Response (200):

{
  "success": true,
  "sessionId": "MOP_abc123_ORDER12345",
  "paymentUrl": "https://mopay.co.ls/pay/MOP_abc123_ORDER12345",
  "reference": "ORDER12345",
  "amount": "100.00"
}

Step 2: Redirect User to MoPay

After receiving the response, redirect the user to the payment URL:

// JavaScript example
window.location.href = response.paymentUrl;

// Or use a link
<a href="https://mopay.co.ls/pay/MOP_abc123_ORDER12345">Complete Payment</a>

The user will be taken to MoPay's payment page where they can select a payment method and complete the transaction. Recurring sessions only show M-Pesa because the checkout must create a Direct Debit mandate.

Step 3: Handle the Redirect Back

After payment completion, users are redirected to your URL with query parameters:

Successful Payment

https://yourwebsite.com/payment-complete?status=success&reference=ORDER12345&transactionId=TXN123&sessionId=MOP_abc123_ORDER12345&amount=100.00&paymentMethod=mpesa

Failed Payment

https://yourwebsite.com/payment-complete?status=failed&reference=ORDER12345&sessionId=MOP_abc123_ORDER12345&error=Payment+declined&paymentMethod=card

Cancelled Payment

https://yourwebsite.com/payment-complete?status=cancelled&reference=ORDER12345&amount=100.00&paymentMethod=none&sessionId=MOP_abc123_ORDER12345

When a user clicks "Cancel Payment" on the Mopay payment page, they are redirected with status=cancelled. The paymentMethodwill be none if cancelled before selecting a payment method.

Query Parameters

ParameterDescription
statussuccess, failed, or cancelled
referenceYour original reference
transactionIdMopay transaction ID (on success only)
sessionIdThe payment session ID
amountPayment amount
paymentMethodmpesa, ecocash, card, or none (if cancelled before selection)
errorError message (on failure only)
Recurring payments

M-Pesa subscriptions

Set a recurring payment frequency when creating a payment session. During checkout, MoPay creates the customer's M-Pesa Direct Debit mandate and subscription.

Recurring sessions currently support M-Pesa only. EcoCash and card are not available when paymentFrequency is recurring.
FrequencyBehavior
DAILYDaily M-Pesa Direct Debit collection
WEEKLYWeekly M-Pesa Direct Debit collection
MONTHLYMonthly M-Pesa Direct Debit collection
QUARTERLYQuarterly M-Pesa Direct Debit collection
HALF_YEARLYHalf-yearly M-Pesa Direct Debit collection
ANNUALLYAnnual M-Pesa Direct Debit collection

Create a recurring session

{
  "amount": "100.00",
  "reference": "ORDER12345",
  "paymentFrequency": "MONTHLY",
  "redirectUrl": "https://yourwebsite.com/payment-complete",
  "customerEmail": "customer@example.com"
}

The checkout collects the customer's M-Pesa number and requires an email address before creating the subscription. Store the returnedsessionId for later subscription management.

Cancel from your application

When a customer cancels in your application, call the cancellation endpoint with exactly one project-scoped identifier.

curl -X POST "https://mopay.co.ls/api/external/subscriptions/cancel" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "sessionId": "MOP_abc123_ORDER12345",
    "reason": "Customer cancelled in the application"
  }'

Send either sessionId orsubscriptionId, not both. MoPay cancels the M-Pesa mandate first; if the provider rejects cancellation, the local subscription remains active and the API returns an error.

Hosted iVeri checkout

Card subscriptions

Let customers pay the first period now or start a trial, save their card through MoPay, and manage future renewals. Card details and bank authentication stay on MoPay. Your backend owns customer identity, consent and access decisions.

This guide uses the guided, setup-managed card API. For products, prices, invoices, plan-change quotes and a customer portal, use the separate invoice-backed Billing API. Its fields and webhook configuration are different.

1. Prepare your project and recurring plan

Keep the project API key on your server. In the MoPay dashboard, create an active fixed-price recurring payment link and store its paymentLinkId against your own plan. The link must have a positive amount, a supported recurring frequency, no pay-what-you-want pricing, and must not be expired. The guided flow uses ZAR and reads price and frequency from that link.

Use project-authorized iVeri initial and recurring MIT credentials. A project may use supported MoPay-owned credentials or its own Enterprise application/certificate pairs, including an optional separate MIT certificate. Configure these in MoPay, never in your client app. Show the amount, interval, trial-end charge and cancellation terms before asking for recurring consent.

2. Choose pay now or a trial

PAY_NOW
Open checkout.paymentUrl. A confirmed first payment normally saves its reusable card and activates the subscription. If an additional verification is needed, follow checkout.hostedSetupUrl; the paid period is attached without charging it again.
TRIAL
Send trialDays from 1 to 365 and open checkout.hostedSetupUrl. MoPay discloses a temporary card authorization hold, then reverses it. A confirmed setup creates TRIALING with no paid period. The first scheduled collection becomes due at trialEndDate.
// In your authenticated backend, never in the browser.
// Resolve the customer and allowed plan from your own database.
const response = await fetch(process.env.MOPAY_BASE_URL + "/api/v1/subscription-checkouts", {
  method: "POST",
  headers: {
    Authorization: "Bearer " + process.env.MOPAY_API_KEY,
    "Content-Type": "application/json",
    "Idempotency-Key": signup.operationKey // persist; reuse on a retry
  },
  body: JSON.stringify({
    paymentLinkId: plan.mopayPaymentLinkId,
    mode: "PAY_NOW", // For a trial: "TRIAL", trialDays: 7
    externalCustomerId: customer.id,
    customerEmail: customer.email,
    customerName: customer.name,
    returnUrl: "https://your-app.example/subscription/return",
    termsVersion: "subscription-v1",
    consentAccepted: true // only after the customer accepted recurring terms
  })
});
const body = await response.json();
if (!response.ok || body.success !== true) {
  throw new Error(body.error?.code || "SUBSCRIPTION_CHECKOUT_FAILED");
}
// Persist body.checkout.id with your customer and plan before redirecting.
// PAY_NOW: checkout.paymentUrl; TRIAL: checkout.hostedSetupUrl.
// Return only the required hosted URL to your frontend, never the project key.

The response is { success: true, checkout: { ... } }. Store checkout.id and map it to the authenticated customer. Use a stable Idempotency-Key per signup and the same body on retry; a changed body with the same key returns 409. Creating metadata does not submit card details. A completed hold/reversal is verification, not a subscription payment.

3. Confirm activation and decide access

On return, fetch GET /api/v1/subscription-checkouts/{id} from your backend. POST the same path when existing setup/activation needs advancing; it does not debit a card. Follow the current hosted action for an unfinished step. If a first payment completed but activation is missing, reconcile that checkout instead of starting a new payment.

Once checkout.subscription exists, save its id and read current subscription state and payment history. Managed responses use uppercase status values and camelCase fields: lastBillingDate, nextBillingDate, trialEndDate, accessUntil, cancelAtPeriodEnd and billingDispatchEnabled. Check completed billing evidence and future accessUntil for paid access. Grant trial access only under your explicit policy, through trialEndDate. Dates or redirects alone do not prove payment.

Show pending/review, failed, paused and cancelled states. REQUIRES_REVIEW must not trigger another payment automatically. Your application decides grace and access rules; resuming collection does not itself create a paid period.

4. Receive signed events

Before signup, POST {"url":"https://your-app.example/webhooks/mopay"} to /api/v1/subscription-webhook with project Bearer authentication. Save the returned signingSecret securely; it is returned once. This configuration call does not require an idempotency key. Existing configuration requires explicit rotate:true. Events created before configuration are not automatically replayed to the new endpoint.

Mopay-Event-Id: <event-id>
Mopay-Signature: t=<unix-seconds>,v1=<hex>

signed bytes = timestamp + "." + exact raw request body
signature = HMAC-SHA256(signingSecret, signed bytes)

Envelope: { id, type, apiVersion, createdAt, subscriptionId, data }

Verify the exact raw bytes and timestamp before parsing JSON. Use constant-time comparison and a five-minute timestamp tolerance. Durably queue and deduplicate verified event.id values, acknowledge quickly, then fetch current subscription/payment state. Delivery can repeat or arrive out of order. Payment success remains recorded if webhook delivery fails.

Handle subscription.activated, subscription.trial_started, subscription.payment_succeeded, subscription.payment_failed, subscription.cancellation_scheduled, subscription.paused, subscription.resumed and subscription.cancelled. Do not grant access from the event alone.

5. Manage renewals and cancellation

MoPay handles scheduled renewals, trial-end collection and retries through its existing card cron. Your app does not need its own debit scheduler or access to MoPay cron secrets. Managed collection must be enabled for your project by the MoPay operator. billingDispatchEnabled:false means future collection is gated, even if signup succeeded; initial pay-now collection and verification are separate from that gate.

POST /api/v1/subscriptions/{id}/cancel
Authorization: Bearer <project_api_key>
Idempotency-Key: <stable-cancellation-operation-key>
Content-Type: application/json

{ "atPeriodEnd": true, "reason": "Customer requested cancellation" }

Period-end cancellation stops future collection immediately while preserving accessUntil for the current paid/trial period. It does not refund. Use atPeriodEnd:false for immediate cancellation. Pause and resume have separate endpoints; a scheduled cancellation cannot be undone through /resume. Authenticate and authorize your own customer before every management command.

Managed API reference

All calls use your project Bearer key from the server. Errors use { success: false, error: { code } }.

EndpointPurpose
POST /api/v1/subscription-checkoutsCreate or replay a guided pay-now/trial checkout. Idempotency-Key required.
GET /api/v1/subscription-checkouts/{id}Read the checkout, available hosted action and attached subscription.
POST /api/v1/subscription-checkouts/{id}Advance existing setup/activation. No card submission or new debit.
GET /api/v1/subscriptions/{id}Read current managed subscription state from subscription (camelCase fields).
GET /api/v1/subscriptions/{id}/paymentsRead payments, cycle outcomes and nextOffset for payment pagination.
POST /api/v1/subscriptions/{id}/pausePause future collection. Idempotency-Key required.
POST /api/v1/subscriptions/{id}/resumeResume an eligible paused subscription. Does not charge. Idempotency-Key required.
POST /api/v1/subscriptions/{id}/cancelCancel now or at period end. Idempotency-Key required.
GET /api/v1/subscription-webhookRead your managed webhook configuration without its signing secret.
POST /api/v1/subscription-webhookRegister {url}; save signingSecret once. Use rotate:true explicitly to replace configuration.
Advanced: attach a separately verified card and an already-paid period

Use POST /api/v1/cards/setup-sessions for a separate hosted card verification with externalCustomerId, customerEmail, optional customerName, consentReference and returnUrl. After setup is COMPLETED and an initial card checkout is confirmed paid, POST /api/v1/subscriptions with setupSessionId, paymentSessionId, consentAccepted:true and termsVersion. Both creates require Idempotency-Key. The paid checkout must match the project, environment, customer and recurring terms, with metadata.customerRef matching the setup customer. Activation attaches the paid period and does not charge again. Do not call it after guided checkout already created a subscription.

Standalone saved-card direct debits and authorization/capture/release are separate services requiring explicit confirmation. The subscription scheduler does not run them. Managed subscriptions do not expose the invoice engine's plan-change, proration or portal APIs. Usage billing, taxes and coupons are not part of this guided contract.

Build with your coding assistant

Fill in the placeholders after copying. Keep secrets out of the chat. This prompt covers the guided card flow, its lifecycle and signed events.

Read or manually copy the full prompt
Help me integrate MoPay's guided iVeri card subscriptions into my application.

Framework and language: [fill in]
MoPay base URL and project environment: [fill in, SANDBOX or LIVE]
Active fixed-price recurring paymentLinkId: [fill in]
Customer identity in my application: [fill in]
Signup mode: [PAY_NOW or TRIAL, with trialDays from 1 to 365]
Return URL and public HTTPS webhook URL: [fill in]
Subscription terms version and access/grace policy: [fill in]

Use https://mopay.co.ls/docs#card-subscriptions as the guided card contract. Do not mix it with /api/v1/checkout/sessions, invoice-backed Billing API prices, snake_case fields or billing-portal operations. Those belong to another subscription engine. Do not ask me to paste API keys, provider credentials or card details into this chat.

Implement this flow:
1. Keep MOPAY_API_KEY on the server. Authenticate my own customer and derive externalCustomerId and the allowed paymentLinkId from server-owned records. MoPay API calls use Authorization: Bearer <project_api_key>. Never let the browser choose another customer's subscription ID or arbitrary plan. Use one stable Idempotency-Key per logical create/lifecycle action and reuse that key with the identical body on retries. A changed body with the same key returns 409.
2. Prerequisite: create an active, unexpired, positive fixed-price recurring payment link in the MoPay project dashboard and save its ID on my server. Price and frequency come from that link; never send browser-owned amount/currency/frequency to override them. The guided flow uses ZAR. Show amount, interval, trial-end charge, hold disclosure if applicable, and cancellation terms; send consentAccepted:true only after affirmative recurring consent.
3. From my backend POST /api/v1/subscription-checkouts with JSON {paymentLinkId, mode, externalCustomerId, customerEmail, customerName, returnUrl, termsVersion, consentAccepted:true}. customerName is optional; for mode TRIAL also supply trialDays (integer 1-365). customerEmail must be valid and returnUrl must use HTTPS outside explicit local testing. Require both response.ok and body.success === true. Store checkout.id and its local customer/plan mapping before redirecting.
4. For PAY_NOW, open checkout.paymentUrl. MoPay collects the first period and normally attaches its reusable card token automatically. If a paid checkout needs separate card verification, open checkout.hostedSetupUrl and disclose its temporary authorization hold and reversal. Never pay the first period again to obtain a token. For TRIAL, open checkout.hostedSetupUrl; a confirmed hold/reversal creates TRIALING without a paid subscription period. Card entry and bank authentication happen on MoPay, never in my app.
5. On return, read GET /api/v1/subscription-checkouts/{id} from my server and, when needed, POST the same path with no payment body to advance/reconcile existing setup/activation. The POST does not submit card details or debit a card. Use the currently returned paymentUrl/hostedSetupUrl only for their matching unfinished state. A redirect, COMPLETED paymentStatus or verification hold alone is not an active subscription. REQUIRES_REVIEW means stop and reconcile; never create another checkout/payment automatically.
6. Once checkout.subscription exists, save its id. Fetch GET /api/v1/subscriptions/{id} and GET /api/v1/subscriptions/{id}/payments to establish current state and completed billing evidence. Managed DTOs use camelCase: setupManaged, status, lastBillingDate, nextBillingDate, trialEndDate, accessUntil, cancelAtPeriodEnd, billingDispatchEnabled. ACTIVE/TRIALING/PAST_DUE/PAUSED/CANCELLED are uppercase. Trial access must be an explicit policy and expire at trialEndDate; a trial is not a payment. Paid access needs verified completed payment evidence and a future accessUntil, subject to my lifecycle/grace policy. nextBillingDate and billingDispatchEnabled do not prove payment. Never expect invoice fields such as paid_through_at on this managed DTO.
7. Before signup, POST /api/v1/subscription-webhook with {url: myPublicHttpsUrl}; store the returned signingSecret securely once. Existing configuration requires explicit rotate:true to replace it. This endpoint does not require Idempotency-Key. Verify Mopay-Signature: t=<unix-seconds>,v1=<hex> as HMAC-SHA256(signingSecret, timestamp + '.' + exactRawBody), with constant-time comparison and a five-minute timestamp tolerance. Verify before JSON parsing. Durably enqueue/deduplicate verified event.id, acknowledge quickly, then fetch current state to handle out-of-order deliveries. Events carry {id,type,apiVersion,createdAt,subscriptionId,data}. Relevant types include subscription.activated, subscription.trial_started, subscription.payment_succeeded, subscription.payment_failed, subscription.cancellation_scheduled, subscription.paused, subscription.resumed and subscription.cancelled. A webhook alone does not grant access.
8. Provide focused server-owned lifecycle actions: POST /api/v1/subscriptions/{id}/pause and /resume, and POST /api/v1/subscriptions/{id}/cancel with {atPeriodEnd:true,reason:'customer request'} or atPeriodEnd:false. These commands require Idempotency-Key. Period-end cancellation immediately stops future collection, preserves accessUntil for the paid/trial period, and does not refund. A scheduled cancellation cannot be resumed through /resume. Resume does not itself collect money or create a paid entitlement. Use the returned state rather than inventing dates.
9. MoPay's existing card cron handles managed renewals, trial-end collection, retries and signed-event delivery. My app must not call operator cron routes, hold CRON_SECRET, run its own recurring card debits, capture unrelated authorizations, or automatically retry uncertain payments. The MoPay operator must separately enable managed collection for my project; billingDispatchEnabled:false means future collections are gated even if signup succeeded. Initial PAY_NOW collection and card verification are separate operations from that renewal gate.

Advanced alternative only when explicitly needed: POST /api/v1/cards/setup-sessions creates hosted card verification; after a completed setup and a separately confirmed first-period checkout, POST /api/v1/subscriptions with {setupSessionId,paymentSessionId,consentAccepted:true,termsVersion} attaches that already-paid period without charging it again. Do not call this after guided checkout already created a subscription. Saved-card standalone direct debits and authorization/capture/release are separate explicitly confirmed services, not subscription signup or scheduled renewals.

Handle errors as {success:false,error:{code}}; preserve uncertain operations for reconciliation and show actionable pending/review/expired/cancelled states. Do not claim generic mobile-money sandbox presets certify card setup or scheduled MIT. Do not invent trial invoices, managed plan-change/proration/portal APIs, usage billing, coupons or taxes.

Deliver server handlers, signup/return/manage screens, a raw-body signed webhook handler, durable customer/checkout/subscription mapping, and focused tests for duplicate submissions, duplicate/out-of-order events, signature/timestamp failures, trial expiry, paid-without-activation, cancelled/paused collection, disabled renewals and unknown outcomes. Never log hosted capability URLs, API keys, signing secrets, raw card data or provider tokens.
Recurring payments

Invoice-backed subscriptions

Build a Stripe-style subscription flow with MoPay-hosted card checkout, a project-scoped billing API, invoice records, signed webhooks and a customer portal. MoPay owns recurring card collection; your application grants service access from the current verified invoice and subscription state.

For guided pay-now signup, card verification and trials, use the card subscription guide and prompt. The invoice-backed API below has a separate contract and does not support trials.

Step 1

Create billing resources

From your backend, create a customer, product, and fixed recurring price using the versioned billing API. A stable external customer ID is supported; hosted card setup requires a valid email.

Step 2

Start hosted checkout

When the customer selects Subscribe, create a subscription checkout session and redirect to MoPay's returned HTTPS URL. Show the price, interval, and cancellation terms before the customer authorizes recurring billing.

Step 3

Verify current paid state

Verify the webhook's exact raw body and timestamp, then fetch the current invoice and subscription from your backend. Grant access only when the invoice is paid and the current subscription has paid service through a future date.

Step 4

Manage changes safely

Preview plan changes, show the returned invoice and timing, and apply the quote with an idempotency key. Use the portal for customer self-management. Reconcile submitted or unknown attempts; never create a replacement charge.

Credential ownership

Projects can use MoPay-owned credentials or their own supported iVeri Enterprise profile. The dashboard shows credential owner and initial/recurring profile presence without exposing values. Project-owned recurring card billing needs an enabled certificate and provider-authorized initial and distinct recurring application IDs. Missing or mismatched configuration blocks the flow; it does not fall back to another profile.

Developer Billing API card setup and durable dispatch require the server-only SUBSCRIPTION_BILLING_ENABLED switch and an exact project ID in SUBSCRIPTION_BILLING_PROJECT_ALLOWLIST. Both are deployment controls; a missing or malformed allowlist fails closed, and project policy cannot override either value. The dashboard does not toggle them. Read APIs and reconciliation remain available while dispatch is off.

Release scope

This API supports fixed recurring card prices, one subscription item and quantity one. Trials, usage billing, tax calculation, discounts/coupons, multiple items and embedded card fields are not supported. Customer email may be omitted when creating a customer with an external ID, but hosted subscription checkout requires a valid email. Provider certification and production dispatch must be verified for the intended deployment.

M-Pesa mandate subscriptions, manual renewals and the legacy recurring payment-link flow remain separate integrations. They are not created by this API. See the subscription setup guide and the OpenAPI contract for exact requests.

Invoice billing AI prompt

Copy this prompt into your coding assistant. Keep the project API key and webhook signing secret server-side.

The subscription prompt is shown below.

Help me integrate MoPay's developer subscription API into my application.

My framework: [fill in]
My application account/customer ID: [fill in]
My product, fixed recurring amount and interval: [fill in]
My MoPay project environment: [SANDBOX or LIVE]

Use only the shipped API described at https://mopay.co.ls/docs#subscription-setup and https://mopay.co.ls/openapi/billing.yaml. Do not invent routes or ask me to paste secrets, card data, or provider credentials into this chat.

Build this flow:
1. Keep the MoPay project API key in a server-only environment variable. Make authenticated Bearer requests from backend routes only. Use Idempotency-Key UUIDs for commands and reuse the same key when retrying the same logical request.
2. Create/update a MoPay customer using my stable external account ID; add a valid email because hosted card setup requires one. Create a product and immutable fixed recurring price in integer minor units.
3. When my customer selects Subscribe, call POST /api/v1/checkout/sessions from my server with mode=subscription, customer, price and a stable purchase_reference. Redirect to MoPay's returned hosted URL. Show the amount, interval and cancellation terms before consent.
4. Configure an HTTPS webhook endpoint and verify MoPay-Signature over the exact raw request bytes and timestamp before parsing. Deduplicate event IDs. Acknowledge quickly and do not grant access from an event or redirect alone.
5. After checkout and every relevant webhook, fetch the current invoice and subscription using the server API key. Grant or extend service only when the invoice is paid, due_minor is zero, and its current subscription is active, current_period_paid is true, and paid_through_at is in the future. Revoke/limit access according to my business policy when these conditions no longer hold.
6. For plan changes, call the preview endpoint, show its returned invoice/timing, and apply only the quote_id. Respect the project's policy and allowed overrides. Never calculate proration or retry an unknown charge yourself.
7. Add customer self-management using the billing portal session API. Never expose the project key, webhook signing secret, card token or provider response in browser data or logs.

Capabilities not in this release: trials, metered/usage billing, tax calculation, coupons/discounts, multiple subscription items and embedded card fields. Describe any unsupported requirement and do not emulate it with hidden charges.

Please implement server handlers, customer-facing signup and return states, a raw-body webhook handler, idempotent entitlement updates, and focused tests for duplicate/out-of-order events, unpaid invoices, expired paid-through state, and failed/unknown checkout. Use https://mopay.co.ls/openapi/billing.yaml as the contract and the runnable example as a reference; call out any mismatch rather than guessing.
MCP

Agent Integration

MoPay exposes a hosted Streamable HTTP MCP server so agents can create payment links, monitor transactions, summarize revenue, test sandbox payments, and manage manual subscriptions.

Hosted endpoint

https://mopay.co.ls/api/mcp

Connect with Streamable HTTP. Your MCP client opens the secure MoPay authorization flow automatically.

Account-wide with consent

Sign in, review the requested permissions, and let one connection work across all projects you own or have joined.

Recommended OAuth configuration

{
  "mcpServers": {
    "mopay": {
      "url": "https://mopay.co.ls/api/mcp"
    }
  }
}

Authentication modes

OAuth mode: connect with only the hosted URL. MoPay issues a user-bound, permission-scoped token after browser approval.

Legacy project mode: useAuthorization: Bearer PROJECT_API_KEYfor merchant-safe access.

Admin mode: useMOPAY_MCP_TOKENonly for internal MoPay operations.

Sandbox behavior

mopay_create_sandbox_payment only works on projects withenvironment: SANDBOXand writes simulated M-Pesa or EcoCash records.

Automation tools

OAuth sessions expose only tools covered by the permissions the user approved.

mopay_list_projectsmopay_create_projectmopay_get_projectmopay_create_payment_linkmopay_list_payment_linksmopay_get_payment_linkmopay_update_payment_linkmopay_list_transactionsmopay_get_transactionmopay_create_sandbox_paymentmopay_revenue_summarymopay_list_subscriptionsmopay_create_manual_subscriptionmopay_cancel_subscription

Internal admin tools

These require the internal MoPay MCP token and are hidden from OAuth sessions and project API keys.

mopay_search_users

Internal admin configuration

{
  "mcpServers": {
    "mopay-admin": {
      "url": "https://mopay.co.ls/api/mcp",
      "headers": {
        "Authorization": "Bearer <MOPAY_MCP_TOKEN>"
      }
    }
  }
}

The MCP can create manual-renewal subscriptions. Automatic card subscriptions are listed and can be cancelled, but creation still happens through checkout because saved card tokenization and 3DS data are required.

Examples

Complete Code Examples

Switch between working examples to see the same flow translated into the stack you already use.

JavaScript
// Create payment session
async function createPaymentSession(orderData) {
  const response = await fetch("https://mopay.co.ls/api/external/payment", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.MOPAY_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      amount: orderData.total.toFixed(2),
      reference: orderData.orderId,
      redirectUrl: "https://yoursite.com/payment-complete",
      description: `Order #${orderData.orderId}`,
      customerEmail: orderData.customerEmail,
      customerName: orderData.customerName,
      // Production projects only. Omit this field in sandbox.
      notificationPhoneNumber: "+26650123456",
    }),
  });

  const data = await response.json();

  if (!data.success) {
    throw new Error(data.error);
  }

  return data;
}

// Usage
const payment = await createPaymentSession({
  orderId: "ORD001",
  total: 150.0,
  customerEmail: "john@example.com",
  customerName: "John Doe",
});

// Redirect user to payment page
window.location.href = payment.paymentUrl;
Payment rails

One integration, the payment methods your customers already trust.

MoPay keeps the checkout surface consistent while each payment option stays visually grounded in its real brand.

Mobile money stays native for Lesotho customers, while card payments use the same hosted flow and session model documented across the rest of the API. Recurring sessions are currently M-Pesa-only.

M-Pesa
Mobile money

M-Pesa

Vodacom Lesotho mobile money checkout with a familiar USSD approval flow and locally optimized payment completion.

Customer approves on phonevia MoPay
EcoCash
Mobile money

EcoCash

Econet EcoCash collections through the same MoPay session architecture, with a simple redirect and return flow.

Customer confirms via handsetvia MoPay
VisaMastercard
Card payments

Cards

Visa and Mastercard payments are processed through iVeri inside the hosted MoPay experience for a secure checkout.

Hosted card entry via MoPayvia MoPay
Verification

Retrieving Session Details

Once you receive the customer back, use the session ID to confirm the final payment state from the API.

Recommended: Always verify the transaction status by retrieving the session details from the API. Do not rely solely on the redirect parameters as they can be tampered with.

GET https://mopay.co.ls/api/external/session/v1/{sessionId}

curl -X GET "https://mopay.co.ls/api/external/session/v1/MOP_abc123_ORDER12345" \
  -H "Accept: application/json"

Response:

{
  "success": true,
  "session": {
    "id": "...",
    "sessionId": "MOP_abc123_ORDER12345",
    "amount": "100.00",
    "reference": "ORDER12345",
    "redirectUrl": "https://yoursite.com/payment-complete",
    "status": "COMPLETED",
    "selectedPaymentMethod": "mpesa",
    "transactionId": "TXN123",
    "createdAt": "2025-12-04T14:00:00Z",
    "completedAt": "2025-12-04T14:05:00Z"
  }
}

Session Status Values

StatusDescription
CREATEDSession created, awaiting payment
PROCESSINGPayment in progress
COMPLETEDPayment successful
FAILEDPayment failed
CANCELLEDPayment cancelled by user
EXPIREDSession expired

Transaction Status Values

StatusDescription
successPayment successful
failedPayment failed
cancelledPayment cancelled

Note: transactionStatus can be null

Reliability

Error Handling

Treat every response as data to validate. A simple success check upfront keeps your checkout flow predictable.

const response = await fetch("https://mopay.co.ls/api/external/payment", {
  // ...
});

const data = await response.json();

if (!data.success) {
  // Handle error
  console.error("Error:", data.error);
  throw new Error(data.error);
}

// Proceed with data.paymentUrl

Common Errors

ErrorCauseSolution
Invalid API keyWrong or missing API keyCheck your API key
Invalid request dataMissing required fieldsInclude a valid amount and use a valid reference when provided
Valid redirect URL is requiredInvalid redirectUrlUse full URL with https://
Payment SMS notifications are only available for production projectsnotificationPhoneNumber was included for a sandbox projectRemove the field while testing or promote the project to production
Security

Security Best Practices

Keep the integration server-led, validate outcomes from the API, and treat your API key like production infrastructure.

Never expose your API key

  • • Store in environment variables
  • • Only make API calls from server-side code

Validate payments server-side

  • • Don't trust client-side redirect parameters alone
  • • Use the session retrieval API to verify payment status

Use unique references

  • • Generate unique order IDs
  • • MoPay does not enforce reference uniqueness

Use HTTPS

  • • Always use https:// for your redirectUrl
  • • Protects customer data
Sandbox

Sandbox Mobile Money Presets

New projects start in sandbox by default. In sandbox, MoPay simulates mobile money outcomes using preset numbers instead of calling live M-Pesa or EcoCash provider APIs.

Use these numbers only while your project environment is set to sandbox. Card payments are available in sandbox and production. Request production access from your dashboard when you're ready to go live.

M-Pesa Sandbox Numbers

Use these values as the customer mobile number when testing M-Pesa checkout flows.

Sandbox NumberExpected Result
52211111Immediate success
52222222Pending first, then success
52233333Failed
52244444Cancelled
52255555Expired

EcoCash Sandbox Numbers

Use these values as the customer mobile number when testing EcoCash checkout flows.

Sandbox NumberExpected Result
63211111Immediate success
63222222Pending first, then success
63233333Failed
63244444Cancelled
63255555Expired
Launch Prep

Testing

Use low-value transactions and the real session lifecycle to confirm redirects, callbacks, and state handling before go-live.

Ready to integrate?

Get started today and start accepting payments in minutes.

Create Account

Quick summary

  1. 1Get API key from https://mopay.co.ls
  2. 2Create session via POST https://mopay.co.ls/api/external/payment
  3. 3Redirect user to the returned paymentUrl
  4. 4Handle redirect back to your redirectUrl with payment status
Mopay LogoMopay

Accept, track, and settle payments through one modern platform built for growing businesses in Lesotho.

info@mopay.co.ls

Product

  • Features
  • How It Works
  • Pricing
  • For Creators
  • Our Team

Developers

  • Documentation
  • MCP
  • MCP Docs
  • Sandbox
  • WordPress Plugin

Account

  • Create Account
  • Sign In

Legal

  • Privacy settings
  • Terms of Service
  • Contact

© 2025 MoPay. All rights reserved.

Lesotho · Local payments infrastructure