Get started
Quickstart
From sign-up to first verified payment in three steps.
[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.
- 01
Create workspace
Sign up and create your merchant workspace. Add your website and verify ownership.
- 02
Generate API keys
From the Developers tab, generate a publishable + secret key pair. Save the secret once.
- 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.
- 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 DBMYPAY_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
- ❌ 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
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.
pk_live_…Safe to embed in client-side code. Used by the embed widget to identify your merchant.
sk_live_…Server-side only. Never expose to browsers. Rotate immediately if leaked.
POST /v1/sessions HTTP/1.1
Host: [api_url not set]
Authorization: Bearer sk_live_xxx
Content-Type: application/jsonREST API
Create a payment session
POST [api_url not set]/v1/sessions — returns a hosted checkout_url to redirect your customer to.
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
| Field | Type | Description |
|---|---|---|
| amount | number | Required. Amount in major units (e.g. 4250 = ৳4,250). |
| currency | string | Defaults to BDT. |
| reference_id | string | Your internal order/invoice id. Returned on every event in the /v1/events feed. |
| customer_email | string | Optional. Pre-fills checkout. |
| success_url | url | Redirect after successful payment. |
| cancel_url | url | Redirect when the customer clicks Cancel. |
| failed_url | url | Redirect if the payment is rejected/failed. |
| callback_url | url | Optional callback URL (currently unused — use polling instead). |
| metadata | object | Up 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 [api_url not set]/v1/sessions/sess_abc123 \
-H "Authorization: Bearer sk_live_xxx"{
"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.
<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.
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.
- 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
curl [api_url not set]/v1/events?since=0&limit=100 \
-H "Authorization: Bearer sk_live_xxx"{
"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.createdCustomer opened a new checkout.
session.submittedCustomer submitted TXID/proof — pending review.
session.approvedPayment confirmed. Fulfil the order.
session.rejectedProof rejected. Do NOT fulfil.
session.expiredSession timed out.
session.cancelledCustomer cancelled.
session.refundedSession fully refunded.
transaction.approvedTransaction approved.
transaction.rejectedTransaction rejected.
refund.requestedRefund created.
refund.approvedRefund approved by admin.
refund.processedRefund money sent.
refund.declinedRefund 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.
// 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);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
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}
curl [api_url not set]/v1/refunds/abc123 \
-H "Authorization: Bearer sk_live_xxx"GET /v1/refunds?status=&since=&limit=&cursor=
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}
curl [api_url not set]/v1/transactions/abc \
-H "Authorization: Bearer sk_live_xxx"GET /v1/transactions?session_id=&status=&since=
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
curl [api_url not set]/v1/balance \
-H "Authorization: Bearer sk_live_xxx"{
"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.