Search pages in the SMS Pay documentation.
Copy the prompt below and paste it into any AI assistant (GitHub Copilot, ChatGPT, Claude, Gemini, etc.). The assistant will have the full context it needs to generate working integration code for your platform.
You are helping me integrate the SMS Pay API into my application.
## What the service does
SMS Pay is a hosted payment service for Bangladesh mobile wallets (bKash, Nagad).
The merchant's server creates a PaymentIntent, the customer is redirected to a hosted checkout
page, they send money via their mobile wallet app, and the gateway matches the incoming SMS
confirmation to the PaymentIntent. The merchant receives a webhook when payment is confirmed.
## Environments
There are two completely separate environments:
- SANDBOX — for development and testing. No real money. Use sandbox API keys and sandbox base URL.
- LIVE — production. Real money. Use live API keys and live base URL.
Never mix sandbox keys with live keys, or sandbox data with live data.
Base URLs (replace with your actual provider URLs):
API Base URL: https://api.smspaybd.com
Docs URL: https://smspaybd.com/docs
## Authentication
All merchant API calls require an API key in the request header:
X-Api-Key: sk_test_xxxxxxxxxxxxxxxxx (sandbox)
X-Api-Key: sk_live_xxxxxxxxxxxxxxxxx (live)
The hosted checkout page (/v1/checkout/:id) requires no authentication — it is public.
## Core workflow
1. Customer initiates checkout on your site.
2. Your server calls POST /v1/payments/intents to create a PaymentIntent.
3. Your server redirects the customer to the returned `checkoutUrl`.
4. The customer selects a payment method, sees payment instructions, sends money via their wallet.
5. The gateway matches the incoming SMS to the PaymentIntent automatically.
6. Your server receives a `payment.paid` webhook at your registered endpoint.
7. Fulfil the order based on the webhook — always verify the HMAC signature first.
## API endpoints
### Create a PaymentIntent
POST /v1/payments/intents
X-Api-Key: sk_test_xxxxxxxxxxxxxxxxx
Content-Type: application/json
Request body:
{
"amount": 500, // required — number, in BDT (no decimals)
"currency": "BDT", // required — always "BDT" currently
"customerReference": "ORDER-10045", // required — your order/reference ID
"idempotencyKey": "pi_ORDER-10045", // optional but recommended — prevents duplicates
"ttlSeconds": 300, // optional — seconds until expiry (default 300)
"successUrl": "https://smspaybd.com/payment/success", // optional — redirect after payment confirmed
"failedUrl": "https://smspaybd.com/payment/failed", // optional — redirect after failure/rejection
"cancelUrl": "https://smspaybd.com/checkout", // optional — redirect if customer cancels
"expiredUrl": "https://smspaybd.com/payment/expired" // optional — redirect after TTL exceeded
}
Success response (201):
{
"id": "b5012f33-207e-4999-bf8d-5a1ebb10988e",
"merchantId": "merchant_abc",
"environment": "SANDBOX",
"amount": "500",
"currency": "BDT",
"status": "PENDING",
"customerReference": "ORDER-10045",
"receiverMsisdn": "01700000001",
"checkoutUrl": "https://pay.example.com/checkout/b5012f33-207e-4999-bf8d-5a1ebb10988e",
"expiresAt": "2026-05-05T10:05:00.000Z",
"createdAt": "2026-05-05T10:00:00.000Z"
}
Redirect the customer to `checkoutUrl` immediately after creating the intent.
### Retrieve a PaymentIntent
GET /v1/payments/intents/:id
X-Api-Key: sk_test_xxxxxxxxxxxxxxxxx
Returns the full PaymentIntent object including current status.
### List PaymentIntents
GET /v1/payments/intents?status=PENDING&page=1&limit=20
X-Api-Key: sk_test_xxxxxxxxxxxxxxxxx
Returns: { data: PaymentIntent[], total: number, page: number, limit: number }
## PaymentIntent status values
PENDING — awaiting payment
PAID — payment confirmed (trigger fulfilment)
EXPIRED — TTL exceeded without payment
REJECTED — manually rejected by operator
FAILED — system-level failure
REVIEW_REQUIRED — ambiguous match, awaiting manual review
## Webhook events
Register a webhook endpoint in the dashboard (Settings → Webhooks) for each environment.
Provide a public HTTPS URL that accepts POST requests.
Events your endpoint will receive:
payment.paid — AUTO_MATCH: payment confirmed — fulfil the order
payment.expired — TTL exceeded
payment.rejected — manually rejected
payment.review_required — ambiguous match, awaiting review
Webhook payload shape:
{
"event": "payment.paid",
"environment": "SANDBOX",
"timestamp": "2026-05-05T10:01:00.000Z",
"data": {
"payment_intent_id": "b5012f33-207e-4999-bf8d-5a1ebb10988e",
"amount": "500",
"currency": "BDT",
"customer_reference": "ORDER-10045"
}
}
## Webhook HMAC signature verification (CRITICAL — always verify)
Every webhook request includes a header:
X-Signature: sha256=<hex_digest>
The digest is HMAC-SHA256 of the raw request body using your webhook secret.
You must verify this before trusting or acting on any webhook payload.
Example verification in Node.js:
const crypto = require("crypto");
function verifyWebhookSignature(rawBody, signatureHeader, secret) {
const expected = "sha256=" + crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signatureHeader)
);
}
// In your route handler (Express example):
app.post("/webhooks/payment", express.raw({ type: "application/json" }), (req, res) => {
const sig = req.headers["x-signature"];
if (!verifyWebhookSignature(req.body, sig, process.env.WEBHOOK_SECRET)) {
return res.status(401).send("Invalid signature");
}
const event = JSON.parse(req.body);
if (event.event === "payment.paid") {
// fulfil order: event.data.customer_reference
}
res.sendStatus(200);
});
Return HTTP 200 promptly. The gateway retries failed deliveries with exponential backoff (max 5 attempts).
Handle each event idempotently — you may receive the same event more than once.
## Sandbox testing
In sandbox, no real money is transferred. Use the simulator endpoint to fake an SMS event:
POST /v1/sandbox/sms-events/simulate
X-Api-Key: sk_test_xxxxxxxxxxxxxxxxx
Content-Type: application/json
{
"paymentIntentId": "b5012f33-207e-4999-bf8d-5a1ebb10988e",
"amount": 500,
"trxId": "DDN1G2WL8W",
"senderMsisdn": "01711111111"
}
After a successful simulate call the matching engine runs, the PaymentIntent transitions to PAID,
and your registered sandbox webhook endpoint receives payment.paid.
## Error responses
All errors follow this shape:
{
"statusCode": 400,
"message": "Human-readable description",
"error": "Bad Request"
}
Common status codes:
400 — validation error or business rule violation
401 — missing or invalid API key
404 — resource not found
409 — idempotency key conflict (duplicate request with different body)
422 — unprocessable entity
429 — rate limit exceeded
500 — internal server error
## Minimal integration checklist
[ ] Store API keys in environment variables — never in source code
[ ] Use sandbox key and sandbox base URL during development
[ ] Create PaymentIntent server-side — never from the browser
[ ] Redirect customer to checkoutUrl after creation
[ ] Register a webhook endpoint for sandbox and live separately
[ ] Always verify X-Signature before processing a webhook
[ ] Handle webhooks idempotently (check if order is already fulfilled)
[ ] Switch to live key and live base URL only after sandbox tests pass
## Utility wrapper (TypeScript / Node.js 18+)
async function smsGateway<T = unknown>(
path: string,
init: RequestInit = {},
): Promise<T> {
const res = await fetch(
`${process.env.SMS_PAY_BASE_URL}/v1${path}`,
{
...init,
headers: {
"Content-Type": "application/json",
"X-Api-Key": process.env.SMS_PAY_API_KEY!,
...(init.headers as Record<string, string>),
},
},
);
if (!res.ok) {
const err = await res.text();
throw new Error(`SMS Pay ${res.status}: ${err}`);
}
return res.json() as Promise<T>;
}
// Usage:
const intent = await smsGateway<{ id: string; checkoutUrl: string }>(
"/payments/intents",
{
method: "POST",
body: JSON.stringify({
amount: 500,
currency: "BDT",
customerReference: "ORDER-10045",
ttlSeconds: 300,
successUrl: "https://yoursite.com/payment/success",
}),
},
);
// Redirect customer to intent.checkoutUrl