Search pages in the SMS Pay documentation.
Reconciliation turns SMS evidence into payment status. The gateway stores every trusted SMS event, parses known provider formats, and matches it to the correct payment intent using deterministic key-based matching only.
There are exactly two valid matching paths:
Weak signals such as amount alone, receiver alone, sender alone, or time proximity never produce a match or trigger review.
When an SMS is ingested and parsed, the gateway checks whether the parsedReference in the SMS equals the customerReference of a pending, non-expired payment intent.
The lookup must satisfy all four conditions:
| Filter | Requirement |
|---|---|
| Reference | sms.parsedReference equals intent.customerReference (case-insensitive) |
| Environment | Same environment (SANDBOX or LIVE) |
| Payment method | Same payment method (e.g. BKASH_SEND_MONEY) |
| Receiver | Same receiverMsisdn |
Then:
| Amount check | Outcome |
|---|---|
| Amount matches exactly | Intent → PAID, webhook payment.paid fired |
| Amount differs | Intent → REVIEW_REQUIRED, webhook payment.review_required fired, reason: reference_match_amount_mismatch |
If no intent has a matching reference, the SMS stays PENDING/unmatched. It is not automatically attached to any other intent.
The same logic runs in reverse when a new intent is created: the gateway checks whether an already-ingested SMS is waiting with a matching reference.
If the customer paid but the reference was missing or incorrect, they can enter the bKash Transaction ID on the checkout page.
The lookup must satisfy all four conditions:
| Filter | Requirement |
|---|---|
| Transaction ID | sms.parsedTxnId equals the submitted trxId (case-insensitive) |
| Environment | Same environment as the intent |
| Payment method | Same payment method |
| Receiver | Same receiverMsisdn |
Then:
| Amount check | Outcome |
|---|---|
| Amount matches exactly | Intent → PAID, webhook payment.paid fired |
| Amount differs | Intent → REVIEW_REQUIRED, reason: trx_id_match_amount_mismatch |
REVIEW_REQUIRED because an old unmatched SMS has the same amountYour system should treat webhooks as the source of fulfillment:
async function handlePaymentPaid(payload: {
data: { payment_intent_id: string; customer_reference: string };
}) {
const order = await db.orders.findUnique({
where: { reference: payload.data.customer_reference },
});
if (!order || order.status === "paid") return;
await db.orders.update({
where: { id: order.id },
data: {
status: "paid",
gatewayPaymentIntentId: payload.data.payment_intent_id,
},
});
}
Do not fulfil orders from the checkout redirect URL. The redirect is a UX hint only. Always verify payment status via the webhook or a server-side GET on the payment intent.
Retrieve a payment intent after matching:
GET /v1/payments/intents/b5012f33-207e-4999-bf8d-5a1ebb10988e
X-Api-Key: sk_test_xxxxxxxxxxxxxxxxx
{
"id": "b5012f33-207e-4999-bf8d-5a1ebb10988e",
"environment": "SANDBOX",
"amount": "500",
"currency": "BDT",
"status": "PAID",
"customerReference": "ORDER-10045",
"receiverMsisdn": "01700000001",
"checkoutUrl": "https://smspaybd.com/checkout/b5012f33-207e-4999-bf8d-5a1ebb10988e",
"expiresAt": "2026-05-05T10:05:00.000Z"
}
Review-required webhook:
{
"event": "payment.review_required",
"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"
}
}
Merchant admins can:
REVIEW_REQUIRED intents)REVIEW_REQUIRED, not PAID.REVIEW_REQUIRED, not PAID.PENDING; customer must use trxId fallback.FAILED with reason duplicate_trx_id.Primary instant match requires:
Fallback manual confirmation uses transaction ID plus amount and receiver validation.
PAID.REVIEW_REQUIRED.The server marks the payment intent PAID when:
| Check | Requirement |
|---|---|
| Amount | SMS amount equals intent amount. |
| Receiver | SMS receiver wallet equals intent receiverMsisdn. |
| Reference | SMS reference equals intent customerReference. |
| Expiry | Intent is not expired. |
| Reuse | SMS event and transaction ID are not already linked to another intent. |
When a customer enters trxId on checkout, the server validates:
If valid, the payment becomes PAID.
Your system should treat webhooks as the source of fulfillment:
async function handlePaymentPaid(payload: {
data: { payment_intent_id: string; customer_reference: string };
}) {
const order = await db.orders.findUnique({
where: { reference: payload.data.customer_reference },
});
if (!order || order.status === "paid") return;
await db.orders.update({
where: { id: order.id },
data: {
status: "paid",
gatewayPaymentIntentId: payload.data.payment_intent_id,
},
});
}
Retrieve a payment intent after matching:
GET /v1/payments/intents/b5012f33-207e-4999-bf8d-5a1ebb10988e
X-Api-Key: sk_test_xxxxxxxxxxxxxxxxx
{
"id": "b5012f33-207e-4999-bf8d-5a1ebb10988e",
"environment": "SANDBOX",
"amount": "500",
"currency": "BDT",
"status": "PAID",
"customerReference": "ORDER-10045",
"receiverMsisdn": "01700000001",
"checkoutUrl": "https://smspaybd.com/checkout/b5012f33-207e-4999-bf8d-5a1ebb10988e",
"expiresAt": "2026-05-05T10:05:00.000Z"
}
Review-required webhook:
{
"event": "payment.review_required",
"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"
}
}
Merchant admins can: