Superchat BD
Developer Docs
v1.0
Official Library

TypeScript / Node.js SDK

@superchatbd/sdk is the official client for the Superchat BD Developer Platform. It covers the full /api/v1 surface — hosted checkout payments, refunds, account data, webhook subscriptions and webhook signature verification — with typed requests and responses, ESM and CommonJS builds, and zero runtime dependencies.

Node.js 18+, Bun or DenoTyped ESM + CJSTiming-safe HMAC verificationAutomatic retries & idempotency keys

Installation

The SDK verifies webhook signatures with node:crypto, so it runs on Node.js 18 or newer (global fetch), Bun, and Deno with Node compatibility.

# npm
npm install @superchatbd/sdk

# bun
bun add @superchatbd/sdk

# pnpm
pnpm add @superchatbd/sdk

Quickstart

import { Superchat, constructWebhookEvent } from "@superchatbd/sdk";

const superchat = new Superchat({
  apiKey: process.env.SUPERCHAT_API_KEY!, // sk_live_... or sk_test_...
});

// 1. Create a hosted checkout session and redirect the customer to checkoutUrl
const payment = await superchat.payments.create({
  amount: 750,
  currency: "BDT",
  success_url: "https://myshop.com/orders/success",
  cancel_url: "https://myshop.com/cart",
  customer: { name: "Karim Rahman", email: "[email protected]" },
  metadata: { orderId: "order_9921" },
});

console.log(payment.id, payment.checkoutUrl, payment.netAmount);

// 2. Verify every incoming webhook before trusting it
const event = constructWebhookEvent({
  payload: rawBody, // raw request body, exactly as received
  signatureHeader: request.headers.get("x-superchat-signature")!,
  secret: process.env.SUPERCHAT_WEBHOOK_SECRET!, // whsec_...
});

if (event.event === "payment.completed") {
  await fulfilOrder(event.data.metadata.orderId);
}

Create a payment in one call

amount, success_url and cancel_url are required. The response contains the checkoutUrl to send the customer to, plus the fee breakdown (platformFeeAmount, feePercentage, netAmount).

Configuration

OptionTypeDefaultDescription
apiKeystringSUPERCHAT_API_KEYSecret key (sk_live_… or sk_test_…). Missing or empty keys throw SuperchatConfigError.
baseUrlstringSUPERCHAT_BASE_URL or https://api.superchatbd.comAPI origin. Trailing slashes are stripped; must be an absolute http(s) URL.
timeoutMsnumber30000Per-request timeout in milliseconds.
maxRetriesnumber2Retry budget for retryable requests. 0 disables retries.
environment'live' | 'test'derived from the key prefixAsserted against the key: passing 'test' with an sk_live_… key throws at construction.
fetchtypeof fetchglobalThis.fetchCustom fetch implementation (edge runtimes, proxies, tests).

The resolved values are exposed as superchat.baseUrl and superchat.environment (undefined when the key prefix is unrecognised).

superchat.payments

MethodEndpointReturns
create(params, options?)POST /api/v1/paymentsPayment
retrieve(paymentId)GET /api/v1/payments/:idPayment
list({ page?, limit?, status? })GET /api/v1/paymentsPaginated<Payment>
refund(paymentId, { reason? }, options?)POST /api/v1/payments/:id/refundPayment
create() parameters

amount — number, required, greater than zero.

success_url, cancel_url — absolute URLs, required.

currency — "BDT" or "USD", defaults to BDT.

description, metadata — optional.

customer — { name?, email?, phone? }.

Payment fields

id, amount, currency, checkoutUrl, merchantTransactionId, customer, metadata, netAmount, platformFeeAmount, feePercentage, refund.

status is the public status — pending, completed, failed, cancelled, refunded, expired — while the internal status is kept in rawStatus.

Legacy snake_case aliases (checkout_url, platform_fee, net_amount) are still returned by the API and remain typed.

Only payments in the internal paid status can be refunded. A refund returns a typed PaymentRefund object on payment.refund containing requiresManualSettlement: true for BDT / EPS transactions, where Bangladeshi MFS gateways require manual settlement to the customer. Test payments cannot be refunded. The status filter on list() is matched against the public status: completed also matches internal paid rows, and pending also matches created and processing.

superchat.account

