Skip to main content
ShadhinPay Docs
Client libraries

Node.js / TypeScript

Integrate ShadhinPay from a Node.js backend today, and track the official SDK.

Official SDK — Planned

A first-party @shadhinpay/node package is on the roadmap. Until it ships, use the built-in fetch and node:crypto as shown here — it's a few lines.

Create a payment

Node 18+ has fetch built in. Keep your API key on the server, never in the browser.

const BASE = 'https://api.shadhinpay.pay/api/v1'

async function createPayment() {
  const res = await fetch(`${BASE}/payments`, {
    method: 'POST',
    headers: {
      'Client-Id': process.env.SHADHINPAY_CLIENT_ID!,
      'Business-Id': process.env.SHADHINPAY_BUSINESS_ID!,
      'X-Api-Key': process.env.SHADHINPAY_API_KEY!,
      'X-Idempotency-Key': crypto.randomUUID(),
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      amount: '500.00',
      currency: 'BDT',
      merchantTxnId: 'order-7831',
      callbackUrl: 'https://shop.example.com/return',
    }),
  })

  const body = await res.json()
  if (body.status !== 'success') throw new Error(body.errorType) // see /docs/developers/errors
  return body.data // { paymentId, paymentUrl, status, ... }
}

Redirect the customer to data.paymentUrl. See Payments.

Verify a webhook (Express)

Capture the raw body so the signature matches the signed bytes:

import express from 'express'
import crypto from 'node:crypto'

const app = express()

app.post(
  '/webhooks/shadhinpay',
  express.raw({ type: 'application/json' }), // raw bytes, not parsed JSON
  (req, res) => {
    const raw = req.body.toString('utf8')
    const header = req.header('x-shadhinpay-signature') ?? ''
    if (!verify(raw, header, process.env.SHADHINPAY_WEBHOOK_SECRET!)) {
      return res.status(400).end()
    }
    const event = JSON.parse(raw)
    // dedupe on event.eventId, then handle event.eventType
    res.status(200).json({ status: 'received' })
  },
)

function verify(raw: string, header: string, secret: string): boolean {
  const p = Object.fromEntries(header.split(',').map((kv) => kv.split('=')))
  const t = Number(p.t)
  if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > 300) return false
  const expected = crypto.createHmac('sha256', secret).update(`t=${t}.${raw}`).digest('hex')
  const a = Buffer.from(expected)
  const b = Buffer.from(p.v1 ?? '')
  return a.length === b.length && crypto.timingSafeEqual(a, b)
}

The full signing scheme, retries, and idempotency rules are in Webhooks.

Next steps

On this page