BDPay৳Gatefor Node.js

Home / Node.js / Moneybag Stable

Moneybag

Also for Laravel: https://github.com/byteminus/laravel-bdpaygate/blob/main/docs/gateways/moneybag.md

Status: stable · Driver: moneybag · Refunds: no public API · Sync: no public API (see below)

Moneybag is a Bangladeshi hosted checkout (cards, mobile wallets, EMI). Its public merchant API is small: create a checkout, and verify a transaction.

Setup

MONEYBAG_API_KEY=your-merchant-api-key
MONEYBAG_SANDBOX=true
# MONEYBAG_BASE_URL=https://api.moneybag.com.bd   # optional: override the API origin

These are read by gatewaysFromEnv(process.env). Or configure the gateway in code:

gateways: {
  moneybag: {
    apiKey: process.env.MONEYBAG_API_KEY,
    sandbox: true,
    // baseUrl: process.env.MONEYBAG_BASE_URL, // override the API origin
  },
}

sandbox defaults to true. Set it to false for live payments.

API origin
Sandbox https://sandbox.api.moneybag.com.bd
Live https://api.moneybag.com.bd

Sandbox accounts are self-serve: create one at https://developers.moneybag.com.bd/sandbox (or https://sandbox.moneybag.com.bd). The merchant API key is shown once, so store it straight away. Production keys need a separate review and approval by Moneybag. The public contract lists only the sandbox origin; the live origin above comes from Moneybag's own SDK and plugin releases. Confirm it with Moneybag at go-live, and set baseUrl if they give you another. Docs: https://developers.moneybag.com.bd/docs.

How a payment is verified

  1. pay.charge() creates a checkout (POST /api/v2/payments/checkout, header X-Merchant-API-Key). This attempt's gatewayReference is sent as order_id, and your own reference goes in metadata.reference. The returned session_id is stored in payment.gatewayPaymentId, and the customer is sent to payment.checkoutUrl.
  2. Moneybag returns the customer to /bdpaygate/moneybag/{success|fail|cancel}?ref=…, adding transaction_id and status (SUCCESS, FAILED or CANCELLED).
  3. Neither parameter is trusted. The driver calls GET /api/v2/payments/verify/{transaction_id} and checks:
    • that the transaction's order_id is this attempt's gatewayReference;
    • for a payment, that status is SUCCESS and verified is true, and that amount and currency match.
  4. A verified SUCCESS completes the payment, even on the fail or cancel URL. A verified FAILED or CANCELLED fails or cancels it. Anything else stays pending: an unknown transaction, a non-success answer, or verified: false.
  5. The Moneybag transaction_id is stored in payment.gatewayTransactionId.

No sync, no refunds

The public API can only verify by transaction_id. There is no lookup by order or session, and no refund API: Moneybag lists refunds, payouts and transaction history as merchant-portal features outside the public API. So:

  • pay.for(payment).capabilities is { sync: false, refunds: false }. pay.sync() and pay.refund() throw UnsupportedOperationError, and pay.syncPending() skips Moneybag payments. Refund from the Moneybag merchant portal.

  • If a customer pays but never comes back, find the transaction id in the merchant portal and settle the payment yourself:

    import type { MoneybagGateway } from 'bdpaygate';
    
    const updated = await (pay.for(payment) as MoneybagGateway).verifyTransaction(payment, 'txn…');
    

    This runs the same checks as a return, and fires the usual events.

  • If the customer cancels before a transaction exists, the return has no transaction_id. The payment stays pending, and the customer lands on your pending redirect.

Limits and quirks

  • Amount must be 10.00 to 1000000.00 BDT. Checked before the API call.
  • order_id must be 10 to 36 characters; attempt ids are 20.
  • customer.email is required by Moneybag; the driver falls back to noemail@example.com. Pass customer.postcode (or meta.postcode) to send a postcode.
  • Validation errors are HTTP 422 with {"detail":[{"loc","msg","type"}]}; they are included in the GatewayError message.
  • Webhooks are not handled yet. Moneybag signs webhooks (X-Webhook-Signature: sha256=HMAC(secret, "{timestamp}.{raw body}")), but the payment event body is not in the public contract, and webhook endpoints are configured in the merchant portal. The driver sends no ipn_url.
  • Still unverified: the non-terminal status values returned by verify. Only SUCCESS, FAILED and CANCELLED are documented; anything else leaves the payment pending.

Hosts (checked 2026-10-15)

Both https://sandbox.api.moneybag.com.bd (this driver's sandbox default) and https://staging.api.moneybag.com.bd (the official SDK's default) are live and require authentication. https://api.moneybag.com.bd is production. Override any of them with baseUrl. A merchant API key is issued from the Moneybag panel; there is no public demo key.

Sandbox runs

The Laravel package was verified against the real Moneybag sandbox through a Laravel 13 app with a self-serve sandbox account (developer portal → sandbox onboarding issues a merchant ID and a one-time-display API key). A 120 BDT payment paid with bKash completed after GET /api/v2/payments/verify/{transaction_id}; a simulated failure was confirmed as FAILED and marked failed. The verify response reports the order amount in amount and Moneybag's service charge separately in charge_amount (the customer saw 121.20), so the amount check correctly compares amount.

This Node package was also run end to end in the Moneybag sandbox on 2026-10-15:

  • A payment was paid via Upay. Moneybag showed its service charge separately, and the order amount was the one checked.
  • There is no status API, so no sync was run.

Edit this page on GitHubView as Markdown