ObPay API Documentation

Base URL: https://k5nkqygnd0i42zb1n5c3.helloreaddy.com/functions/v1/obpay-api-no-jwt

Two integration methods — pick what works for your stack.Option A — Direct HTTP (any hosting): Use your X-API-Key: sk_live_YOUR_KEY header directly. Works with raw fetch, axios, curl, PHP, Python requests, or any HTTP client. No SDK needed. See Direct HTTP section in SDK Examples.Option B — Supabase SDK: Install @supabase/supabase-js (JS) or supabase (Python) and use functions.invoke(). The SDK handles JWT automatically.

What You Need — 3 Things

Only 3 things needed to integrate. The first two are the same for every ObPay user — only your Secret Key is unique.

1API URL

...helloreaddy.com/functions/v1/obpay-api-no-jwt

Same for everyone

2Public Key

sb_publishable...

Supabase anon key — safe to commit

3Secret Key

sk_live_YOUR_KEY

Server-side only — never expose

How it works: Install the Supabase client SDK (@supabase/supabase-js or supabase for Python), create a client with #1 + #2 (both public), then pass #3 as _api_key in every request body. The SDK handles JWT auth automatically. Raw HTTP / curl will return 401.
ObPay Public Key (Supabase Anon Key)This is your public key — the same for all ObPay integrations. Use it with createClient(url, publicKey). It's already public and safe to commit.
sb_publishable_dFKnLJZfWkXfRVr9l97rEm5s8RweReFt

This key is already public (it ships in the ObPay website's browser code). Your Secret Key (from Dashboard → API Keys) is what actually controls access.

ObPay is a complete payment platform for mobile money collections and payouts across East Africa. It gives you a RESTful API, real-time webhooks, a merchant dashboard, and built-in wallet management — everything you need to accept and send mobile money payments.

Under the hood, ObPay routes transactions through trusted mobile money aggregators to reach MTN, Airtel, and bank networks. But you only need your ObPay API key — we handle the rest.

One platform, one API key. Get your API credentials from Dashboard → API Keys after completing KYC verification. All endpoints use the same base URL and authentication.

Supported Countries & Networks (MarzPay)

CountryCurrencyNetworks
🇺🇬UgandaUGXMTNAIRTEL
🇰🇪KenyaKESSAFARICOMAIRTEL
🇬🇭GhanaGHSMTNAIRTELTIGOVODAFONE
🇨🇲CameroonXAFMTNORANGE

Quick Start — Request Format

Terminal (Format only — see SDK for working code)
curl "https://k5nkqygnd0i42zb1n5c3.helloreaddy.com/functions/v1/obpay-api-no-jwt" \
  -X POST \
  -H "Content-Type: application/json" \
  -H "X-API-Key: sk_live_YOUR_KEY" \
  -d '{
    "_action": "collect",
    "phoneNumber": "0777123456",
    "amount": 50000,
    "currency": "UGX",
    "reference": "INVOICE123456789",
    "description": "Payment for invoice"
  }'

Raw HTTP returns 401. The Supabase gateway requires JWT. Jump to SDK Examples for copy-paste-ready working code.

Authentication

AUTH

All API requests require your ObPay Secret Key in the X-API-Key header.

Header Format
X-API-Key: sk_live_YOUR_OBPAY_SECRET_KEY

Your API key is validated against ObPay's database. The key must be active and your account must be verified (KYC approved).

Available Endpoints

POST/collect
POST/payout
GET/balance
GET/transaction/{ref}
GET/health

Collect Money API

POST/obpay-api (with _action: collect)

Receive payments from mobile money users. Funds are added to your ObPay wallet balance when the transaction is successful.

Terminal (Request Format — see SDK for working code)
curl "https://k5nkqygnd0i42zb1n5c3.helloreaddy.com/functions/v1/obpay-api-no-jwt" \
  -X POST \
  -H "Content-Type: application/json" \
  -H "X-API-Key: sk_live_YOUR_KEY" \
  -d '{
    "_action": "collect",
    "phoneNumber": "0777123456",
    "amount": 50000,
    "currency": "UGX",
    "reference": "INVOICE123456789",
    "description": "Payment for invoice"
  }'

Note: Raw HTTP / curl calls return HTTP 401 due to Supabase JWT verification. This example shows the request format. For working code, see the SDK Examples section below which uses the Supabase client SDK.

Request Parameters

ParameterTypeRequiredDescription
_actionStringRequiredMust be 'collect' for collection requests
phoneNumberStringRequiredCustomer's phone number (any format accepted)
amountNumberRequiredAmount to collect in the specified currency
currencyStringRequiredCurrency code (e.g., UGX, KES)
referenceStringOptionalUnique reference ID (auto-generated if empty)
descriptionStringOptionalTransaction description or reason
networkStringOptionalFor non-UGX currencies (e.g., SAFARICOM, AIRTEL)

Success Response (200 OK)