MethodEndpointReturns
retrieve()GET /api/v1/account{ workspace, balance, volume }
balance()GET /api/v1/account/balance{ bdt_balance, usd_balance }
apps()GET /api/v1/account/appsDeveloperApp[]

retrieve() returns the workspace profile (id, name, slug, website, status), the available balance per currency, and lifetime volume as { bdt, usd, total_payments }. Applications expose redirectUris; the platform currently mirrors webhookUrl into websiteUrl.

superchat.webhooks

MethodEndpointReturns
create({ url, events, appId? })POST /api/v1/webhooksWebhookEndpoint (includes secret)
list()GET /api/v1/webhooksWebhookEndpoint[]
deliveries({ webhookId?, page?, limit? })GET /api/v1/webhooks/deliveriesPaginated<WebhookDelivery>
remove(webhookId)DELETE /api/v1/webhooks/:id{ success, message }
test(webhookId)POST /api/v1/webhooks/:id/testWebhookDelivery | null

events accepts event names such as payment.completed or * for everything. The signing secret (whsec_…) is returned by create() and by list(). test() delivers a test.ping event and returns the logged delivery, or null when the platform could not record it. Deliveries expose both the raw fields (responseStatus, attempts) and their aliases (statusCode, attemptNumber).

Signature helpers live on the same resource: webhooks.verifySignature(options) returns a boolean, and webhooks.constructEvent(options) returns the parsed envelope or throws. Both are also exported standalone — see Webhook signatures.

superchat.checkout

MethodEndpointReturns
retrieve(sessionId)GET /api/v1/checkout/:idCheckoutSession
simulate(sessionId, { status? | action? })POST /api/v1/checkout/:id/simulateCheckoutSimulationResult
pay(sessionId, { paymentMethod?, phoneNumber?, redirectUrl? })POST /api/v1/checkout/:id/payCheckoutPayResult
createDemoSession({ amount?, currency?, … })POST /api/v1/checkout/demo-sessionDemoCheckoutSession

Two shapes from pay()

These routes power the hosted checkout page and need no API key. For live sessions pay() resolves to { redirectUrl }. Test sessions are short-circuited into the simulator and resolve to { status, redirectUrl, success } instead — check for the success property to tell them apart. simulate() only works on test sessions.

Errors

ClasscodeThrown when
SuperchatConfigErrorinvalid_configOptions are missing or invalid (no API key, bad baseUrl, bad timeout/retries, environment mismatch).
SuperchatAPIErrorapi_errorThe API answered with a non-2xx status. Carries statusCode, body and requestId.
SuperchatConnectionErrornetwork_error, timeout, abortedThe request never reached the API.
SuperchatSignatureErrormalformed_header, timestamp_outside_tolerance, signature_mismatch, missing_secretWebhook verification failed.
SuperchatErrorinvalid_response, invalid_payloadBase class of all of the above; also thrown for a non-JSON success body and for a verified webhook body that is not an event envelope.
import { SuperchatAPIError, SuperchatConnectionError, SuperchatError } from "@superchatbd/sdk";

try {
  await superchat.payments.create({
    amount: 750,
    success_url: "https://myshop.com/orders/success",
    cancel_url: "https://myshop.com/cart",
  });
} catch (error) {
  if (error instanceof SuperchatAPIError) {
    console.error(error.statusCode, error.message, error.body); // 400, "…", { statusCode, message, error }
  } else if (error instanceof SuperchatConnectionError) {
    console.error(error.code); // "network_error" | "timeout" | "aborted"
  } else if (error instanceof SuperchatError) {
    console.error(error.code, error.message);
  }
}

The API returns validation failures as { statusCode, message: string[] | string, error }; the SDK joins array messages into a single readable string and keeps the raw payload on error.body.

Webhook signatures

Every delivery is signed with HMAC-SHA256 over `${timestamp}.${rawBody}` using the subscription secret. Verify the raw body before parsing it — re-serialising JSON changes the signed bytes.

HeaderDescription
X-Superchat-Signaturet=<unix-seconds>,v1=<hex>
X-Superchat-EventEvent name, for example payment.completed.
X-Superchat-DeliveryDelivery ID for support and log correlation.

The body is an envelope: { id: "evt_…", event, created_at, data }.

import { constructWebhookEvent, SuperchatSignatureError } from "@superchatbd/sdk";

