Developer Documentation

MASTER PAY Integration Guide

Everything you need to integrate SMS-based wallet payment verification into your ecommerce flow. Step-by-step, with copy-pasteable code in Node, PHP, Python and cURL.

Get Started

Overview

MASTER PAY verifies wallet, bank, and UPI payments by matching the customer's transaction ID against the actual SMS your bound Android device receives. We don't process payments — we only confirm them.

This guide walks you through the full integration: from creating your merchant account to having verified transactions land in your dashboard.

Three things make a payment work
  1. A configured gateway — your wallet/bank account where money lands
  2. A bound APK on the phone that receives the SMS
  3. An API call from your backend (or a customer-typed TxnID)
Get Started

Quick Start (5 minutes)

  1. 1. Sign up at /register — pick your country (currency auto-set)
  2. 2. Note your API Key and Device Auth Key — both shown under Integration credentials on your dashboard overview
  3. 3. Add a Gateway — your wallet/bank account number (or last 4 of bank acct)
  4. 4. Install the APK on your shop's phone, paste the Device Auth Key
  5. 5. Test with a checkout session (see below)
Test the API now — replace pk_live_... with your key
bash
curl -X POST https://checkout.masterpay.it.com/api/payment/sessions \
  -H "X-API-Key: pk_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 500,
    "order_id": "TEST-001",
    "redirect_url": "https://example.com/done"
  }'

The response gives you a checkout_url — open it in a browser, pick a gateway, paste a TxnID, click Verify. That's the whole flow.

Get Started

How It Works

MASTER PAY sits between the merchant's e-commerce backend, the customer's browser, and the bound Android phone receiving wallet SMS.

┌─────────────────────┐                                  ┌─────────────────────┐
│  Customer's browser │                                  │  Vendor's bound     │
│  on yourstore.com   │                                  │  phone (MASTER PAY) │
└──────────┬──────────┘                                  └─────────────────────┘
           │   1) POST /api/payment/sessions  (+ vendor_id)        │
           │   ◄──────── checkout_url ────────                     │
           ▼                                                       │
┌─────────────────────┐  2) redirect to checkout_url              │
│  MASTER PAY hosted   │ ◄─────────────────────────────────        │
│  /pay/<sessionId>   │                                            │
└──────────┬──────────┘                                            │
           │   3) customer pays, submits                           │
           │      TxnID + sender number + screenshot               │
           ▼                                                       ▼
       transaction.status = pending          ◄────── wallet SMS arrives
           │                                          │
           ├──── 4a) auto-match SMS vs TxnID ◄────────┘
           │                                          │
           └──── 4b) no match → notify ONLY this      │
           │          vendor's phone → Approve/Reject ◄┘
           ▼
       transaction.status = success  (or failed)
           │   5) redirect customer back to merchant
           ▼
┌─────────────────────┐
│ yourstore.com/done  │
│ ?status=success     │
└─────────────────────┘
  • 1. Customer hits your checkout — your backend calls POST /api/payment/sessions
  • 2. We return a hosted checkout_url — you redirect the customer
  • 3. Customer pays from their wallet app, then submits the TxnID + sender number + a payment screenshot on the checkout and clicks Verify — a pending transaction is created
  • 4. Your bound APK forwards the incoming wallet SMS to our backend (pure staging — nothing is auto-created from random SMS)
  • 5. The pending transaction resolves one of two ways: (a) the TxnID auto-matches a staged SMS, or (b) no match — so we notify only that vendor's phone to Approve / Reject (you can also Mark Paid / Failed from the dashboard)
  • 6. Once resolved as success (or failed), the customer is redirected back to you with the status
Concepts

Authentication

MASTER PAY uses three credentials, each for a different actor:

CredentialScopeUsed byPurpose
api_key (pk_live_...)Per brandMerchant's serverCreate checkout sessions, query session status
secret_key (sk_live_...)Per brandMerchant's serverSign webhook payloads (future)
device_auth_key (PV-…)Per merchantThe APK on your phoneBind device, upload SMS, poll for pending verifications
JWT (Bearer)Per sessionMerchant dashboardLogged-in merchant access to /api/merchant/*
Keep keys server-side
Never expose api_key or device_auth_key to the browser. They belong on your backend (api_key) or inside the bound APK (device_auth_key) — never in client-side JavaScript.
Concepts

Gateways

A gateway is one wallet/bank account where you receive customer payments — e.g. bKash Personal · 01711111111, or UPI · last-4-of-bank XX6788. Each gateway has an account_number that gets matched against incoming SMS.

Multiple identifiers per gateway
You can comma-separate identifiers in a single gateway — we match if any one appears in the SMS body. Example: 01711111111, XX6788. Only add identifiers your provider's SMS actually contains: every extra one is another chance to match an unrelated message.
UPI (India): use the bank account, not the UPI ID
For GPay and PhonePe gateways, put your UPI ID in the UPI ID field and your bank account number (or its last 4 digits, e.g. 4328) in Account Number — not your UPI ID or mobile number. GPay and PhonePe don't send payment SMS; your bank does, and it quotes the account (“A/c XX4328 credited by Rs…”) and never the UPI ID. Also pick your receiving bank so we can reject forged messages.
Concepts

The APK

The MASTER PAY Android app runs on the phone you use to receive wallet SMS. It does one job: read incoming wallet SMS and forward them to our backend as data staging. The SMS only becomes a verification when a customer submits a matching TxnID through your checkout — random unrelated SMS sit unused and never become orphan transactions.

The APK also receives verify requests for any pending row a customer submits that hasn't auto-matched yet, so the merchant can Approve / Reject from the phone (mirror of the dashboard's Mark Paid / Mark Failed).

The vendor installs the APK on the phone holding their SIM, opens it, and pastes the Device Auth Key you gave them (PV-…, ~350 characters — copy it, never retype). A marketplace binds no phone of its own.

For Android developers building/extending the APK, see the full contract at docs/APK_API.md in the repo.

Integration

Step 1 — Register

Visit /register. Fill in your business details. Your country choice determines your default currency (India → INR, Bangladesh → BDT, US → USD, etc.) — you can override per-session later.

On successful registration, three things are generated for you:

  • api_key (pk_live_...) — for your backend
  • secret_key (sk_live_...) — for webhook signatures (future)
  • A default brand matching your domain

You get no device auth key. That belongs to each vendor — POST /api/vendors returns one per seller.

Integration

Step 2 — Onboard a Vendor

You never hold a payment number — your vendors do. Create one per seller with POST /api/vendors, hand them the device_auth_key (PV-XXXXXX) it returns, and they register at /vendor/register and add their own bKash / Nagad number in their own panel.

The vendor's Account Number must contain what the wallet's SMS will mention — for UPI/bank deposits, that's the bank account suffix (e.g. XX6788), not the GPay phone (the bank SMS doesn't echo it).

Only some of your sellers will ever do this, and that's expected. Call GET /api/vendors/availability before rendering a Pay button to find out which ones can take money — the rest come back payable: false with a ready-to-render message.

Integration

Step 3 — Bind the APK

The vendor installs MASTER PAY on the Android phone that receives their wallet SMS, and pastes the device_auth_key you gave them. The phone shows up in their panel at /vendor/dashboard/device as Online. A marketplace binds no phone of its own.

From this moment, every wallet SMS that arrives on the phone gets forwarded to MASTER PAY automatically.

Integration

Step 4 — Hosted Checkout

When a customer reaches your checkout, your backend creates a payment session and redirects them to the MASTER PAY checkout URL.

js
// Node.js (Express)
app.post('/checkout', async (req, res) => {
  const r = await fetch('https://checkout.masterpay.it.com/api/payment/sessions', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-API-Key':     process.env.PAYVERIFY_API_KEY,
    },
    body: JSON.stringify({
      amount:        req.body.amount,
      order_id:      req.body.order_id,
      redirect_url:  'https://yourstore.com/payment/result',
      customer_phone: req.body.customer_phone,
    }),
  });
  const { checkout_url } = await r.json();
  res.redirect(checkout_url);
});
Session lifecycle
Sessions stay live for 24 hours after creation. Once a customer submits a TxnID and we verify it, the session flips to success. If they cancel, it flips to cancelled. Beyond 24 hours, a still-pending session auto-expires.
Integration

Step 5 — Verify on Return

After payment, the customer is redirected back to your redirect_url with query params:

text
https://yourstore.com/payment/result?session_id=eNOuUpsg2IGX4ko4HbFL&status=success
Don't trust query params
A bad actor could craft ?status=success directly without paying. Always verify server-side by calling GET /api/payment/sessions/:id.
Heads up — pending returns are normal
The checkout waits ~15 s for the APK to confirm, then redirects the customer back to you regardless. So ?status=pending simply means "not resolved yet" — not "failed". Render a "we're confirming your payment" page and poll GET /sessions/:id server-side until it flips to success / failed / expired (within the 24-hour session window). Treating pending as failure will reject paying customers.
js
app.get('/payment/result', async (req, res) => {
  const r = await fetch(
    `https://checkout.masterpay.it.com/api/payment/sessions/${req.query.session_id}`,
    { headers: { 'X-API-Key': process.env.PAYVERIFY_API_KEY } }
  );
  const { session } = await r.json();
  if (session.status === 'success') {
    await markOrderPaid(session.order_id);
    return res.render('order-confirmed', { session });
  }
  if (session.status === 'pending') {
    // Verification didn't finish before redirect. Show a "processing" page;
    // keep polling GET /sessions/:id server-side until it resolves or expires.
    return res.render('payment-processing', { session });
  }
  res.render('payment-failed', { reason: session.status });
});
Integration

Marketplace / Vendors

Hosting many independent sellers under one site? Give each seller their own phone to confirm payments — a payment for Seller A only buzzes Seller A's phone, never Seller B's. You create one vendor per seller; each gets its own device_auth_key and its own payment numbers.

Single-seller? Skip this.
Vendors are fully optional. If you don't pass vendor_id, everything works against your one default account exactly as before.
  1. 1. A seller signs up on your site → you call POST /api/vendors → we return a vendor_id and a device_auth_key.
  2. 2. The seller installs the APK and pastes that device_auth_key → their phone is bound to that vendor.
  3. 3. You add the seller's wallet number → POST /api/vendors/:id/gateways.
  4. 4. A customer buys from that seller → create the session with "vendor_id": "…". The checkout shows only that vendor's numbers, and only that vendor's phone is notified.
Each vendor gets their own panel
Beyond the API, every vendor can log into their own dashboard — separate from yours — to see only their stats and transactions (Approve / Reject), manage their own payment numbers, and download the APK. Nothing extra for you to provision: the device_auth_key you already give the seller is their invite.
  1. The seller opens /vendor/register, enters the PV-XXXXXX code you gave them, and picks a username & password.
  2. They sign in at /vendor/login → Overview · Transactions (Approve / Reject) · Gateways · Device & App · Profile — all scoped to just their account.
  3. The seller can add/edit their own gateways from the panel — so step 3 above (calling POST /api/vendors/:id/gateways yourself) is optional.

A vendor never sees another vendor's data, and approving a payment from the panel debits your verification wallet — exactly like the APK auto-confirm. If your account is suspended, your vendors' panels are frozen too.

POST/api/vendorsauth: X-API-Key

Create a vendor (seller) under your merchant account. Free and immediately usable. Pass your own external_id to make this idempotent — repeat calls with the same external_id return the same vendor instead of duplicating.

Request body
json
{
  "label":       "Riya's Sarees",   // optional
  "external_id": "seller_4821"       // optional, YOUR seller id (idempotent)
}
Response (200)
json
// 201 Created (or 200 with "existed": true if external_id already mapped)
{
  "vendor": {
    "vendor_id":       "0f4c…-uuid",
    "label":           "Riya's Sarees",
    "external_id":     "seller_4821",
    "device_auth_key": "PV-7K2QF9",   // give this to the seller for the APK
    "is_default":      false,
    "created_at":      "2026-06-20T10:00:00Z"
  }
}
GET/api/vendorsauth: X-API-Key

List every vendor under your merchant account. The Primary account (is_default: true) is your own default receiving unit and is listed first.

Response (200)
json
{
  "vendors": [
    {
      "vendor_id":       "0f4c…-uuid",
      "label":           "Riya's Sarees",
      "external_id":     "seller_4821",
      "device_auth_key": "PV-7K2QF9",
      "is_default":      false,
      "created_at":      "2026-06-20T10:00:00Z"
    }
  ]
}
GET/api/vendors/:idauth: X-API-Key

Fetch one vendor with its gateways and a live count of bound (active) devices. 404 if the id isn't a vendor under your merchant.

Response (200)
json
{
  "vendor": {
    "vendor_id":       "0f4c…-uuid",
    "label":           "Riya's Sarees",
    "external_id":     "seller_4821",
    "device_auth_key": "PV-7K2QF9",
    "is_default":      false,
    "created_at":      "2026-06-20T10:00:00Z",
    "bound_devices":   1,
    "gateways": [
      {
        "id":             "9a1b…-uuid",
        "provider":       "bkash",
        "variant":        "personal",
        "account_number": "01711111111",
        "label":          null,
        "is_enabled":     true,
        "created_at":     "2026-06-20T10:05:00Z"
      }
    ]
  }
}

// 404 — not your vendor
{ "error": "Vendor not found" }
POST/api/vendors/:id/gatewaysauth: X-API-Key

Add a payment number (gateway) for a vendor — the wallet/bank account where that seller receives money. Same rules as the dashboard's gateways, scoped to this vendor. One gateway per provider+variant per vendor.

Request body
json
{
  "provider":       "bkash",          // a provider your platform admin enabled
  "variant":        "personal",
  "account_number": "01711111111",    // what the wallet SMS will mention
  "label":          "Riya bKash",     // optional
  "charge_value":   0,                 // optional, fee added to the bill
  "charge_type":    "fixed",           // "fixed" | "percent"
  "discount_value": 0,                 // optional
  "discount_type":  "fixed"
}
Response (200)
json
// 201 Created
{
  "gateway": {
    "id":             "9a1b…-uuid",
    "account_id":     "0f4c…-uuid",   // = vendor_id
    "provider":       "bkash",
    "variant":        "personal",
    "account_number": "01711111111",
    "label":          "Riya bKash",
    "is_enabled":     true,
    "created_at":     "2026-06-20T10:05:00Z"
  },
  "retroactively_matched": 0
}

// 400 — unknown provider/variant, or missing account_number
{ "error": "Provider \"bkash\" not found or disabled" }

// 409 — vendor already has a gateway for this provider+variant
{ "error": "This account already has a gateway for this provider and type. Each account allows one per provider+variant." }
GET/api/vendors/:id/gatewaysauth: X-API-Key

List a vendor's gateways.

Response (200)
json
{
  "gateways": [
    {
      "id":             "9a1b…-uuid",
      "account_id":     "0f4c…-uuid",
      "provider":       "bkash",
      "variant":        "personal",
      "account_number": "01711111111",
      "label":          "Riya bKash",
      "min_amount":     null,
      "max_amount":     null,
      "charge_value":   "0",
      "charge_type":    null,
      "discount_value": "0",
      "discount_type":  null,
      "balance_check":  false,
      "is_enabled":     true,
      "created_at":     "2026-06-20T10:05:00Z",
      "updated_at":     "2026-06-20T10:05:00Z"
    }
  ]
}
PATCH/api/vendors/:id/gateways/:gatewayIdauth: X-API-Key

Update a vendor gateway. provider/variant are fixed once created; account_number, label, charges and discounts may change. Omitted fields are reset (send the full object you want to keep).

Request body
json
{
  "account_number": "01722222222",
  "label":          "Riya bKash (new)"
}
Response (200)
json
{
  "gateway": {
    "id":             "9a1b…-uuid",
    "account_id":     "0f4c…-uuid",
    "provider":       "bkash",
    "variant":        "personal",
    "account_number": "01722222222",
    "label":          "Riya bKash (new)",
    "is_enabled":     true,
    "updated_at":     "2026-06-20T11:00:00Z"
  },
  "retroactively_matched": 0
}

// 404 — gateway not found / not yours
{ "error": "Gateway not found" }
DELETE/api/vendors/:id/gateways/:gatewayIdauth: X-API-Key

Remove a vendor gateway.

Response (200)
json
// 200 OK
{ "ok": true }

// 404 — gateway not found / not yours
{ "error": "Gateway not found" }
Vendor endpoint errors
All vendor endpoints use your X-API-Key — a bad/missing key returns 401 { "error": "Invalid API key" }. A vendor_id that isn't one of your vendors returns 404 (reads) or 400 { "error": "Invalid vendor_id for this merchant" } (on session create). A suspended merchant returns 403.
API Reference

Payment Sessions

POST/api/payment/sessionsauth: X-API-Key

Create a new checkout session for a customer. order_id is enforced unique-per-merchant (see status codes below).

Request body
json
{
  "amount":         500.00,
  "order_id":       "ORD-1001",
  "redirect_url":   "https://yourstore.com/done",
  "currency":       "USD",
  "customer_phone": "+8801712345678",
  "customer_name":  "Rahim Khan",
  "vendor_id":      "0f4c…-uuid",            // optional — marketplaces, see Vendors
  "metadata":       { "cart_id": "cart-42" }
}
Response (200)
json
// 201 Created — fresh session
{
  "session_id":   "eNOuUpsg2IGX4ko4HbFL",
  "checkout_url": "https://masterpay.it.com/pay/eNOuUpsg2IGX4ko4HbFL",
  "expires_at":   "2026-05-12T12:00:00Z",
  "status":       "pending"
}

// 200 OK — idempotent: same order_id while a prior session is still alive
{
  "session_id":   "eNOuUpsg2IGX4ko4HbFL",
  "checkout_url": "https://masterpay.it.com/pay/eNOuUpsg2IGX4ko4HbFL",
  "expires_at":   "2026-05-12T12:00:00Z",
  "status":       "pending",
  "existed":      true
}

// 409 Conflict — order already paid (any prior session is success)
{ "error": "This order has already been paid.", "existing_session_id": "..." }

// 402 Payment Required — merchant wallet too low to cover verification fee
{ "error": "Merchant wallet has insufficient balance. Top up to resume.",
  "insufficient_balance": true, "balance": 0, "fee": 2, "threshold": 10 }
Handling the response
Always use data.checkout_url regardless of whether you get 201 or 200 — the URL is the same when existed: true. Treat 409 as "this order is done — show the customer the paid state, don't retry." Treat 402 as "merchant configuration issue — surface a friendly retry-later message."
GET/api/payment/sessions/:idauth: X-API-Key

Fetch the current status of a session. Use this after the customer returns (and poll it while pending). Always includes latest_transaction — the most recent attempt of ANY status, carrying the UTR, payment method, and the customer’s sender account. successful_transaction is only present once a payment succeeds.

Response (200)
json
// ── PENDING — submitted, waiting for the SMS/APK to confirm ──
{
  "session": {
    "id":         "eNOuUpsg2IGX4ko4HbFL",
    "order_id":   "ORD-1001",
    "amount":     "500.00",
    "currency":   "BDT",
    "status":     "pending",
    "expires_at": "2026-05-13T11:30:00Z",   // 24h after creation
    "created_at": "2026-05-12T11:30:00Z",
    "successful_transaction": null,
    "latest_transaction": {
      "txnid_submitted": "BKX92H1",          // UTR the customer entered
      "method":          "nagad",            // gateway provider (bkash/nagad/rocket/upay)
      "variant":         "personal",
      "account_number":  "01711111111",      // your receiving wallet
      "sender_account":  "01822222222",      // number the customer paid FROM
      "amount":          "500.00",
      "status":          "pending",
      "result_source":   null,
      "failure_reason":  null,
      "verified_at":     null
    }
  }
}

// ── SUCCESS (auto) — matched against the bound APK's SMS ──
{
  "session": {
    "id": "eNOuUpsg2IGX4ko4HbFL", "order_id": "ORD-1001",
    "amount": "500.00", "currency": "BDT", "status": "success",
    "successful_transaction": {
      "txnid_submitted": "BKX92H1",
      "verified_at":     "2026-05-12T11:34:10Z",
      "result_source":   "apk"               // or "sms_inbound" / "sms_late_match"
    },
    "latest_transaction": {
      "txnid_submitted": "BKX92H1", "method": "nagad", "variant": "personal",
      "account_number": "01711111111", "sender_account": "01822222222",
      "amount": "500.00", "status": "success",
      "result_source": "apk", "failure_reason": null,
      "verified_at": "2026-05-12T11:34:10Z"
    }
  }
}

// ── SUCCESS (manually approved) — merchant clicked "Mark Paid" ──
//    Identical to above, except result_source is "manual".
{
  "session": {
    "status": "success",
    "successful_transaction": { "txnid_submitted": "BKX92H1", "result_source": "manual",
                                "verified_at": "2026-05-12T11:40:00Z" },
    "latest_transaction": { "txnid_submitted": "BKX92H1", "method": "nagad",
                            "sender_account": "01822222222", "amount": "500.00",
                            "status": "success", "result_source": "manual",
                            "failure_reason": null, "verified_at": "2026-05-12T11:40:00Z" }
  }
}

// ── FAILED (manually rejected) — merchant clicked "Mark Failed" + reason ──
//    NOTE: the transaction is failed, but session.status stays "pending"
//    so the customer can still retry with a fresh TxnID.
{
  "session": {
    "id": "eNOuUpsg2IGX4ko4HbFL", "order_id": "ORD-1001",
    "status": "pending",
    "successful_transaction": null,
    "latest_transaction": {
      "txnid_submitted": "BKX92H1", "method": "nagad",
      "sender_account": "01822222222", "amount": "500.00",
      "status":         "failed",
      "result_source":  "manual",            // "apk" if rejected from the phone
      "failure_reason": "TxnID not found in our wallet SMS",
      "verified_at":    "2026-05-12T11:40:00Z"
    }
  }
}

// ── EXPIRED — no resolution within the 24h window ──
{ "session": { "status": "expired", "successful_transaction": null, "latest_transaction": null } }

// ── CANCELLED — customer abandoned the checkout ──
{ "session": { "status": "cancelled", "successful_transaction": null } }
Reading the states
Two status fields matter. session.status is the order-level state: pending · success · expired · cancelled. latest_transaction.status is the attempt-level verdict: pending · success · failed. A manual rejection fails the transaction but keeps the session pending (so the customer can retry) — so to detect a rejection, read latest_transaction.status === "failed" plus its failure_reason. result_source tells you how it resolved: apk (phone confirmed), sms_inbound / sms_late_match (auto-matched SMS), or manual (merchant Mark Paid/Failed).
API Reference

Manual Verify

POST/api/merchant/verifyauth: Bearer JWT (merchant login)

Manually verify a TxnID given to you out-of-band. Used by the dashboard's Verify page. Looks up SMS, extracts amount + gateway, creates a transaction if it all matches.

Request body
json
{
  "txnid": "613384596583"
}
Response (200)
json
{
  "matched": true,
  "already_existed": false,
  "transaction": {
    "txnid_submitted": "613384596583",
    "amount":          1.00,
    "provider":        "bkash",
    "variant":         "personal",
    "account_number":  "XX6788"
  },
  "sms": { "sender": "AD-AXISBK-S", "body": "INR 1.00 credited ...", "received_at": "..." }
}
API Reference

Devices (APK)

These endpoints are called by your Android APK, authenticated by device_auth_key in the body. Never call from a server or browser.

Wallet-empty (402) handling
Every APK endpoint EXCEPT /bind and /unbind returns 402 with { insufficient_balance: true, balance, fee, threshold } when the merchant's wallet doesn't have enough to cover the per-verification fee. The APK should switch to a "Wallet empty — please top up" screen and keep heartbeating; when /heartbeat returns 200 again, auto-recover.
POST/api/device/bindauth: device_auth_key

Register a phone with the merchant account. Idempotent — re-binding the same device_id refreshes the row.

Request body
json
{
  "auth_key":     "PV-XXXXXX",
  "device_id":    "android-stable-id",
  "model":        "CPH1937",
  "manufacturer": "OPPO",
  "os_version":   "11"
}
POST/api/device/unbindauth: device_auth_key

User taps "Disconnect" in the app. Soft-delete — the row stays in history.

Request body
json
{ "auth_key": "PV-XXXXXX", "device_id": "android-stable-id" }
POST/api/device/smsauth: device_auth_key

Forward incoming wallet SMS. Backend stores the SMS as staging data only — it becomes a verification when a customer submits a matching TxnID through the checkout. Unmatched SMS never become orphan transactions.

Request body
json
{
  "auth_key":  "PV-XXXXXX",
  "device_id": "android-stable-id",
  "messages": [{
    "sender":      "bKash",
    "body":        "Cash In Tk 500.00 successful. TrxID: BKX92H1 from 01712345678",
    "received_at": "2026-05-12T11:25:30Z"
  }]
}
POST/api/device/pollauth: device_auth_key

Fetch pending verifications waiting for a decision. Call on a timer (every 10–30s). Each returned row has the data needed to render an Approve/Reject prompt offline.

Request body
json
{ "auth_key": "PV-XXXXXX", "device_id": "android-stable-id" }
Response (200)
json
{
  "verifications": [{
    "verification_id": 421,
    "txnid_submitted": "BKX92H1",
    "amount":          500.00,
    "currency":        "BDT",
    "customer_phone":  "01712345678",
    "customer_name":   "Customer Name",
    "order_id":        "ORD-9911",
    "provider":        "bkash",
    "variant":         "personal",
    "account_number":  "01799999999",
    "created_at":      "2026-05-12T11:25:30Z"
  }]
}
POST/api/device/reportauth: device_auth_key

Resolve a pending verification from the APK ("Approve" / "Reject" buttons). Mirrors the web dashboard's Mark Paid / Mark Failed.

Request body
json
{
  "auth_key":        "PV-XXXXXX",
  "device_id":       "android-stable-id",
  "verification_id": 421,
  "result":          "success",
  "matched_sms":     "Cash In Tk 500.00 successful. TrxID: BKX92H1...",
  "failure_reason":  null
}
Response (200)
json
{
  "ok":              true,
  "verification_id": 421,
  "status":          "success",
  "session_id":      "Y_giE7l9...",
  "transaction": {
    "id":            421,
    "status":        "success",
    "result_source": "apk"
  }
}
POST/api/device/transactionsauth: device_auth_key

List recent transactions (pending + history) for the merchant on the APK. Filter by status, search by TxnID or order_id.

Request body
json
{
  "auth_key":  "PV-XXXXXX",
  "device_id": "android-stable-id",
  "status":    "pending",
  "q":         "BKX92",
  "limit":     50
}
Response (200)
json
{
  "transactions": [{
    "id":              421,
    "txnid_submitted": "BKX92H1",
    "amount":          500.00,
    "status":          "pending",
    "result_source":   null,
    "verified_at":     null,
    "failure_reason":  null,
    "provider":        "bkash",
    "account_number":  "01799999999",
    "order_id":        "ORD-9911",
    "customer_name":   "Customer Name",
    "created_at":      "2026-05-12T11:25:30Z"
  }]
}
POST/api/device/verifyauth: device_auth_key

Paste a TxnID into the APK. Backend searches received SMS, validates against a configured gateway, creates a successful transaction if everything aligns. Mirrors the web Verify page.

Request body
json
{
  "auth_key":  "PV-XXXXXX",
  "device_id": "android-stable-id",
  "txnid":     "BKX92H1"
}
Response (200)
json
{
  "matched": true,
  "already_existed": false,
  "transaction": {
    "id": 422, "txnid_submitted": "BKX92H1", "amount": 500.00,
    "status": "success", "result_source": "manual_verify",
    "provider": "bkash", "account_number": "01799999999"
  },
  "sms": { "id": 7711, "sender": "bKash", "body": "...", "received_at": "..." }
}
Manual verification flow inside the APK
The APK has the same verification powers as the web dashboard:
  • List view: call /api/device/transactions for full history, or /api/device/poll for pending-only.
  • Approve / Reject: call /api/device/report with result: "success" or "failed".
  • Type-a-TxnID verify: call /api/device/verify — searches SMS, auto-resolves.
All of these flip the same transactions row that the web dashboard reads, so a decision from the phone shows up on the dashboard immediately.

Full APK contract (poll/report patterns, SMS matching rules, permissions) lives in docs/APK_API.md.

API Reference

Errors & Status Codes

CodeMeaning
200OK (existing resource fetched)
201Created (new session / new resource)
202Accepted (TxnID submitted, verification pending)
400Bad request — check error message; usually a missing/invalid field
401Invalid or missing API key (X-API-Key or JWT)
402Merchant wallet has insufficient balance — top up to resume
403Forbidden — usually merchant suspended
404Resource not found (wrong id, or not yours)
409Conflict — duplicate (TxnID reused, order already paid, session in wrong state)
429Too many requests — back off, see Rate Limits below
5xxServer error — retry with exponential backoff

Error response shape

Customer-facing endpoints (/api/payment/sessions, /api/checkout/:id/submit) split every error into two tiers so you can safely route the right text to each audience:

json
{
  "error":            "Services currently unavailable.",
  "merchant_message": "Merchant wallet has insufficient balance to cover the per-verification fee. Top up at the dashboard.",
  "insufficient_balance": true,
  "code":             "merchant_wallet_empty"
}
  • errorsafe to display to your customer. Neutral, no MASTER PAY-specific language, no numeric leaks.
  • merchant_messagefor your server logs / admin alerts only. Tells you exactly what to fix. Never render this to the end customer.
  • code — stable machine-readable identifier (e.g. merchant_wallet_empty, order_already_paid). Use this for programmatic branching.
  • Other fields like insufficient_balance or existing_session_id are programmatic hints — safe but uninformative if shown.
Don't dump the whole response to the customer
A common integration bug: alert(JSON.stringify(response)) on the customer's screen. They see "Merchant wallet has insufficient balance" and lose trust in your site. Always read error and display only that.

Reference handler (Node):

js
const r = await fetch(...);
const body = await r.json();
if (!r.ok) {
  // Log the technical detail for ops
  console.warn('[masterpay]', body.merchant_message || body.error, '— code:', body.code);
  // Show ONLY the safe text to the customer
  return res.status(503).json({ error: body.error });
}
API Reference

Rate Limits

MASTER PAY throttles every public endpoint to protect against brute force, TxnID mining, and runaway integrations. When you exceed a limit you get HTTP 429 instead of the normal response — the request did not run.

Per-endpoint limits

EndpointLimitWindowKeyed by
POST /api/payment/sessions601 minAPI key
POST /api/checkout/:id/submit61 minIP + session
POST /api/merchant/login815 minIP
POST /api/merchant/verify201 minIP
POST /api/device/sms1201 mindevice_auth_key

429 response shape

Every throttled response carries a body explaining the retry window and headers a polite client can read.

http
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit:     60
X-RateLimit-Remaining: 0
X-RateLimit-Reset:     1716210000
Retry-After:           37

{
  "error":               "API rate limit exceeded. Slow down session creation.",
  "retry_after_seconds": 37
}
  • X-RateLimit-Limitthe cap for this endpoint.
  • X-RateLimit-Remaininghow many calls you have left in the current window.
  • X-RateLimit-ResetUnix timestamp when the counter resets.
  • Retry-Afterseconds to wait before retrying (also in the body as retry_after_seconds).
429 does NOT mean the action failed
A 429 means the request did not run at all. It is safe to retry after waiting Retry-After seconds. For /sessions, slow your integration. For /submit, ask the customer to wait a moment and try again — don't auto-retry on their behalf or you'll just hit the limit harder.

Polite retry pattern (Node)

js
async function callWithBackoff(url, init, maxAttempts = 3) {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    const r = await fetch(url, init);
    if (r.status !== 429) return r;

    // Server told us how long to wait; honour it (cap at 60s).
    const ra = Number(r.headers.get('Retry-After')) ||
               (await r.clone().json().then(b => b.retry_after_seconds).catch(() => 1));
    const waitMs = Math.min(60, Math.max(1, ra)) * 1000;
    if (attempt === maxAttempts) return r; // give up after last attempt
    await new Promise((res) => setTimeout(res, waitMs));
  }
}
Need a higher limit?
These caps fit normal merchant traffic with plenty of headroom. If you have a legitimate burst use case (bulk reconciliation, migration, etc.) email support before you launch — raising your limit ahead of time is easier than recovering from blocked traffic.
API Reference

Troubleshooting

Issues integrators hit most often, with the fix.

PHP cURL: SSL routines:ssl3_read_bytes:tlsv1 unrecognized name

TLS handshake error. The client didn't send a Server Name Indication (SNI) the cert recognizes, and old OpenSSL (≤ 1.0.x, common in PHP 5.x / 7.0–7.3) treats it as fatal instead of a warning. Three checks, in order:

  1. Hostname must be exact. Use https://checkout.masterpay.it.com — not an IP, not www., not a CDN alias. Calling by IP doesn't send SNI at all.
  2. Don't bypass DNS. Do NOT set CURLOPT_RESOLVE or pass --resolve on the CLI — both can send the wrong SNI name.
  3. Force TLS 1.2 and keep verification on. Don't disable CURLOPT_SSL_VERIFYPEER or CURLOPT_SSL_VERIFYHOST — that often makes the problem worse, not better.

Working PHP example:

php
<?php
$ch = curl_init('https://checkout.masterpay.it.com/api/payment/sessions');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Content-Type: application/json',
        'X-API-Key: pk_live_...',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'amount'         => 1.00,
        'order_id'       => 'ORD-1',
        'redirect_url'   => 'https://yoursite.com/payment/result',
        'customer_phone' => '7557012345',
        'customer_name'  => 'Customer Name',
    ]),
    CURLOPT_SSLVERSION     => CURL_SSLVERSION_TLSv1_2,
    CURLOPT_SSL_VERIFYPEER => true,
    CURLOPT_SSL_VERIFYHOST => 2,
]);
$response = curl_exec($ch);
if (curl_errno($ch)) {
    error_log('MASTER PAY cURL error: ' . curl_error($ch));
}
curl_close($ch);
$body = json_decode($response, true);
header('Location: ' . $body['checkout_url']);

Still failing? Test from the shell first:

bash
curl -v https://checkout.masterpay.it.com/api/providers
  • curl works, PHP fails → upgrade PHP/OpenSSL (PHP ≥ 7.4 with OpenSSL ≥ 1.1.1 fixes it permanently).
  • Both fail with the same SNI error → you're on the wrong hostname, or behind a corporate proxy mangling SNI.
  • Both succeed but your code still errors → it's your request body, not TLS. Check the response status & body.

404 from /api/payment/sessions

You're hitting the wrong host. MASTER PAY has two services — the dashboard UI and the API — on different domains. The API base URL is https://checkout.masterpay.it.com. Pointing your backend at the dashboard URL returns 404 because the dashboard doesn't serve the API.

Sanity check: curl https://checkout.masterpay.it.com/api/providers should return JSON with the provider list (no auth needed). If it does, the URL is correct; if it doesn't, fix the base URL first.

401 Invalid or expired token

You sent the wrong header for the wrong endpoint:

  • Backend-to-MASTER PAY (/api/payment/...): use X-API-Key: pk_live_...
  • Merchant dashboard / your own JWT calls (/api/merchant/...): use Authorization: Bearer <jwt>
  • APK calls (/api/device/...): use the auth_key field in the JSON body.

Trailing-slash double-slash //api/...

If your config stores the base URL with a trailing slash and your code appends /api/payment/..., you'll request https://checkout.masterpay.it.com//api/payment/sessions. Some nginx setups normalize this; others 404. Strip the trailing slash from your base URL.

402 insufficient_balance

MASTER PAY charges merchants a small per-verification fee, debited from the merchant's wallet on every successful verification. When the wallet drops below that fee, the merchant's operations are paused:

  • New POST /api/payment/sessions calls return 402 — xyz.com can't create new checkouts.
  • APK endpoints (poll/sms/report/verify/heartbeat) return 402 — the phone shows a "Wallet empty" screen.
  • /bind and /unbind stay available so the phone can disconnect/reconnect.

Fix: log into the merchant dashboard → WalletAdd Balance → pay via wallet. Operations resume on the next request after the recharge confirms.

409 "This order has already been paid"

You called POST /sessions with an order_id that already has a successful session. Don't retry — fetch the existing session via existing_session_id in the error body and treat the order as fulfilled.

Transactions stuck on Pending

The customer submitted a TxnID but no matching SMS has reached the backend. Two common causes:

  • No bound device. Check Dashboard → Devices. An "Online" row means the APK is heartbeating; it does NOT guarantee SMS is being forwarded. If the SMS log (Dashboard → SMS) is empty, the APK isn't uploading.
  • Gateway identifier mismatch. The number you set on the gateway must appear in the SMS body. For SMS that show only a masked bank suffix (e.g. XX6788), store multiple identifiers comma-separated.

As a fallback, the merchant can resolve any pending row from Dashboard → Transactions (Mark Paid / Mark Failed). Customers still on the checkout page (within ~15s of submitting) see the decision and are redirected; customers who've already returned to your site with status=pending pick up the new state through your backend's polling of GET /sessions/:id.

Production

Best Practices

  • 1. Always verify server-side. Don't trust query params on return — call GET /sessions/:id from your backend before fulfilling.
  • 2. Keep keys out of git + browser. Store api_key in env vars; never log it.
  • 3. Set customer_phone on the session. Tightens SMS matching, reduces false positives dramatically.
  • 4. Use HTTPS for redirect_url. No exceptions in production.
  • 5. Configure multiple identifiers per gateway. Mobile + bank suffix (comma-separated) means new banks rolling out won't silently stop working.
  • 6. Tell vendors to watch /vendor/dashboard/device. If a vendor's bound phone goes offline, their verifications queue but don't complete. You can't see or fix their phone — only they can.
  • 7. Keep your wallet topped up. Each successful verification debits a small fee from your wallet balance. Below the per-verification fee, all your endpoints return 402 and your APK is paused. The dashboard shows an amber warning before you hit empty — top up then. Wallet →
  • 8. Handle the order_id idempotency response. POST /sessions can return 200 (with existed:true) when the same order_id has a live pending session — use the URL it returns, don't create a parallel checkout.
Production

Webhooks (Coming Soon)

Today, you confirm payments by polling GET /sessions/:id after the customer returns.

Webhooks are on the roadmap. When live, MASTER PAY will POST to a URL on your domain whenever a session resolves, signed with HMAC-SHA256 of the body using your brand's secret_key:

http
POST https://yourstore.com/masterpay/webhook
X-MASTER PAY-Signature: t=1683900000,v1=<hmac-sha256-hex>

{ "event": "session.success", "session": { ... } }

Until then, polling on customer return is the recommended pattern.

Production

Support

Questions? Run into something the docs don't cover?

  • 📧 Email: support@masterpay.it.com
  • 🐛 Bugs / API issues: open a ticket from your dashboard
  • 📖 APK developer reference: docs/APK_API.md
  • 📖 Integration deep-dive: docs/INTEGRATION.md
That's everything. Time to ship → Create your merchant account