success.json
{
  "success": true,
  "data": {
    "reference": "INVOICE123456789",
    "transaction_id": "uuid-here",
    "status": "pending",
    "amount": 50000,
    "currency": "UGX",
    "message": "Collection request sent. Awaiting customer confirmation."
  }
}

Send Money (Payout) API

POST/obpay-api (with _action: payout)

Send money directly to mobile money users. Funds are deducted from your ObPay wallet balance.

Terminal
curl "https://k5nkqygnd0i42zb1n5c3.helloreaddy.com/functions/v1/obpay-api-no-jwt" \
  -X POST \
  -H "Content-Type: application/json" \
  -H "X-API-Key: sk_live_YOUR_KEY" \
  -d '{
    "_action": "payout",
    "phoneNumber": "0777123456",
    "amount": 50000,
    "currency": "UGX",
    "reference": "WITHDRAW123",
    "description": "Salary payment"
  }'

Note: Raw HTTP / curl calls return HTTP 401. See the SDK Examples for working Supabase client code.

Request Parameters

ParameterTypeRequiredDescription
_actionStringRequiredMust be 'payout' for send money requests
phoneNumberStringRequiredRecipient's phone number
amountNumberRequiredAmount to send
currencyStringRequiredCurrency code (UGX, KES, etc.)
referenceStringOptionalUnique reference (auto-generated if empty)
descriptionStringOptionalPayment description
networkStringOptionalRequired for non-UGX currencies

Success Response (200 OK)

success.json
{
  "success": true,
  "data": {
    "reference": "WITHDRAW123",
    "transaction_id": "uuid-here",
    "status": "pending",
    "amount": 50000,
    "currency": "UGX",
    "message": "Payout request submitted. Awaiting processing."
  }
}

Check Balance API

POST/obpay-api (with _action: balance)

Check your ObPay wallet balance.

Terminal
curl "https://k5nkqygnd0i42zb1n5c3.helloreaddy.com/functions/v1/obpay-api-no-jwt" \
  -X POST \
  -H "Content-Type: application/json" \
  -H "X-API-Key: sk_live_YOUR_KEY" \
  -d '{
    "_action": "balance"
  }'

Note: Raw HTTP / curl calls return HTTP 401. See the SDK Examples for working Supabase client code.

Parameters

ParameterTypeRequiredDescription
_actionStringRequiredMust be 'balance'

Success Response (200 OK)

success.json
{
  "success": true,
  "data": {
    "available_balance": 1500000,
    "pending_balance": 200000,
    "currency": "UGX"
  }
}

Transaction Status API

POST/obpay-api (with _action: transaction-status)

Check the status of any transaction using its reference ID.

Terminal
curl "https://k5nkqygnd0i42zb1n5c3.helloreaddy.com/functions/v1/obpay-api-no-jwt" \
  -X POST \
  -H "Content-Type: application/json" \
  -H "X-API-Key: sk_live_YOUR_KEY" \
  -d '{
    "_action": "transaction-status",
    "reference": "INVOICE123"
  }'

Note: Raw HTTP / curl calls return HTTP 401. See the SDK Examples for working Supabase client code.

Parameters

ParameterTypeRequiredDescription
_actionStringRequiredMust be 'transaction-status'
referenceStringRequiredYour transaction reference

Success Response (200 OK)

success.json
{
  "success": true,
  "data": {
    "id": "uuid",
    "reference": "INVOICE123456789",
    "status": "success",
    "amount": 50000,
    "currency": "UGX",
    "fee_amount": 2500,
    "net_amount": 47500,
    "customer_phone": "+256777123456",
    "payment_method": "MTN",
    "created_at": "2024-01-15 10:30:00",
    "completed_at": "2024-01-15 10:32:15"
  }
}

Webhooks

EVENT

Receive real-time notifications when a transaction status changes. ObPay sends an HTTP POST to your configured webhook URL with the transaction result. But more importantly, the payment provider sends webhooks to ObPay's webhook receiver — which automatically credits your wallet the moment a payment completes.

How Auto-Sync Works (Two Webhook Directions)

Provider → ObPay

The payment provider sends webhooks to ObPay's receiver when payments complete. ObPay automatically updates the transaction status and credits your wallet. You must configure the webhook URL in your provider's dashboard for this to work.

ObPay → Your Server

ObPay forwards the event to your webhook URL (configured in Dashboard → Webhooks) so your own server is notified in real-time. This is optional but recommended for production.

Your webhook endpoint must return a 200 OK within 10 seconds. Failed deliveries are retried 3 times with a 30-second delay.

CRITICAL: Configure This Webhook URL in Your Provider

Without this, payments complete with the provider but NEVER show in your ObPay wallet. You MUST configure your payment provider to send webhooks to ObPay's webhook receiver URL.

Webhook URL

https://k5nkqygnd0i42zb1n5c3.helloreaddy.com/functions/v1/marz-webhook

Steps:

1. Copy the URL above

2. Login to your payment provider dashboard

3. Go to Settings → Webhooks or Developer → Webhooks

4. Paste the URL as the webhook endpoint

5. Select events: all transaction events or specifically Success and Failed

This is the #1 reason payments don't show in ObPay. Configure this once and all future payments sync automatically.

Webhook Payload (ObPay → Your Server)

webhook-payload.json
{
  "event": "payment.success",
  "data": {
    "reference": "INV-123456789",
    "transaction_id": "uuid",
    "status": "success",
    "gross_amount": 50000,
    "fee_amount": 2500,
    "net_amount": 47500,
    "currency": "UGX",
    "customer_phone": "+256777123456",
    "payment_method": "MTN",
    "completed_at": "2024-01-15 10:35:00"
  },
  "timestamp": "2024-01-15T10:35:00Z"
}

What Happens When a Webhook Arrives

1

Provider processes the payment

Customer enters PIN on their phone. The provider confirms the transaction and sends a webhook to ObPay's receiver.

2

ObPay matches the transaction

The webhook receiver matches the incoming event to a pending transaction using provider_reference, merchant_reference, or phone+amount.

3

Wallet is credited instantly

On success: transaction status updated, fee deducted, wallet balance increased. On failure: marked failed, payout wallet refunded.

4

Your server is notified (if configured)

ObPay forwards the event to your webhook URL so you can update your order status, send confirmation emails, etc.

Webhook didn't arrive? If the provider's webhook wasn't configured and a payment completed, the v3.8+ status API auto-checks the provider on your next poll. See the Polling & Sync section for the polling + auto-sync pattern.

Signature Verification

Each webhook includes an X-ObPay-Signature header. Format: t=TIMESTAMP,v=HASH

verify.js
const crypto = require('crypto');

function verifyWebhookSignature(payload, signatureHeader, webhookUrl, secret) {
  const [tPart, vPart] = signatureHeader.split(',');
  const timestamp = tPart.split('=')[1];
  const received = vPart.split('=')[1];

  const params = {
    status: payload.status,
    customer_reference: payload.customer_reference,
    internal_reference: payload.internal_reference
  };

  const sorted = Object.keys(params).sort();
  let str = webhookUrl + timestamp;
  sorted.forEach(k => str += k + params[k]);

  const expected = crypto
    .createHmac('sha256', secret)
    .update(str)
    .digest('hex');

  return received === expected;
}

Polling, Sync & Status Checking

How external websites check transaction results — automatic sync via webhooks + real-time polling

How ObPay Syncs with Payment Providers

Your Website
ObPay API
Provider
MTN / Airtel
Provider
Webhook Receiver
ObPay Wallet

Two sync paths: (1) Webhooks push results in real-time — configure once, auto-syncs forever. (2) Status API pulls on-demand with v3.8+ auto-checking the provider for stale pending transactions.

Auto-Sync (Webhooks)

Real-time

The payment provider sends webhooks to ObPay automatically when payments complete. ObPay credits your wallet instantly. Nothing to code on your side.

Requires: Paste the webhook URL into your provider dashboard once. See the Webhooks section for the URL.

Status Polling (API)

On-demand

Your website calls ObPay's status API with a transaction reference. As of v3.8, pending transactions older than 30 seconds are auto-checked against the provider in real-time.

Best for: External websites that need to confirm payment status before delivering goods or services.

How Status Polling Works (v3.8+)

When your website calls the ObPay status API, here's what happens behind the scenes:

1

Your website sends request

POST to ObPay API with _action: 'transaction-status' and your transaction reference. Authenticated with your Secret Key.

2

ObPay looks up the transaction

Finds your transaction in the database and checks its current status.

3

Auto-check provider if stale (NEW v3.8)

If the transaction is still 'pending' and older than 30 seconds, ObPay automatically calls the provider to check the real status. This means even without webhooks configured, your status checks return accurate results.

4

Wallet auto-credited on success

If the provider confirms the payment went through, ObPay updates the transaction to 'success' and credits your wallet — all in one status check.

5

Response returned to your site

You get the final status: success, failed, or still pending. The response includes an 'auto_synced' flag so you know if the provider was checked live.

Status Check API Reference

Use _action: "transaction-status" with your transaction reference. Requires your Secret Key for authentication.

Request Format
curl "https://k5nkqygnd0i42zb1n5c3.helloreaddy.com/functions/v1/obpay-api-no-jwt" \
  -X POST \
  -H "Content-Type: application/json" \
  -H "X-API-Key: sk_live_YOUR_KEY" \
  -d '{
    "_action": "transaction-status",
    "reference": "INVOICE123"
  }'

Note: Raw HTTP / curl calls return HTTP 401 due to Supabase JWT verification. Use the Supabase client SDK below for working code.