try {
  const event = constructWebhookEvent<{ id: string; metadata: Record<string, string> }>({
    payload: rawBody,
    signatureHeader: req.headers["x-superchat-signature"] as string,
    secret: process.env.SUPERCHAT_WEBHOOK_SECRET!,
  });

  // event: { id: "evt_...", event: "payment.completed", created_at: "...", data: {...} }
  if (event.event === "payment.completed") {
    await fulfilOrder(event.data.metadata.orderId);
  }
} catch (error) {
  if (error instanceof SuperchatSignatureError) {
    // error.code: "malformed_header" | "timestamp_outside_tolerance" | "signature_mismatch"
    return new Response("invalid signature", { status: 400 });
  }
  throw error;
}
import { verifyWebhookSignature } from "@superchatbd/sdk";

// Safe for middleware: never throws on untrusted input.
const isValid = verifyWebhookSignature({
  payload: rawBody, // string | Buffer | Uint8Array
  signatureHeader: req.headers["x-superchat-signature"] as string,
  secret: process.env.SUPERCHAT_WEBHOOK_SECRET!,
  toleranceInSeconds: 300, // default; 0 disables the replay check
});

if (!isValid) {
  return new Response("invalid signature", { status: 400 });
}
verifySignature() vs constructEvent()

verifyWebhookSignature() returns false for missing, malformed, expired or mismatched signatures, and throws only when the secret is empty (a configuration error). Ideal for middleware.

constructWebhookEvent() throws a SuperchatSignatureError with the matching code, and returns the parsed envelope otherwise.

Replay protection

Signatures older than toleranceInSeconds (default 300) are rejected in both directions. Pass 0 to disable the age check.

computeSignature(payload, secret, timestamp) is exported so you can reproduce the header in your own tests. Comparison is constant-time.

Payload shapes the SDK does not rewrite

For payment.created, payment.completed and payment.refunded, data is the payment object — payment.refunded adds refund_reason. Two cases differ: test.ping sends data: { event, timestamp, developer_id, message } (typed as TestPingPayload), and sandbox sessions created from the dashboard simulator send payment.created with data wrapped as { event, payment }. Type the generic accordingly: constructWebhookEvent<T>(…).

Idempotency & retries

// The SDK generates an Idempotency-Key per call, so a retried request
// can never create a second charge.
await superchat.payments.create(paymentParams);

// Pass your own key to deduplicate across processes:
await superchat.payments.create(paymentParams, {
  idempotencyKey: `order_${order.id}`,
});

// Refunds accept a key too — without one they are never retried.
await superchat.payments.refund(paymentId, { reason: "Customer request" }, {
  idempotencyKey: `refund_${paymentId}`,
});
RequestRetried?
GET requestsYes — up to maxRetries.
POST with an idempotency keyYes — the key is resent, so the API replays the first response instead of charging twice.
POST without an idempotency keyNo — the SDK never replays a request it cannot deduplicate.
Client errors (4xx)No — except 408 and 429.
429 and 5xx, network errors, timeoutsYes, with exponential backoff and jitter, honouring Retry-After.

The API caches idempotent responses for 24 hours per developer and replays them with the Idempotent-Replayed: true header. A replay is not a new payment: payments.create() therefore generates a fresh key per call and only reuses it across its own internal retries.

Rate limits

Each API key can make 120 requests per minute. Keyless routes — the public signature verifier — are limited per IP address instead. Every response reports the current budget:

HeaderMeaning
X-RateLimit-LimitRequests allowed per minute for this key.
X-RateLimit-RemainingRequests left in the current window.
Retry-AfterSeconds to wait, sent with a 429 response.

Exceeding the limit returns 429 Too Many Requests. The SDK retries those automatically with exponential backoff and honours Retry-After, so a burst resolves itself — but a sustained 429 means it is time to slow down or batch your calls.

Test mode

Keys prefixed with sk_test_ run against the sandbox: the payment lifecycle is identical, nothing is charged, and checkout.simulate() can settle a session instantly as completed, failed or cancelled. Payments created with a test key carry isSimulated: true and environment: "test".

The API also accepts the public demo key sk_test_demo, so you can try endpoints from the Swagger UI before creating your own keys. In live mode, redirect URLs must use HTTPS (localhost is allowed for local development).