Get startedOverview
SPV Payment API — v1

Automatic payment verification for bKash, Nagad & Rocket

A merchant configures payout wallets in the Android app, creates payment intents from their own server, and the SPV backend confirms payments after matching the customer's SMS confirmation.

https://spv-payment-api.pages.dev/api/v1

How a payment flows

1
Merchant creates a payment intentServer call with amount and order reference, using the merchant's own API key.
2
Customer opens the checkout linkSees only the merchant's enabled payout wallets and submits a Transaction ID.
3
SMS confirmation is matchedThe Android app forwards the confirmation, the backend verifies, and status updates automatically.
Payment verification completes automatically once the merchant's SPV flow receives and matches a payment confirmation. This is not an official bKash, Nagad, or Rocket integration or endorsement.
Get started

Authentication

Two auth models, depending on who is calling the API.

Merchant server API key

HEADER
Authorization: Bearer SPV-XXXXXXXX-XXXXXXXX
Content-Type: application/json

Keep this key on the merchant's own server only. Never embed it in browser JavaScript, Android source, website HTML, or a public repository.

Firebase ID token

HEADER
Authorization: Bearer <FIREBASE_ID_TOKEN>

Used by the Android app. The backend validates the Firebase signature, project ID, expiry, and user ID.

Endpoints
GET

API health

/health
CURL
curl https://spv-payment-api.pages.dev/api/v1/health
RESPONSE 200
{
  "ok": true,
  "service": "SPV Payment API",
  "version": "v1",
  "databaseReady": true,
  "timestamp": "2026-07-10T00:00:00.000Z"
}
Endpoints
POST

Merchant payout-wallet sync

Android app only. Syncs enabled payout wallets from the authenticated user's Firestore record.

/merchants/sync
REQUEST
POST /merchants/sync
Authorization: Bearer <FIREBASE_ID_TOKEN>
Content-Type: application/json

{}
  • Merchant ID: SPV- + first 8 upper-case Firebase UID characters
  • Only enabled wallet fields sync: walletBkash, walletNagad, walletRocket
  • Requires kycStatus == approved and at least one enabled wallet
RESPONSE 200
{
  "ok": true,
  "merchantId": "SPV-ABCDEFGH",
  "enabledMethods": ["bKash", "Nagad"],
  "message": "Merchant wallet configuration synced."
}
Endpoints
POST

Create payment intent

Called from the merchant's own server — never from browser JavaScript.

/payment-intents
REQUEST
POST /payment-intents
Authorization: Bearer SPV-XXXXXXXX-XXXXXXXX
Content-Type: application/json

{
  "amount": 1250,
  "orderReference": "ORDER-1001",
  "description": "Premium package purchase"
}
FieldRule
amountBDT number greater than 0
orderReferenceMerchant's own unique order string
descriptionOptional, customer-facing
checkout linkFixed 30-minute validation window
RESPONSE 200
{
  "ok": true,
  "paymentId": "pi_...",
  "status": "pending",
  "amount": 1250,
  "expiresAt": 1760000000000,
  "checkoutUrl": "https://spv-payment-api.pages.dev/payment-portal/checkout.html?payment_id=pi_...&token=pct_..."
}
Redirect the customer to checkoutUrl.

Server example — Node.js

const result = await fetch('https://spv-payment-api.pages.dev/api/v1/payment-intents', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.SPV_API_KEY}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    amount: 1250,
    orderReference: `ORDER-${order.id}`,
    description: 'Premium package purchase'
  })
}).then(r => r.json());

if (result.ok) {
  res.redirect(result.checkoutUrl);
}
Endpoints
GET

Read payment status

/payment-intents/{paymentId}?token={checkoutToken}

Customer checkout polling. Merchant server polls the same path without a token, using its API key instead.

MERCHANT REQUEST
GET /payment-intents/{paymentId}
Authorization: Bearer SPV-XXXXXXXX-XXXXXXXX
RESPONSE 200
{
  "ok": true,
  "paymentId": "pi_...",
  "merchant": { "id": "SPV-ABCDEFGH", "name": "Merchant Brand" },
  "orderReference": "ORDER-1001",
  "description": "Premium package purchase",
  "amount": 1250,
  "currency": "BDT",
  "status": "pending",
  "expiresAt": 1760000000000,
  "updatedAt": 1759990000000,
  "payoutNumbers": {
    "bKash": "01XXXXXXXXX",
    "Nagad": "01XXXXXXXXX"
  }
}
StateMeaning
pendingCheckout created, no submission yet
submittedTransaction ID entered, verifying
verifiedPayment confirmation matched
rejectedVerification window expired
cancelledMerchant/backend cancelled the intent
expiredLink expired; may be renewed for legacy links
Endpoints
POST

Customer payment detail submission

/payment-intents/{paymentId}/submit
REQUEST
POST /payment-intents/{paymentId}/submit
Content-Type: application/json

{
  "token": "pct_...",
  "provider": "bKash",
  "transactionId": "5AB12CD34E"
}

Supported providers: bKash, Nagad, Rocket. Only a Transaction ID is requested — never a sender number. A submitted customer may resubmit a corrected Transaction ID while verification is pending.

RESPONSE 200
{
  "ok": true,
  "paymentId": "pi_...",
  "status": "submitted",
  "message": "Payment details received. Verification in progress."
}
Endpoints
POST

Merchant app SMS event

Generic merchant payments. Matches a pending/submitted intent by amount and expiry for the authenticated merchant.

/mobile/sms-events
REQUEST
POST /mobile/sms-events
Authorization: Bearer SPV-XXXXXXXX-XXXXXXXX
Content-Type: application/json

{
  "transactionId": "5AB12CD34E",
  "provider": "bKash",
  "amount": "1250"
}
Endpoints
POST

Subscription checkout

Android app. Requires an authenticated Firebase user and approved KYC. Subscription checkout always uses the platform owner's configured wallet, never the subscriber's payout wallet.

/subscriptions/checkout
REQUEST
POST /subscriptions/checkout
Authorization: Bearer <FIREBASE_ID_TOKEN>
Content-Type: application/json

{
  "planId": "30d"
}
Plan IDAmountDuration
30d৳9930 days
90d৳28090 days
365d৳999365 days

The app opens the returned browser checkout URL.

Endpoints
POST

Subscription SMS verification

/subscriptions/sms-events
REQUEST
POST /subscriptions/sms-events
Authorization: Bearer <FIREBASE_ID_TOKEN>
Content-Type: application/json

{
  "transactionId": "5AB12CD34E",
  "provider": "bKash",
  "amount": "999"
}

The device can submit payment evidence only after the confirmation SMS names the platform owner's wallet. Evidence enters the secure verification flow; client-provided SMS never activates a subscription directly.

Guides

Checkout UI specification

Rules for the payment-portal checkout page.

Dynamic data source

GET /api/v1/payment-intents/{paymentId}?token={checkoutToken}

Provider visibility

const enabled = Object.entries(data.payoutNumbers)
  .filter(([provider, number]) => number);

If exactly one method is enabled, skip method-selection and open its instruction panel directly.

  • Customer fields: only Transaction ID. Never request sender phone number.
  • After submit, show a professional full-screen verification animation. Verification runs in the background for up to 30 seconds with no visible countdown.
  • No website navbar, hamburger menu, avatar, or account navigation on the payment portal — payment content only.
Backend stateUI
verifiedDedicated receipt page with “You can leave this page” and PDF download
rejectedRejected screen only, with PDF receipt download and no navigation link
submittedResume verifying overlay immediately on reopen