Developer Reference · v1

The PayNOC API

REST over HTTPS. JSON in, JSON out. Idempotent writes, HMAC-signed webhooks, predictable errors. Ship a production integration this afternoon.

REST · JSON
v1 Stable
HMAC Webhooks
Test + Live

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

  1. Create a merchant account and generate an API key from the dashboard.
  2. Set PAYNOC_SECRET_KEY in your server env.
  3. POST to /invoices and redirect the buyer to data.checkout_url.
  4. Register a webhook URL and verify the HMAC signature on every event.
cURL
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.

Header
Authorization: Bearer sk_live_xxx

Requests without a valid key return 401 Unauthorized. If an IP whitelist is configured, requests from other IPs return 403 Forbidden.

Environments

ModeKey prefixBehaviour
testsk_test_No real money moves. Gateway sandbox is used.
livesk_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 response
{ "error": "amount is required and must be > 0" }
CodeMeaningFix
400Invalid JSON or missing fieldVerify request body shape.
401Missing / invalid API keySend Authorization: Bearer sk_…
403IP not whitelistedAdd caller IP in the dashboard.
404Resource not foundCheck the ID.
409Idempotency key reused with different bodyUse a new key.
429Rate limit exceededBack off; 120 req/min per key.
500Internal errorRetry 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.

Header
Idempotency-Key: 8f14e45f-ceea-467a-9575-d0ab1af1b1e5

Replayed 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
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

Invoice
{
  "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..."
  }
}
idstring

Unique invoice identifier.

invoice_numberstring

Human-readable number shown on checkout, e.g. INV-20260708-A1B2C3.

amountnumber

Amount in major currency units (e.g. 1500 means 1,500 BDT). Must be greater than 0.

currencystring

ISO-4217 currency code. Defaults to BDT.

statusenum

pending · completed · failed · expired · refunded

modeenum

test or live — matches the key that created the invoice.

checkout_urlurl

Hosted checkout URL — redirect the customer here.

metadataobject

Any JSON object you attach; returned verbatim in webhooks.

expires_atdatetime

ISO timestamp after which the invoice cannot be paid.

paid_atdatetime

ISO timestamp of successful payment (null until paid).

Create an invoice

POST/v1/invoices
amountnumberrequired

Amount to charge in major currency units (e.g. 1500 = 1,500 BDT). Must be > 0.

currencystring

Currency code. Defaults to BDT.

customer_namestring

Buyer name shown on checkout and receipts.

customer_emailstring

Buyer email — receives receipt.

customer_phonestring

Buyer phone — used by mobile-wallet gateways.

descriptionstring

What the buyer is paying for.

redirect_urlurl

Where to send the buyer after checkout.

webhook_urlurl

Per-invoice webhook URL. Overrides the merchant default.

expires_in_hoursinteger

Invoice lifetime in hours. Default 24.

metadataobject

Arbitrary JSON returned unchanged in every webhook.

cURL
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" }
  }'
201 Created
{
  "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

GET/v1/invoices/{id}
cURL
curl https://paynoc.bd/api/public/v1/invoices/{invoice_id} \
  -H "Authorization: Bearer sk_live_xxx"
200 OK
{
  "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

GET/v1/invoices
cURL
curl "https://paynoc.bd/api/public/v1/invoices?limit=25" \
  -H "Authorization: Bearer sk_live_xxx"
200 OK
{
  "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.

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

GET/v1/balance

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
curl https://paynoc.bd/api/public/v1/balance \
  -H "Authorization: Bearer sk_live_xxx"
200 OK
{
  "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.

Sample event
{
  "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-signaturet=<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.

Node.js / Express
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
<?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

EventWhen it fires
invoice.createdA new invoice was created via API.
invoice.completedPayment verified by the underlying gateway.
invoice.failedBuyer cancelled or gateway rejected the payment.
invoice.expiredInvoice passed its expires_at unpaid.
refund.requestedMerchant (dashboard or API) opened a refund request.
refund.approvedAdmin approved the refund for processing.
refund.rejectedAdmin rejected the refund request.
refund.processedRefund completed at the gateway; funds returned to buyer.
invoice.refundedInvoice has been fully refunded.
payout.processedMerchant 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 approvedprocessed once reviewed. Every state change fires a webhook.

POST/v1/refunds
invoice_idstringrequired

ID of the completed invoice to refund.

amountnumber

Amount to refund. Omit to refund the full invoice. Partial refunds are supported; total refunded across all requests can't exceed the invoice amount.

reasonstring

Short human-readable reason (surfaced in the admin dashboard).

cURL
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"
  }'
JavaScript
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",
  }),
});
201 Created
{
  "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"
  }
}
GET/v1/refunds

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
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 collection

Integrations

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

  1. Merchant dashboard → Integrations → Auto gateways (API).
  2. Pick the provider (bKash, Nagad, SSLCommerz, UddoktaPay, PipraPay, OwnPay, Stripe…).
  3. Open the Where do I get these credentials? guide inside the form.
  4. Paste values, save in Sandbox mode, verify one test payment, then flip to Live.
ProviderWhat PayNOC needsWhere to find it
bKash PGWapp_key · app_secret · username · passwordEmailed by bKash after PGW merchant approval (developer.bka.sh).
Nagadmerchant_id · merchant_number · nagad_public_key · your_private_keyNagad issues the merchant IDs + their public key; you generate the RSA keypair and share only the public half with Nagad.
SSLCommerzstore_id · store_passwordSSLCommerz Merchant Panel → Integration → API/IPN. Sandbox creds available immediately at developer.sslcommerz.com.
UddoktaPay (self-hosted)base_url · api_keyYour UddoktaPay admin → API Settings. Register PayNOC's webhook URL under Webhook Settings.
PipraPaybase_url · api_keyPipraPay dashboard → Developers. Sandbox base URL https://sandbox.piprapay.com.
OwnPay (self-hosted)base_url · api_key · webhook_secretOwnPay admin → Settings → API. Verify the X-Signature HMAC on every event.
Stripesecret_key · publishable_key · webhook_secretStripe Dashboard → Developers → API keys + Webhooks (paste PayNOC's webhook URL, copy the whsec_… secret).
Rocket / Upay / Tap / MCash / MyCashmerchant_numberThese 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.

  1. 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.
  2. 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.
  3. Grant SMS read permission when Android asks. The APK forwards only sender / amount / TrxID over HTTPS.
  4. Test one payment end-to-end, then leave the phone running (whitelist the APK from battery optimisation).

Event payload (APK → PayNOC)

POST /v1/sms-events
{
  "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.

Need help? Email developers@paynoc.bd or open a support ticket from your merchant dashboard — we typically reply within one business day.