My Pay

Get started

Quickstart

From sign-up to first verified payment in three steps.

Base URL
[api_url not set]

Use this exact host in every request below — it works today. The branded domains [api_url not set] (REST) and [pay_url not set] (hosted checkout) will route to the same endpoints once the custom domain is connected — no code change required on your side.

  1. 01

    Create workspace

    Sign up and create your merchant workspace. Add your website and verify ownership.

  2. 02

    Generate API keys

    From the Developers tab, generate a publishable + secret key pair. Save the secret once.

  3. 03

    Poll the events feed

    Run a small cron on your server that calls GET /v1/events?since=<last_id> — no webhook URL or HTTPS endpoint needed.

  4. 04

    Create a session

    Call POST [api_url not set]/v1/sessions from your backend and redirect the customer to checkout_url.

How the whole flow works

We give you a single REST API. Your server calls it — no SDK and no webhook URL in between. Here's the whole picture at a glance.

Customer ──Pay Now──▶ Your server ──POST /v1/sessions──▶ My Pay
                                  ◀──── checkout_url ───
        ◀──── 302 redirect ────
Customer ── pays on hosted checkout (crypto / mobile banking) ──▶ My Pay
        ◀── redirect → success_url / failed_url / cancel_url ────

Your cron (every 60s) ──GET /v1/events?since=last_id──▶ My Pay
                       ◀──── [ session.approved, … ] ───
Your server → mark order paid in DB
What lives on your website
  • MYPAY_SECRET — in your env / config file
  • Pay route — calls /v1/sessions
  • 3 return pages — success / cancel / failed
  • Poll script — reads /v1/events
  • Cron entry — runs the poll script every 60 sec
What you don't need
  • ❌ A webhook endpoint or public HTTPS URL
  • ❌ A signing secret / signature verification
  • ❌ Any SDK or library install
  • ❌ Open inbound ports on your server
  • ❌ A separate domain for test mode
For file-by-file ready code for your platform, see the Integration guide — 20+ stacks including PHP, Laravel, WordPress, Node, Django, and Next.js.
curl
curl [api_url not set]/v1/sessions \
  -H "Authorization: Bearer sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 4250,
    "currency": "BDT",
    "reference_id": "INV-2046",
    "customer_email": "alice@example.com",
    "success_url": "https://yoursite.com/thanks",
    "metadata": { "order_id": "INV-2046" }
  }'

Authentication

API keys & headers

All server-side requests are authenticated with a secret key. Pass it as a Bearer token or via x-api-key header.

Publishable
pk_live_…

Safe to embed in client-side code. Used by the embed widget to identify your merchant.

Secret
sk_live_…

Server-side only. Never expose to browsers. Rotate immediately if leaked.

http
POST /v1/sessions HTTP/1.1
Host: [api_url not set]
Authorization: Bearer sk_live_xxx
Content-Type: application/json

REST API

Create a payment session

POST [api_url not set]/v1/sessions — returns a hosted checkout_url to redirect your customer to.

javascript
const res = await fetch("[api_url not set]/v1/sessions", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.MYPAY_SECRET}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    amount: 4250, currency: "BDT",
    reference_id: "INV-2046",
    success_url: "https://yoursite.com/thanks",
  }),
});
const session = await res.json();
res.redirect(session.checkout_url);

Request fields

FieldTypeDescription
amountnumberRequired. Amount in major units (e.g. 4250 = ৳4,250).
currencystringDefaults to BDT.
reference_idstringYour internal order/invoice id. Returned on every event in the /v1/events feed.
customer_emailstringOptional. Pre-fills checkout.
success_urlurlRedirect after successful payment.
cancel_urlurlRedirect when the customer clicks Cancel.
failed_urlurlRedirect if the payment is rejected/failed.
callback_urlurlOptional callback URL (currently unused — use polling instead).
metadataobjectUp to 50 key/value pairs, echoed back in events.

Tip: don't trust success_url alone to mark an order paid — the customer can manipulate the URL. Always confirm via the events feed or by calling GET /v1/sessions/{id}.

REST API

Get session status

GET [api_url not set]/v1/sessions/:id — fetch the current state of a session and its latest transaction. Useful for polling or reconciliation.

curl
curl [api_url not set]/v1/sessions/sess_abc123 \
  -H "Authorization: Bearer sk_live_xxx"
json
{
  "id": "sess_abc123",
  "status": "approved",          // pending | submitted | approved | rejected | expired | cancelled
  "amount": 4250,
  "currency": "BDT",
  "reference_id": "INV-2046",
  "metadata": { "order_id": "INV-2046" },
  "checkout_url": "[pay_url not set]/...",
  "created_at": "2026-05-17T09:21:00Z",
  "latest_transaction": {
    "status": "approved",
    "txid": "0xabc...",
    "approved_at": "2026-05-17T09:24:11Z"
  }
}

Use this for an instant status check (e.g. on success_url) without waiting for the next polling cycle. Authenticate with the same secret key you use for POST /v1/sessions.

Drop-in

Embed widget (no backend)

Add a Pay button anywhere on your site. The widget creates the session via your server endpoint and opens hosted checkout in a modal.

html
<script src="[api_url not set]/widget.js" defer></script>

<button
  data-mypay
  data-public-key="pk_live_xxx"
  data-create-url="/api/create-mypay-session"
  data-amount="4250"
  data-reference="INV-2046"
>
  Pay with My Pay
</button>

The widget POSTs to data-create-url on your backend, which calls our Sessions API with your secret key and returns the JSON response. Then it opens checkout_url in a centered modal and listens for completion.