Success Response (with auto-sync)
{
  "success": true,
  "data": {
    "id": "uuid",
    "reference": "INVOICE123",
    "status": "success",
    "amount": 50000,
    "currency": "UGX",
    "fee_amount": 2500,
    "net_amount": 47500,
    "customer_phone": "+256777123456",
    "created_at": "2024-01-15T10:30:00Z",
    "completed_at": "2024-01-15T10:32:15Z",
    "auto_synced": true,
    "previous_status": "pending"
  }
}

New in v3.8: When auto_synced: true appears, it means ObPay checked the provider live during your request and found the payment completed — your wallet was credited right then.

Complete Status Polling (JavaScript — Supabase SDK)

Installation: npm install @supabase/supabase-jsUses Supabase client SDK — handles JWT automatically. Works 100%.
status-check.js — Check transaction status with auto-sync
const { createClient } = require('@supabase/supabase-js');

const obpay = createClient(
  'https://k5nkqygnd0i42zb1n5c3.helloreaddy.com',
  'sb_publishable_dFKnLJZfWkXfRVr9l97rEm5s8RweReFt'
);

const OBPAY_KEY = process.env.OBPAY_SECRET_KEY || 'sk_live_YOUR_KEY';

// Check a single transaction — auto-syncs with provider if pending > 30s
async function checkStatus(reference) {
  const { data, error } = await obpay.functions.invoke('obpay-api-no-jwt', {
    body: {
      _api_key: OBPAY_KEY,
      _action: 'transaction-status',
      reference
    }
  });

  if (error) throw new Error(error.message);
  if (!data.success) throw new Error(data.error || 'Status check failed');

  const txn = data.data;
  return {
    reference: txn.reference,
    status: txn.status,              // 'pending' | 'success' | 'failed'
    amount: txn.amount,
    fee: txn.fee_amount || 0,
    netAmount: txn.net_amount || txn.amount,
    wasAutoSynced: txn.auto_synced || false,  // true = provider checked live
    completedAt: txn.completed_at || null
  };
}

Complete Polling Loop — Wait for Payment Confirmation

This is the full pattern external websites use: initiate a payment, then poll until the result comes back. The v3.8 auto-sync means even if the provider's webhooks aren't configured, your poll will still get accurate results.

full-payment-flow.js
const { createClient } = require('@supabase/supabase-js');

const obpay = createClient(
  'https://k5nkqygnd0i42zb1n5c3.helloreaddy.com',
  'sb_publishable_dFKnLJZfWkXfRVr9l97rEm5s8RweReFt'
);

const OBPAY_KEY = process.env.OBPAY_SECRET_KEY;

// Step 1: Initiate collection
async function startPayment(phone, amount) {
  const { data, error } = await obpay.functions.invoke('obpay-api-no-jwt', {
    body: {
      _api_key: OBPAY_KEY,
      _action: 'collect',
      phoneNumber: phone,
      amount: Number(amount),
      currency: 'UGX',
      reference: 'INV-' + Date.now().toString(36).toUpperCase(),
      description: 'Payment for order'
    }
  });
  if (error || !data.success) throw new Error(data.error || error.message);
  return data.data.reference;  // Save this reference
}

// Step 2: Poll until resolved
async function waitForPayment(reference, maxSeconds = 120) {
  const start = Date.now();
  const pollInterval = 4000;  // 4 seconds

  while (Date.now() - start < maxSeconds * 1000) {
    await new Promise(r => setTimeout(r, pollInterval));

    const { data, error } = await obpay.functions.invoke('obpay-api-no-jwt', {
      body: { _api_key: OBPAY_KEY, _action: 'transaction-status', reference }
    });

    if (error || !data?.success) continue;  // transient error, keep polling

    const txn = data.data;

    if (txn.status === 'success') {
      return {
        confirmed: true,
        reference: txn.reference,
        amount: txn.amount,
        fee: txn.fee_amount || 0,
        netAmount: txn.net_amount || txn.amount,
        autoSynced: txn.auto_synced || false
      };
    }

    if (txn.status === 'failed') {
      return { confirmed: false, error: 'Payment was declined or cancelled' };
    }

    // Still pending — continue polling (auto-sync runs on each check)
  }

  return { confirmed: false, error: 'Payment timed out' };
}

// Step 3: Full flow
async function processPayment(phone, amount) {
  console.log('Initiating payment...');
  const ref = await startPayment(phone, amount);
  console.log('Transaction reference:', ref);

  console.log('Waiting for customer to enter PIN...');
  const result = await waitForPayment(ref, 120);

  if (result.confirmed) {
    console.log('Payment confirmed!');
    console.log('Received:', result.netAmount, 'UGX (fee:', result.fee, 'UGX)');
    // Deliver goods, update order status, send confirmation email, etc.
    return { success: true, ...result };
  } else {
    console.log('Payment failed:', result.error);
    // Prompt customer to retry
    return { success: false, error: result.error };
  }
}

Status Polling (Python)

Installation: pip install supabase
poll_status.py
from supabase import create_client
import time

