Developer
API keys, webhooks, and integration tools.
Overview
SecurePay is a hosted-checkout payment layer. Your server creates a checkout session for an amount, you redirect the customer to SecurePay's hosted page to pay (bKash, Nagad, Rocket, cards via SSLCommerz, and more), and SecurePay notifies your server of the result with a signed webhook. You never handle raw card or wallet credentials.
The integration in three steps
- Create a session server-side with your secret key → you get a hosted
url. - Redirect the customer to that
url. They pay on SecurePay. - Receive the webhook (
payment.succeeded) and fulfil the order. Optionally poll the status endpoint as a fallback.
Base URL: https://api.pay.ccbd.dev — all API paths below are relative to this origin. Amounts are always in minor units (e.g. ৳6,000.00 = 600000). The default currency is BDT.
Authentication
Authenticate server-side requests with your secret key as a bearer token. Create and manage keys in the API keys tab.
Authorization: Bearer sk_live_your_secret_key| Field | Type | Description |
|---|---|---|
| sk_live_… | secret | Live secret key — full server-side access. Routes to real gateways. Never expose it in client-side code. |
| sk_test_… | secret | Test secret key — same API, but payments run against the sandbox simulator (no real money). See Test mode. |
| pk_live_… / pk_test_… | publishable | Publishable key — safe for client-side checkout initiation (scope: create checkout sessions only). |
The environment (test vs live) is determined by the key you use — there is no separate flag. A key's secret is shown only once at creation; store it securely.
Quickstart
A minimal end-to-end integration in Node.js:
// 1) Create a checkout session (server-side)
const res = await fetch("https://api.pay.ccbd.dev/v1/checkout-sessions", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SECUREPAY_SECRET_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": `order-${orderId}`, // safe retries
},
body: JSON.stringify({
amountMinor: 600000, // ৳6,000.00
currency: "BDT",
description: `Order #${orderId}`,
metadata: { orderId }, // echoed back in the webhook
successUrl: "https://yourapp.com/pay/return?order=" + orderId,
cancelUrl: "https://yourapp.com/pay/cancel?order=" + orderId,
}),
});
const { data } = await res.json();
// 2) Redirect the customer to the hosted checkout
redirect(data.url);
// 3) Fulfil the order when the signed webhook arrives (see Webhooks).Create a checkout session
Creates a session and returns a hosted checkout url. Send the Idempotency-Key header to make retries safe (a repeated key returns the same session instead of creating a duplicate).
Request body
| Field | Type | Description |
|---|---|---|
| amountMinor* | integer | Amount in minor units (× 100). Must be positive. |
| currency* | string | ISO currency code. Currently BDT. |
| description | string | Shown to the payer / on the receipt. |
| customer | object | Optional payer contact: { name, email, phone }. |
| successUrl | string | Where the payer returns after paying. Defaults to SecurePay’s hosted result page. |
| cancelUrl | string | Where the payer returns on cancel/failure. |
| metadata | object | Free-form string map echoed verbatim in webhooks — use it to correlate your order/invoice id. |
| invoiceId | string | Optional: link the session to your own invoice reference. |
Example request
curl -X POST https://api.pay.ccbd.dev/v1/checkout-sessions \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1001" \
-d '{
"amountMinor": 600000,
"currency": "BDT",
"description": "Order #1001",
"customer": { "name": "Rahim", "email": "rahim@example.com", "phone": "01700000000" },
"successUrl": "https://yourapp.com/pay/return?order=1001",
"cancelUrl": "https://yourapp.com/pay/cancel?order=1001",
"metadata": { "orderId": "1001" }
}'Response (201)
{
"data": {
"id": "a01dc849-9d27-4faa-a590-daa0bb96efad",
"status": "OPEN",
"amountMinor": 600000,
"currency": "BDT",
"url": "https://pay.ccbd.dev/checkout/a01dc849-9d27-4faa-a590-daa0bb96efad",
"eligibleMethods": [
{
"acceptanceMethodId": "a2dc7499-5c07-4914-87ab-22023039d559",
"mode": "GATEWAY",
"gatewayCode": "sslcommerz",
"displayLabel": "SSLCommerz",
"sortOrder": 1
}
],
"livemode": true,
"expiresAt": "2026-08-11T20:44:34.243Z"
}
}Redirect the customer
Send the customer to data.url (a full-page redirect). What they see depends on how many payment methods you have enabled:
- One gateway → they go straight to that gateway's page (the SecurePay picker is skipped).
- Two or more → SecurePay shows a method picker first, then routes to the chosen gateway.
After payment, the payer is returned to your successUrl / cancelUrl. Treat this return purely as UX — the authoritative result is the webhook (the customer may close the tab before returning).
Check payment status
A lightweight, unauthenticated poll of a session's outcome. Use it as a fallback or to drive a “waiting for payment” screen — webhooks remain the primary settlement signal.
curl https://api.pay.ccbd.dev/v1/public/checkout-sessions/a01dc849.../status
# { "data": { "status": "COMPLETED", "paymentStatus": "SUCCEEDED" } }| Field | Type | Description |
|---|---|---|
| status | string | Session state: OPEN, COMPLETED, or EXPIRED. |
| paymentStatus | string | Payment state: PENDING, SUCCEEDED, FAILED, or EXPIRED. |
Webhooks
Register an HTTPS endpoint in the Webhooks tab and subscribe to events. SecurePay POSTs a signed JSON payload; a signing secret is shown once when you create the endpoint.
Request headers
| Field | Type | Description |
|---|---|---|
| X-SecurePay-Event | string | The event type, e.g. payment.succeeded. |
| X-SecurePay-Signature | string | sha256=<hex> — HMAC-SHA256 of the raw request body, keyed by your endpoint’s signing secret. |
| X-SecurePay-Delivery | string | Unique delivery id (stable across retries of the same event). |
Payload envelope
{
"event": "payment.succeeded",
"merchantId": "888e49fd-aa4f-4f9a-9c39-a413ddbb7cb9",
"data": {
"paymentId": "cfdfb57f-388d-4a18-8f73-d61c88f9f843",
"checkoutSessionId": "a01dc849-9d27-4faa-a590-daa0bb96efad",
"amountMinor": 600000,
"currency": "BDT",
"metadata": { "orderId": "1001" }
}
}Event types
| Field | Type | Description |
|---|---|---|
| payment.succeeded | event | Payment completed. data: paymentId, checkoutSessionId, amountMinor, currency, metadata. |
| payment.failed | event | Payment failed/declined. Same data shape as payment.succeeded. |
| refund.succeeded | event | Refund completed. data: refundId, paymentId, amountMinor, status. |
| refund.failed | event | Refund failed. Same data shape as refund.succeeded. |
Verify the signature (Node.js / Express)
import express from "express";
import crypto from "crypto";
const app = express();
// IMPORTANT: verify against the RAW body bytes, not a re-serialized object.
app.post(
"/webhooks/securepay",
express.raw({ type: "application/json" }),
(req, res) => {
const signature = req.header("X-SecurePay-Signature") || "";
const expected =
"sha256=" +
crypto
.createHmac("sha256", process.env.SECUREPAY_WEBHOOK_SECRET)
.update(req.body) // Buffer of the raw body
.digest("hex");
// Constant-time comparison
const valid =
signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
if (!valid) return res.status(400).send("invalid signature");
const { event, data } = JSON.parse(req.body.toString());
if (event === "payment.succeeded") {
// Fulfil using your own reference from data.metadata (e.g. orderId).
fulfilOrder(data.metadata.orderId, data.paymentId);
}
// Respond 2xx to acknowledge. Non-2xx (or a timeout) triggers retries.
res.status(200).send("ok");
}
);2xx. Always verify the signature and treat handling as idempotent — the same event may arrive more than once (dedupe on data.paymentId or X-SecurePay-Delivery). You can inspect and re-send deliveries from the Webhooks tab.Test mode & magic amounts
Any payment made with a sk_test_ key runs against a deterministic sandbox simulator — no real gateway, no money moved. The outcome is chosen by the exact amountMinor. Any amount not in the table below simply succeeds.
| Field | Type | Description |
|---|---|---|
| 1001 | ৳10.01 | DECLINE → payment.failed |
| 1002 | ৳10.02 | TIMEOUT → stays PENDING, then EXPIRED |
| 1003 | ৳10.03 | LOST callback → still SUCCEEDED (resolved by polling) |
| 1004 | ৳10.04 | SLOW → PENDING for a few seconds, then SUCCEEDED |
| any other | — | Instant SUCCESS (happy path) |
With a sk_live_ key, the same calls route to the real gateway (e.g. SSLCommerz) where the customer completes payment with a card or wallet.
Errors
Errors use standard HTTP status codes with a machine-readable code and a human-readable message.
| Field | Type | Description |
|---|---|---|
| 401 | unauthorized | Invalid or revoked API key. |
| 400 | bad_request | Malformed body — e.g. amountMinor not a positive integer. |
| 422 | unsupported_currency | The currency is not enabled for this merchant. |
| 422 | method_unavailable | The selected method/gateway is not available for this session. |
| 409 | session_not_open | The session is already completed or expired. |
Endpoint reference
Secret-key endpoint (server-side) and the public payer endpoints used by the hosted checkout page.
The /public/ endpoints are unauthenticated and are driven by SecurePay's hosted checkout page — you normally only call POST /v1/checkout-sessions and consume webhooks.