Canonical base URL
https://paynoc.bd/api/public/v1
This deployment
https://paynoc.bd/api/public/v1
Auth header
Authorization: Bearer sk_live_…
Getting started
Quickstart
- Create a merchant account and generate an API key from the dashboard.
- Set PAYNOC_SECRET_KEY in your server env.
- POST to /invoices and redirect the buyer to data.checkout_url.
- Register a webhook URL and verify the HMAC signature on every event.
curl -X POST https://paynoc.bd/api/public/v1/invoices \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 8f14e45f-ceea-467a-9575-d0ab1af1b1e5" \
-d '{
"amount": 1500,
"currency": "BDT",
"customer_name": "Rakib Hasan",
"customer_email": "rakib@example.com",
"customer_phone": "+8801710000000",
"description": "Order #4021",
"redirect_url": "https://yourshop.com/thanks",
"webhook_url": "https://yourshop.com/hooks/paynoc",
"expires_in_hours": 24,
"metadata": { "order_id": "4021", "source": "checkout" }
}'Core
Authentication
Every request must include an Authorization header with your secret key. Test keys are prefixed sk_test_ and live keys with sk_live_. Never expose secret keys in browser code — treat them like passwords.
Authorization: Bearer sk_live_xxxRequests without a valid key return 401 Unauthorized. If an IP whitelist is configured, requests from other IPs return 403 Forbidden.
Environments
| Mode | Key prefix | Behaviour |
|---|---|---|
| test | sk_test_ | No real money moves. Gateway sandbox is used. |
| live | sk_live_ | Production. Real charges, real payouts. |
Errors
PayNOC uses conventional HTTP status codes. Every non-2xx response returns a JSON body with an error field describing what went wrong.
{ "error": "amount is required and must be > 0" }| Code | Meaning | Fix |
|---|---|---|
| 400 | Invalid JSON or missing field | Verify request body shape. |
| 401 | Missing / invalid API key | Send Authorization: Bearer sk_… |
| 403 | IP not whitelisted | Add caller IP in the dashboard. |
| 404 | Resource not found | Check the ID. |
| 409 | Idempotency key reused with different body | Use a new key. |
| 429 | Rate limit exceeded | Back off; 120 req/min per key. |
| 500 | Internal error | Retry with exponential backoff. |
Idempotency
Safely retry POST requests by sending an Idempotency-Key header. The first response is stored for 24 hours and replayed on subsequent requests with the same key and identical body. Reusing a key with a different body returns 409 Conflict.
Idempotency-Key: 8f14e45f-ceea-467a-9575-d0ab1af1b1e5Replayed responses include Idempotent-Replay: true.
Pagination
List endpoints support a limit query parameter (default 25, max 100). Results are ordered by created_at descending.
curl "https://paynoc.bd/api/public/v1/invoices?limit=50" -H "Authorization: Bearer sk_live_xxx"Rate limits
- 120 requests per minute per API key.
- Exceeding returns 429 Too Many Requests.
- All requests are audit-logged with IP and user agent.
Invoices
The Invoice object
{
"data": {
"id": "inv_7f2b4c8a...",
"invoice_number": "INV-20260708-A1B2C3",
"amount": 1500,
"currency": "BDT",
"status": "pending",
"mode": "live",
"customer_name": "Rakib Hasan",
"customer_email": "rakib@example.com",
"customer_phone": "+8801710000000",
"description": "Order #4021",
"redirect_url": "https://yourshop.com/thanks",
"webhook_url": "https://yourshop.com/hooks/paynoc",
"metadata": { "order_id": "4021" },
"expires_at": "2026-07-09T12:00:00.000Z",
"created_at": "2026-07-08T12:00:00.000Z",
"paid_at": null,
"checkout_url": "https://pay.paynoc.bd/inv_7f2b4c8a..."
}
}Unique invoice identifier.
Human-readable number shown on checkout, e.g. INV-20260708-A1B2C3.
Amount in major currency units (e.g. 1500 means 1,500 BDT). Must be greater than 0.
ISO-4217 currency code. Defaults to BDT.
pending · completed · failed · expired · refunded
test or live — matches the key that created the invoice.
Hosted checkout URL — redirect the customer here.
Any JSON object you attach; returned verbatim in webhooks.
ISO timestamp after which the invoice cannot be paid.
ISO timestamp of successful payment (null until paid).
Create an invoice
Amount to charge in major currency units (e.g. 1500 = 1,500 BDT). Must be > 0.
Currency code. Defaults to BDT.
Buyer name shown on checkout and receipts.
Buyer email — receives receipt.
Buyer phone — used by mobile-wallet gateways.
What the buyer is paying for.
Where to send the buyer after checkout.
Per-invoice webhook URL. Overrides the merchant default.
Invoice lifetime in hours. Default 24.
Arbitrary JSON returned unchanged in every webhook.
curl -X POST https://paynoc.bd/api/public/v1/invoices \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 8f14e45f-ceea-467a-9575-d0ab1af1b1e5" \
-d '{
"amount": 1500,
"currency": "BDT",
"customer_name": "Rakib Hasan",
"customer_email": "rakib@example.com",
"customer_phone": "+8801710000000",
"description": "Order #4021",
"redirect_url": "https://yourshop.com/thanks",
"webhook_url": "https://yourshop.com/hooks/paynoc",
"expires_in_hours": 24,
"metadata": { "order_id": "4021", "source": "checkout" }
}'{
"data": {
"id": "inv_7f2b4c8a...",
"invoice_number": "INV-20260708-A1B2C3",
"amount": 1500,
"currency": "BDT",
"status": "pending",
"mode": "live",
"customer_name": "Rakib Hasan",
"customer_email": "rakib@example.com",
"customer_phone": "+8801710000000",
"description": "Order #4021",
"redirect_url": "https://yourshop.com/thanks",
"webhook_url": "https://yourshop.com/hooks/paynoc",
"metadata": { "order_id": "4021" },
"expires_at": "2026-07-09T12:00:00.000Z",
"created_at": "2026-07-08T12:00:00.000Z",
"paid_at": null,
"checkout_url": "https://pay.paynoc.bd/inv_7f2b4c8a..."
}
}Retrieve an invoice
curl https://paynoc.bd/api/public/v1/invoices/{invoice_id} \
-H "Authorization: Bearer sk_live_xxx"{
"data": {
"id": "inv_7f2b4c8a...",
"invoice_number": "INV-20260708-A1B2C3",
"amount": 1500,
"currency": "BDT",
"status": "pending",
"mode": "live",
"customer_name": "Rakib Hasan",
"customer_email": "rakib@example.com",
"customer_phone": "+8801710000000",
"description": "Order #4021",
"redirect_url": "https://yourshop.com/thanks",
"webhook_url": "https://yourshop.com/hooks/paynoc",
"metadata": { "order_id": "4021" },
"expires_at": "2026-07-09T12:00:00.000Z",
"created_at": "2026-07-08T12:00:00.000Z",
"paid_at": null,
"checkout_url": "https://pay.paynoc.bd/inv_7f2b4c8a..."
}
}List invoices
curl "https://paynoc.bd/api/public/v1/invoices?limit=25" \
-H "Authorization: Bearer sk_live_xxx"{
"data": [
{ "id": "inv_…", "invoice_number": "INV-…", "amount": 1500, "status": "completed", "…": "…" },
{ "id": "inv_…", "invoice_number": "INV-…", "amount": 900, "status": "pending", "…": "…" }
]
}Hosted checkout
Every invoice has a checkout_url. Redirect the buyer there and PayNOC handles gateway selection (bKash, Nagad, Rocket, Uddoktapay, OWNpay, Piprapay, card), verification, receipts, and the return-to-merchant redirect.
res.redirect(302, invoice.checkout_url);After payment the buyer is sent to your redirect_url with ?invoice_id=…&status=completed. Never trust the redirect alone — confirm state via the webhook or a follow-up GET /invoices/{id}.
Balance summary
Returns an aggregated accounting summary per currency for the authenticated merchant: total collected (completed invoices), total refunded, and net. Because merchants receive funds directly to their own gateway account, PayNOC never custodies money — this endpoint is informational only, not a withdrawable wallet.
curl https://paynoc.bd/api/public/v1/balance \
-H "Authorization: Bearer sk_live_xxx"{
"data": {
"balances": [
{ "currency": "BDT", "collected": 125000, "refunded": 500, "net": 124500, "completed_invoice_count": 87 }
],
"note": "Funds settle directly to your connected gateway account. This is an informational summary only."
}
}Webhooks
Webhooks
PayNOC POSTs a JSON payload to your webhook URL for every lifecycle event. Each request is signed with HMAC-SHA256 so you can verify it originated from PayNOC and was not tampered with.
{
"id": "evt_9a2f7c4e...",
"event": "invoice.completed",
"created": 1783512345,
"mode": "live",
"data": {
"id": "inv_7f2b4c8a...",
"invoice_number": "INV-20260708-A1B2C3",
"amount": 1500,
"currency": "BDT",
"status": "completed",
"customer_email": "rakib@example.com",
"paid_at": "2026-07-08T12:03:11.000Z",
"metadata": { "order_id": "4021" },
"gateway": "bkash",
"gateway_txn_id": "TRX12345XYZ"
}
}Headers sent with every webhook
- x-paynoc-signature — t=<ts>,v1=<hex_hmac>
- x-paynoc-timestamp — unix seconds
- x-paynoc-event — event type
- x-paynoc-delivery — unique delivery ID
Verify the signature
Compute HMAC-SHA256 over <timestamp>.<raw_body> using your webhook secret, and compare it to the v1= part of the signature header with a timing-safe compare. Reject events older than 5 minutes to prevent replay attacks.
import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";
const app = express();
// IMPORTANT: preserve raw body to verify the signature
app.post(
"/hooks/paynoc",
express.raw({ type: "application/json" }),
(req, res) => {
const sig = req.header("x-paynoc-signature") ?? "";
const ts = req.header("x-paynoc-timestamp") ?? "";
const raw = req.body.toString("utf8");
const expected = createHmac("sha256", process.env.PAYNOC_WEBHOOK_SECRET)
.update(`${ts}.${raw}`).digest("hex");
const v1 = sig.split(",").find(p => p.startsWith("v1="))?.slice(3) ?? "";
if (
v1.length !== expected.length ||
!timingSafeEqual(Buffer.from(v1), Buffer.from(expected))
) return res.status(401).send("bad signature");
// Reject events older than 5 minutes (replay protection)
if (Math.abs(Date.now()/1000 - Number(ts)) > 300)
return res.status(400).send("stale");
const event = JSON.parse(raw);
switch (event.event) {
case "invoice.completed": /* fulfill order */ break;
case "invoice.failed": /* notify buyer */ break;
case "refund.processed": /* update ledger */ break;
}
res.send("ok");
}
);<?php
$raw = file_get_contents("php://input");
$sig = $_SERVER["HTTP_X_PAYNOC_SIGNATURE"] ?? "";
$ts = $_SERVER["HTTP_X_PAYNOC_TIMESTAMP"] ?? "";
$expected = hash_hmac("sha256", $ts . "." . $raw, getenv("PAYNOC_WEBHOOK_SECRET"));
$v1 = "";
foreach (explode(",", $sig) as $p) {
if (str_starts_with($p, "v1=")) $v1 = substr($p, 3);
}
if (!hash_equals($expected, $v1)) { http_response_code(401); exit("bad signature"); }
if (abs(time() - (int)$ts) > 300) { http_response_code(400); exit("stale"); }
$event = json_decode($raw, true);
// handle $event["event"] ...
echo "ok";Event types
| Event | When it fires |
|---|---|
| invoice.created | A new invoice was created via API. |
| invoice.completed | Payment verified by the underlying gateway. |
| invoice.failed | Buyer cancelled or gateway rejected the payment. |
| invoice.expired | Invoice passed its expires_at unpaid. |
| refund.requested | Merchant (dashboard or API) opened a refund request. |
| refund.approved | Admin approved the refund for processing. |
| refund.rejected | Admin rejected the refund request. |
| refund.processed | Refund completed at the gateway; funds returned to buyer. |
| invoice.refunded | Invoice has been fully refunded. |
| payout.processed | Merchant payout has been sent. |
Retries
Return HTTP 2xx within 10 seconds to acknowledge delivery. Any other response — or a timeout — is retried with exponential backoff for up to 24 hours: 30s, 2m, 10m, 30m, 1h, 2h, 6h, 12h. Handle events idempotently using the event id.
Advanced
Refunds
Request a full or partial refund on a completed invoice. Refunds start in requested state and move through approved → processed once reviewed. Every state change fires a webhook.
ID of the completed invoice to refund.
Amount to refund. Omit to refund the full invoice. Partial refunds are supported; total refunded across all requests can't exceed the invoice amount.
Short human-readable reason (surfaced in the admin dashboard).
curl -X POST https://paynoc.bd/api/public/v1/refunds \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"invoice_id": "inv_7f2b4c8a...",
"amount": 500,
"reason": "Customer request"
}'await fetch("https://paynoc.bd/api/public/v1/refunds", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PAYNOC_SECRET_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
invoice_id: "inv_7f2b4c8a...",
amount: 500, // omit for full refund
reason: "Customer request",
}),
});{
"data": {
"id": "rf_1a2b3c4d...",
"invoice_id": "inv_7f2b4c8a...",
"amount": 500,
"currency": "BDT",
"status": "requested",
"reason": "Customer request",
"created_at": "2026-07-09T09:11:00.000Z"
}
}List refunds for the authenticated merchant. Optional ?invoice_id= filter, ?limit= up to 100. Refunds are strictly scoped to the merchant that owns the API key — a key can never touch or refund another merchant's invoice.
curl "https://paynoc.bd/api/public/v1/refunds?invoice_id=inv_7f2b4c8a..." \
-H "Authorization: Bearer sk_live_xxx"Security note
API keys can only request refunds — they never mark an invoice paid, complete a transaction, or bypass gateway verification. Payment state changes only when the underlying gateway (bKash, Nagad, Rocket, SSLCOMMERZ, Uddoktapay, OWNpay, Piprapay, card, BYO…) confirms the money moved, or a platform admin manually verifies a transaction from the dashboard. Approving and processing a refund still requires a signed-in admin.
Testing
- Use a sk_test_ key — every invoice is created in test mode.
- Gateway sandboxes accept any well-formed OTP for a completed payment.
- Trigger a test webhook from the dashboard to any URL, including https://webhook.site.
- Use an Idempotency-Key to safely re-run scripts.
SDKs & Postman
Official SDKs (Node.js, PHP, Python) are coming soon. In the meantime, any HTTP client works — the API is standard REST + JSON.
⬇ Download Postman collectionIntegrations
Plugins & platform integrations
Drop-in modules for the platforms you already run. Every listing here is published live by the PayNOC team — download the archive, install it in your platform, paste your API key, and you're accepting payments in minutes.
Loading plugins…
Merchant setup
Connect your site or app
PayNOC accepts payments in two ways: PayNOC-hosted checkout (create an invoice → redirect to checkout_url) or a Bring-Your-Own gateway where PayNOC talks to your own bKash / Nagad / SSLCommerz / Uddoktapay / PipraPay account. Either way, you paste credentials once in the dashboard — never in your code.
Where credentials go
- Merchant dashboard → Integrations → Auto gateways (API).
- Pick the provider (bKash, Nagad, SSLCommerz, UddoktaPay, PipraPay, OwnPay, Stripe…).
- Open the Where do I get these credentials? guide inside the form.
- Paste values, save in Sandbox mode, verify one test payment, then flip to Live.
| Provider | What PayNOC needs | Where to find it |
|---|---|---|
| bKash PGW | app_key · app_secret · username · password | Emailed by bKash after PGW merchant approval (developer.bka.sh). |
| Nagad | merchant_id · merchant_number · nagad_public_key · your_private_key | Nagad issues the merchant IDs + their public key; you generate the RSA keypair and share only the public half with Nagad. |
| SSLCommerz | store_id · store_password | SSLCommerz Merchant Panel → Integration → API/IPN. Sandbox creds available immediately at developer.sslcommerz.com. |
| UddoktaPay (self-hosted) | base_url · api_key | Your UddoktaPay admin → API Settings. Register PayNOC's webhook URL under Webhook Settings. |
| PipraPay | base_url · api_key | PipraPay dashboard → Developers. Sandbox base URL https://sandbox.piprapay.com. |
| OwnPay (self-hosted) | base_url · api_key · webhook_secret | OwnPay admin → Settings → API. Verify the X-Signature HMAC on every event. |
| Stripe | secret_key · publishable_key · webhook_secret | Stripe Dashboard → Developers → API keys + Webhooks (paste PayNOC's webhook URL, copy the whsec_… secret). |
| Rocket / Upay / Tap / MCash / MyCash | merchant_number | These MFS have no public API — merchant number only. PayNOC uses the APK SMS ingestion path (below) to auto-verify. |
Two ways to accept payments in your own app
- Server-side integration (recommended). Create an invoice from your backend with the PayNOC REST API and redirect the buyer to data.checkout_url. PayNOC handles gateway selection, SCA, and the return redirect. See Quickstart.
- Embed the checkout button. Drop <script src="https://paynoc.bd/embed.js"> on any page — no backend required for simple flows.
Connect the Android APK
For mobile-wallet channels that don't expose an API (Rocket, Upay, Tap, MCash, personal bKash / Nagad numbers), install the PayNOC Merchant APK on the phone that owns the merchant number. The APK reads the provider's SMS confirmation, extracts the amount + TrxID, and POSTs it to /api/public/v1/sms-events. PayNOC auto-verifies any pending transaction with a matching TrxID.
- In your dashboard, open Security → Devices (APK) and click Create device key — this mints a dedicated
sk_live_…key labelled after the phone, so you can revoke that device without affecting other integrations. - Install the APK on the merchant phone. Open it and tap Scan to connect, then scan the QR shown on the Devices page — it encodes
{ backend_url, api_key, env }so both fields are set in one step. - Grant SMS read permission when Android asks. The APK forwards only sender / amount / TrxID over HTTPS.
- Test one payment end-to-end, then leave the phone running (whitelist the APK from battery optimisation).
Event payload (APK → PayNOC)
{
"events": [{
"provider": "bkash",
"raw_body": "You have received Tk 1500.00 from 01710000000. TrxID TRX12345XYZ …",
"trx_id": "TRX12345XYZ",
"amount": 1500,
"sender": "01710000000",
"received_at": "2026-07-08T12:03:00.000Z",
"device_id": "a1b2c3d4-…"
}]
}Response: { "ok": true, "results": [{ "trx_id": "…", "matched": true, "invoice_id": "…" }] }. Batched up to 50 events. Offline SMS queue and retry.