obpay = create_client(
    "https://k5nkqygnd0i42zb1n5c3.helloreaddy.com",
    "sb_publishable_dFKnLJZfWkXfRVr9l97rEm5s8RweReFt"
)

OBPAY_KEY = "sk_live_YOUR_KEY"

def check_status(reference: str):
    """Check transaction status — auto-syncs with provider for pending txns."""
    resp = obpay.functions.invoke("obpay-api-no-jwt", {
        "body": {
            "_api_key": OBPAY_KEY,
            "_action": "transaction-status",
            "reference": reference
        }
    })
    if not resp.get("success"):
        raise Exception(resp.get("error", "Status check failed"))
    return resp["data"]

def wait_for_payment(reference: str, max_seconds: int = 120):
    """Poll until payment succeeds or fails."""
    deadline = time.time() + max_seconds
    while time.time() < deadline:
        time.sleep(4)
        try:
            txn = check_status(reference)
            if txn["status"] == "success":
                return {"confirmed": True, **txn}
            if txn["status"] == "failed":
                return {"confirmed": False, "error": "Payment declined"}
        except Exception:
            continue  # transient error, keep polling
    return {"confirmed": False, "error": "Timeout"}

Best Practices for Status Checking

Poll every 4 seconds

Most mobile money payments confirm within 10-30 seconds. 4-second intervals balance speed with API load.

Set reasonable timeouts

60-120 seconds for collections, up to 5 minutes for payouts. Mobile money is fast but not instant.

Handle all three outcomes

Success = deliver goods. Failed = prompt retry. Timeout = show pending message with manual refresh option.

Don't stop on transient errors

Network blips happen. Use try/catch inside the poll loop — only stop when you get a definitive status.

Auto-sync is your safety net (v3.8+)

Even without webhooks configured, each status check auto-verifies with the provider for pending transactions older than 30 seconds.

Webhooks + Polling = best combo

Configure webhooks for instant updates, use polling as fallback. Webhooks handle 95% of cases instantly; polling catches the rest.

Bulk Sync: Check All Pending Transactions at Once

Use _action: "sync-all-pending" to check the payment provider for all your pending transactions in one call. Useful for reconciliation or after webhook outages.

Sync All Pending (JavaScript)
const { data } = await obpay.functions.invoke('obpay-api-no-jwt', {
  body: { _api_key: OBPAY_KEY, _action: 'sync-all-pending' }
});
// Response: { synced: 10, updated: [{ id, reference, status }, ...], logs: [] }

Error Codes

REFERENCE
StatusErrorDescription
400Missing required fieldsA required parameter was not provided
400Invalid amountAmount is not a valid number
400Reference already usedReference ID is a duplicate
400Reference must not contain spacesUse hyphens or underscores instead
400Reference must be at most 30 charactersShorten your reference string
400Network is required for non-UGX currenciesAdd the network field for KES/GHS/XAF
400Insufficient balanceNot enough funds in wallet
401Authorization requiredMissing API key header
401Invalid API keyKey format or value is incorrect
403Account number does not match API key ownerAccount number mismatch
403User account pending approvalKYC not yet approved
403User account suspendedAccount has been suspended
404Transaction not foundNo transaction with given reference
429Too many requestsRate limit exceeded
500Internal server errorServer-side issue — retry with exponential backoff
504Payment service timeoutPayment gateway timed out

SDK Examples

Full code examples for integrating ObPay's API into your application.

New: Direct HTTP integration. ObPay now supports raw HTTP requests with X-API-Key: sk_live_YOUR_KEY — no SDK, no Supabase, works on any hosting. See the Direct HTTP tab below for copy-paste examples that work everywhere.
Installation: npm install @supabase/supabase-jsThis uses the Supabase client SDK which handles JWT automatically — works 100%. Raw HTTP / axios calls return 401.

Complete ObPay Client (Node.js — Supabase SDK)

obpay.js
const { createClient } = require('@supabase/supabase-js');

// Create Supabase client (URL + ObPay Public Key — both safe to commit)
const obpay = createClient(
  'https://k5nkqygnd0i42zb1n5c3.helloreaddy.com',
  'sb_publishable_dFKnLJZfWkXfRVr9l97rEm5s8RweReFt'
);

const OBPAY_SECRET_KEY = process.env.OBPAY_SECRET_KEY || 'sk_live_YOUR_KEY';

async function collect({ phone, amount, currency = 'UGX', reference, description = '' }) {
  const ref = reference || 'INV' + Date.now().toString(36).toUpperCase();
  const { data, error } = await obpay.functions.invoke('obpay-api-no-jwt', {
    body: {
      _api_key: OBPAY_SECRET_KEY,
      _action: 'collect',
      phoneNumber: phone,
      amount: Number(amount),
      currency,
      reference: ref.slice(0, 30),
      description
    }
  });
  if (error) throw new Error(error.message);
  if (!data.success) throw new Error(data.error || 'Collection failed');
  return data.data;
}

async function payout({ phone, amount, currency = 'UGX', reference, description = '' }) {
  const ref = reference || 'PAY' + Date.now().toString(36).toUpperCase();
  const { data, error } = await obpay.functions.invoke('obpay-api-no-jwt', {
    body: {
      _api_key: OBPAY_SECRET_KEY,
      _action: 'payout',
      phoneNumber: phone,
      amount: Number(amount),
      currency,
      reference: ref.slice(0, 30),
      description
    }
  });
  if (error) throw new Error(error.message);
  if (!data.success) throw new Error(data.error || 'Payout failed');
  return data.data;
}

async function getBalance() {
  const { data, error } = await obpay.functions.invoke('obpay-api-no-jwt', {
    body: { _api_key: OBPAY_SECRET_KEY, _action: 'balance' }
  });
  if (error) throw new Error(error.message);
  return data.data;
}

async function getTransactionStatus(reference) {
  const { data, error } = await obpay.functions.invoke('obpay-api-no-jwt', {
    body: { _api_key: OBPAY_SECRET_KEY, _action: 'transaction-status', reference }
  });
  if (error) throw new Error(error.message);
  return data.data;
}

async function waitForPayment(reference, maxWaitSeconds = 120) {
  const start = Date.now();
  while (Date.now() - start < maxWaitSeconds * 1000) {
    await new Promise(r => setTimeout(r, 4000));
    try {
      const txn = await getTransactionStatus(reference);
      if (txn.status === 'success') return { confirmed: true, ...txn };
      if (txn.status === 'failed') return { confirmed: false, error: 'Payment declined' };
    } catch (e) {}
  }
  return { confirmed: false, error: 'Timeout' };
}

module.exports = { collect, payout, getBalance, getTransactionStatus, waitForPayment };

ObPay API Reference

Your gateway to mobile money — use ObPay's REST API to collect and send payments.

Important: You authenticate with your ObPay API key (Dashboard → API Keys), and ObPay routes requests to payment providers automatically. You never need provider credentials.

Architecture

Your Website
ObPay API
Payment Providers
MTN / Airtel

Your API key never reaches the browser — all calls go through Supabase Edge Functions.

Base URL

Base URL
https://k5nkqygnd0i42zb1n5c3.helloreaddy.com/functions/v1/obpay-api-no-jwt

Authentication

All API requests require your ObPay Secret Key. Use either header:

Using X-API-Key (Recommended for server-to-server)
X-API-Key: sk_live_xxxxxxxxxxxxxxxx
Using Bearer Token
Authorization: Bearer sk_live_xxxxxxxxxxxxxxxx

Platform Fee

ObPay deducts a platform fee from every successful collection before crediting your wallet. Default 5%. For a 50,000 UGX collection, you receive 47,500 UGX net.

Website Integration

Integrate ObPay payments into any website — from a simple button to a full checkout flow.

Option 1: Payment Link (Simplest)

Your payment page URL:

https://obpay.online/pay/YOUR_MERCHANT_SLUG

Find your exact URL in Dashboard → API Keys.

Option 2: Embed a Payment Button

pay-button.html
<a href="https://obpay.online/pay/YOUR_MERCHANT_SLUG"
   target="_blank"
   style="display:inline-flex;align-items:center;gap:8px;
          padding:12px 24px;background:#10B981;color:white;
          border-radius:8px;text-decoration:none;font-weight:600;
          font-family:system-ui,sans-serif;font-size:14px;">
  Pay with ObPay
</a>
Security Note: Always call the ObPay API from your backend server. Your secret key should never be exposed in client-side JavaScript.

How ObPay + MarzPay Work Together

Behind the scenes — how ObPay uses MarzPay to reach mobile money networks across East Africa.

How it works: ObPay is powered by MarzPay as the underlying payment provider. When you request a collection or payout, ObPay's Edge Functions securely forward the request to MarzPay's API, which then routes it to MTN Mobile Money, Airtel Money, and other networks across Uganda, Kenya, Ghana, and Cameroon.

MarzPay API Charges (applied by MarzPay to the transaction)

TypeChargeApplied By
Collection (receive money)Variable charge per transactionMarzPay (see wallet.wearemarz.com dashboard)
Send Money (payout to others)Variable charge per transactionMarzPay (see wallet.wearemarz.com dashboard)
ObPay Platform Fee (collections)5% of collected amountObPay (before crediting your wallet)
ObPay Payout Fee500–70K: 2,000 UGX flat / 70K+–5M: 3%ObPay (deducted from your wallet)
ObPay Withdrawal FeeSame tiered pricing as payoutsObPay (deducted from your wallet)

Payment Flow

1

You Initiate a Collection

Your website sends a request to ObPay with the customer's phone number and amount.

2

ObPay Routes the Payment

ObPay's Edge Function securely forwards your request to the mobile money aggregator.

3

Customer Gets USSD Prompt

MTN or Airtel triggers a USSD prompt. Customer enters their PIN to approve.

4

ObPay Receives Confirmation

The aggregator sends a webhook back to ObPay with the result.

5

ObPay Updates Your Wallet

On success: fee is deducted and balance credited. On failure: marked failed.

6

You Get Notified

If webhooks are configured, ObPay forwards the event to your server.

Important Notes

Transactions are asynchronous

A collection returns 'pending'. Actual payment takes 30-90 seconds while customer enters PIN.

Platform fee is auto-deducted

ObPay deducts the platform fee before crediting your wallet. Default 5%.

Payouts require sufficient balance

Your available balance must cover the payout amount plus fees.

Failed transactions are not charged

If a collection fails, no fee is charged and no money moves.

Rate Limiting

Understand API rate limits and how to handle them gracefully.

Exceeding limits returns a 429 Too Many Requests response.
EndpointMethodLimitWindow
collectPOST5015 minutes
payoutPOST51 minute
balancePOST5015 minutes
transaction-statusPOST10015 minutes
healthGETUnlimited

Best Practices

Poll at reasonable intervals

For status checks, poll every 5-10 seconds, not every second.

Cache balance results

Cache balance for 30-60 seconds instead of calling on every page render.

Handle 429 gracefully

Respect the Retry-After header. Use exponential backoff with jitter.

Security Best Practices

Protect your API keys, validate webhooks, and secure your integration.

Critical: Your secret API key grants full access to your ObPay wallet. Treat it like a password. Never expose it in client-side code.

1. Never Expose Your Secret Key

DO — Server-side only

server.js (secure)
// ✅ GOOD: API key stored in environment variable
const OBPAY_KEY = process.env.OBPAY_SECRET_KEY;
app.post('/api/pay', async (req, res) => {
  const result = await fetch(OBPAY_API, {
    headers: { 'X-API-Key': OBPAY_KEY },
    body: JSON.stringify({ _action: 'collect', ...req.body })
  });
});

DON'T — Client-side exposure

browser.js (insecure)
// ❌ BAD: Secret key exposed in browser — anyone can steal it
const OBPAY_KEY = 'sk_live_abc123'; // Visible in browser DevTools

2. Validate Webhook Signatures

webhook-verify.js
const crypto = require('crypto');

function verifyObPayWebhook(req) {
  const signature = req.headers['x-obpay-signature'];
  if (!signature) return false;
  const [tPart, vPart] = signature.split(',');
  const timestamp = tPart.split('=')[1];
  const receivedHash = vPart.split('=')[1];
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp)) > 300) return false; // Replay attack prevention
  const payload = JSON.stringify(req.body);
  const expectedHash = crypto.createHmac('sha256', process.env.OBPAY_WEBHOOK_SECRET)
    .update(timestamp + '.' + payload).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(receivedHash), Buffer.from(expectedHash));
}

Security Checklist

1

Secret key stored in environment variables, never in code or git

2

All API calls made from backend server, never from browser JavaScript

3

Webhook signatures validated on every incoming event

4

HTTPS enforced on your webhook endpoint

5

API keys rotated every 90 days

6

Unique references used for every transaction (idempotency)

7

Webhook replay attacks prevented (timestamp check within 5 minutes)

Testing & Go-Live Checklist

Verify your integration before accepting real payments.

Phase 1: Sandbox Testing

Call the collect endpoint with test phone numbers and amounts
Verify the response structure matches documentation
Test error scenarios: missing fields, invalid amounts, wrong currency
Test the transaction-status endpoint with a known reference
Verify balance endpoint returns correct format

Phase 2: Go-Live Checklist

KYC Verified

Your merchant account is approved and active

API Keys Secured

Secret keys stored in environment variables

Webhook Configured

Webhook URL set up with signature verification

Error Handling

All API calls wrapped in try/catch

Rate Limiting Handled

Exponential backoff implemented for 429

Idempotency

Unique references prevent duplicate charges

HTTPS Enforced

All endpoints use HTTPS

Logging

Transaction logs stored for debugging

Build a Payment Website with ObPay

Complete production-ready integration — backend API module, frontend checkout, webhooks, and automatic payment flow.

What you'll build: A complete payment system. Customer enters phone & amount → ObPay sends USSD prompt → Customer confirms on phone → Your website shows confirmation. All code is copy-paste ready.

Architecture Overview

Customer's
Browser
Your Backend
Server
ObPay
Edge Function
MTN / Airtel

Browser never touches ObPay directly — all API calls go through your backend.

Step 1: Backend API Module (Node.js — Supabase SDK)

obpay-client.js
const { createClient } = require('@supabase/supabase-js');

// Create Supabase client
const obpay = createClient(
  'https://k5nkqygnd0i42zb1n5c3.helloreaddy.com',
  'sb_publishable_dFKnLJZfWkXfRVr9l97rEm5s8RweReFt'
);

const OBPAY_SECRET_KEY = process.env.OBPAY_SECRET_KEY || 'sk_live_YOUR_KEY';