javascript
window.My Pay.open({
  publicKey: "pk_live_xxx",
  createUrl: "/api/create-mypay-session",
  amount: 4250,
  reference: "INV-2046",
  onSuccess: (session) => console.log("paid", session),
  onCancel:  () => console.log("cancelled"),
});

Events

Polling & Events feed

My Pay does NOT send webhooks. Instead, your server pulls an ordered event feed on a schedule. Every status change (session/transaction/refund) is captured in the feed in order — you can never miss an event.

Why polling instead of webhooks?
  • No public HTTPS URL required — works on shared hosting, behind firewalls, in any framework.
  • No signature verification, no retry/backoff logic to maintain.
  • Events are stored chronologically with a monotonically increasing id. Save the last id and you can never miss an event, even after downtime.
  • Same flow works in test (sk_test_…) and live (sk_live_…) mode — the feed is scoped to the key's mode automatically.

GET /v1/events?since=<last_event_id>&limit=100

bash
curl [api_url not set]/v1/events?since=0&limit=100 \
  -H "Authorization: Bearer sk_live_xxx"
json
{
  "data": [
    {
      "id": 1024,
      "event_type": "session.approved",
      "resource_type": "session",
      "resource_id": "f7a1...",
      "mode": "live",
      "payload": {
        "id": "f7a1...",
        "status": "approved",
        "amount": 4250,
        "currency": "BDT",
        "reference_id": "INV-2046"
      },
      "created_at": "2026-05-19T16:42:11Z"
    }
  ],
  "last_event_id": 1024,
  "has_more": false
}

Event types

session.created

Customer opened a new checkout.

session.submitted

Customer submitted TXID/proof — pending review.

session.approved

Payment confirmed. Fulfil the order.

session.rejected

Proof rejected. Do NOT fulfil.

session.expired

Session timed out.

session.cancelled

Customer cancelled.

session.refunded

Session fully refunded.

transaction.approved

Transaction approved.

transaction.rejected

Transaction rejected.

refund.requested

Refund created.

refund.approved

Refund approved by admin.

refund.processed

Refund money sent.

refund.declined

Refund declined.

The "never miss an event" pattern

Store last_event_id in your database. On every poll, send it as ?since= and update it from the response. Run the poller every 10–30 seconds via cron.

typescript
// poll-mypay.ts — run every 15s via cron or setInterval
const API = "[api_url not set]";
const KEY = process.env.MYPAY_SECRET_KEY!;

async function pollOnce(lastId: number): Promise<number> {
  const r = await fetch(`${API}/v1/events?since=${lastId}&limit=100`, {
    headers: { Authorization: `Bearer ${KEY}` },
  });
  const { data, last_event_id } = await r.json();
  for (const ev of data) {
    if (ev.event_type === "session.approved") {
      // 👉 mark order paid using ev.payload.reference_id
    }
  }
  return last_event_id;
}

let lastId = Number(await readLastIdFromDb()) || 0;
lastId = await pollOnce(lastId);
await writeLastIdToDb(lastId);
Tip: when you want an instant confirmation in the customer's browser (after they hit success_url), call GET /v1/sessions/{id} directly — that returns the current status without waiting for the next poll cycle.

Refunds

Refunds API

Programmatically request refunds for approved transactions. Status changes flow through the events feed as refund.* events.

POST /v1/refunds

bash
curl [api_url not set]/v1/refunds \
  -H "Authorization: Bearer sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "transaction_id": "11111111-2222-3333-4444-555555555555",
    "amount": 4250,
    "reason": "Customer requested cancellation"
  }'

GET /v1/refunds/{id}

bash
curl [api_url not set]/v1/refunds/abc123 \
  -H "Authorization: Bearer sk_live_xxx"

GET /v1/refunds?status=&since=&limit=&cursor=

bash
curl "[api_url not set]/v1/refunds?status=requested&limit=50" \
  -H "Authorization: Bearer sk_live_xxx"

Transactions

Transactions API

Read access to the underlying payment attempts (one session can have multiple attempts).

GET /v1/transactions/{id}

bash
curl [api_url not set]/v1/transactions/abc \
  -H "Authorization: Bearer sk_live_xxx"

GET /v1/transactions?session_id=&status=&since=

bash
curl "[api_url not set]/v1/transactions?session_id=sess_abc&status=approved" \
  -H "Authorization: Bearer sk_live_xxx"

Balance

Balance API

Real-time view of your settled balance per currency, minus completed refunds and paid-out amounts.

GET /v1/balance

bash
curl [api_url not set]/v1/balance \
  -H "Authorization: Bearer sk_live_xxx"
json
{
  "mode": "live",
  "balances": [
    {
      "currency": "BDT",
      "available": 128500.00,
      "gross_approved": 152000.00,
      "refunded": 3500.00,
      "paid_out": 20000.00
    }
  ]
}

Test keys (sk_test_…) always return an empty balance — test mode does not move real money.

Hardening

Security best practices

A short checklist for production-grade integrations.

  • Never expose sk_live_ keys client-side — use environment variables.
  • Poll /v1/events from your server only; never from the browser.
  • Treat amounts as integers in major units to avoid floating-point errors.
  • Use HTTPS for success_url, cancel_url, failed_url.
  • Rotate API keys periodically; revoke unused ones.
  • Persist last_event_id transactionally so a crash mid-poll cannot lose events.

Ready to integrate?

Spin up a merchant workspace, generate a key pair, and start accepting payments in under 5 minutes.