async function collect({ phone, amount, currency = 'UGX', reference, description = '' }) {
  const ref = reference || 'INV' + Date.now().toString(36).toUpperCase();
  const { data, error } = await obpay.functions.invoke('obpay-api-no-jwt', {
    body: { _api_key: OBPAY_SECRET_KEY, _action: 'collect', phoneNumber: phone, amount: Number(amount), currency, reference: ref.slice(0, 30), description }
  });
  if (error) throw new Error(error.message);
  if (!data.success) throw new Error(data.error || 'Collection failed');
  const result = data.data;
  return { reference: result.reference, transactionId: result.transaction_id, status: result.status, amount: result.amount, fee: result.collection_fee || 0, netAmount: result.net_amount_to_receive || result.amount };
}

async function payout({ phone, amount, currency = 'UGX', reference, description = '' }) {
  const ref = reference || 'PAY' + Date.now().toString(36).toUpperCase();
  const { data, error } = await obpay.functions.invoke('obpay-api-no-jwt', {
    body: { _api_key: OBPAY_SECRET_KEY, _action: 'payout', phoneNumber: phone, amount: Number(amount), currency, reference: ref.slice(0, 30), description }
  });
  if (error) throw new Error(error.message);
  if (!data.success) throw new Error(data.error || 'Payout failed');
  const result = data.data;
  return { reference: result.reference, transactionId: result.transaction_id, status: result.status, amount: result.amount, fee: result.fee_amount || 0 };
}

async function getBalance() {
  const { data, error } = await obpay.functions.invoke('obpay-api-no-jwt', {
    body: { _api_key: OBPAY_SECRET_KEY, _action: 'balance' }
  });
  if (error) throw new Error(error.message);
  const result = data.data;
  return { available: result.available_balance || 0, pending: result.pending_balance || 0, currency: result.currency || 'UGX' };
}

async function getTransactionStatus(reference) {
  if (!reference) throw new Error('reference is required');
  const { data, error } = await obpay.functions.invoke('obpay-api-no-jwt', {
    body: { _api_key: OBPAY_SECRET_KEY, _action: 'transaction-status', reference }
  });
  if (error) throw new Error(error.message);
  const result = data.data;
  return { reference: result.reference, status: result.status, amount: result.amount, fee: result.fee_amount || 0, netAmount: result.net_amount || result.amount };
}

async function waitForPayment(reference, maxWaitSeconds = 120) {
  const start = Date.now();
  while (Date.now() - start < maxWaitSeconds * 1000) {
    await new Promise(r => setTimeout(r, 4000));
    try {
      const txn = await getTransactionStatus(reference);
      if (txn.status === 'success') return { confirmed: true, reference: txn.reference, amount: txn.amount, fee: txn.fee, netAmount: txn.netAmount };
      if (txn.status === 'failed') return { confirmed: false, error: 'Payment declined' };
    } catch (e) {}
  }
  return { confirmed: false, error: 'Timeout' };
}

module.exports = { collect, payout, getBalance, getTransactionStatus, waitForPayment };

Step 2: Express Server with API Routes

server.js
const express = require('express');
const cors = require('cors');
const obpay = require('./obpay-client');
const app = express();
app.use(cors()); app.use(express.json());

app.post('/api/pay', async (req, res) => {
  try {
    const { phone, amount, currency, description } = req.body;
    if (!phone || !amount) return res.status(400).json({ error: 'phone and amount are required' });
    const payment = await obpay.collect({ phone, amount: Number(amount), currency: currency || 'UGX', description });
    res.json({ success: true, reference: payment.reference, status: 'pending', amount: payment.amount });
  } catch (error) { res.status(500).json({ error: error.message }); }
});

app.get('/api/pay/status/:reference', async (req, res) => {
  try { res.json({ success: true, ...(await obpay.getTransactionStatus(req.params.reference)) }); }
  catch (error) { res.status(500).json({ error: error.message }); }
});

app.get('/api/balance', async (req, res) => {
  try { res.json({ success: true, ...(await obpay.getBalance()) }); }
  catch (error) { res.status(500).json({ error: error.message }); }
});

app.post('/api/webhooks/obpay', (req, res) => {
  console.log('[Webhook]', req.body.event, req.body.data?.reference);
  res.status(200).json({ received: true });
});

app.listen(process.env.PORT || 3000, () => console.log('ObPay Server running on port', process.env.PORT || 3000));

Complete Project Structure

your-payment-website/
├── obpay-client.js          # Payment module (Step 1)
├── server.js                # Express server (Step 2)
├── checkout.html            # Frontend checkout page
├── package.json             # npm init && npm install express cors @supabase/supabase-js
└── .env                     # OBPAY_SECRET_KEY=sk_live_YOUR_KEY
                            # OBPAY_PUBLIC_KEY=sb_publishable_dFKnLJZfWkXfRVr9l97rEm5s8RweReFt

Ready to test?

Use the API Sandbox to make real test calls without moving money.

Open Sandbox