Nagad
Status: beta · Driver: nagad · Refunds: no API (refund from the merchant portal) · Sync: yes
Beta. This driver was built from public information and has not yet been verified against a Nagad merchant sandbox. Get the official integration guide from Nagad during onboarding and check the behaviour described here against it. Please report anything that differs.
Nagad's Online Payment API is a hosted checkout. The customer pays with their Nagad wallet (number, OTP, PIN) on Nagad's page. Access is not self-serve: you need a Nagad merchant account, and Nagad's key account team issues sandbox credentials by email.
Regulatory note. Nagad is operating, but as of 2026 it runs under an interim Bangladesh Bank approval and a Bangladesh Bank appointed administrator. Its licence status after that interim period was not publicly confirmed when this driver was written. That is a business-continuity risk rather than a technical one. If Nagad matters to your checkout, consider an aggregator (ShurjoPay, aamarPay) as a fallback.
Setup
NAGAD_MERCHANT_ID=
NAGAD_MERCHANT_NUMBER=
NAGAD_PUBLIC_KEY=
NAGAD_PRIVATE_KEY=
NAGAD_SANDBOX=true
NAGAD_BASE_URL=
NAGAD_SIGNATURE_ALGORITHM=sha256
NAGAD_IP=
NAGAD_PROXY=
// config/bdpaygate.php → gateways
'nagad' => [
'driver' => 'nagad',
'merchant_id' => env('NAGAD_MERCHANT_ID'),
'merchant_number' => env('NAGAD_MERCHANT_NUMBER'), // optional, your 11-digit merchant wallet
'public_key' => env('NAGAD_PUBLIC_KEY'), // Nagad's payment gateway public key
'private_key' => env('NAGAD_PRIVATE_KEY'), // your merchant private key
'sandbox' => env('NAGAD_SANDBOX', true),
'base_url' => env('NAGAD_BASE_URL'), // use the exact URL from your onboarding email
'signature_algorithm' => env('NAGAD_SIGNATURE_ALGORITHM', 'sha256'),
'verify_response_signature' => env('NAGAD_VERIFY_RESPONSE_SIGNATURE', false),
'ip' => env('NAGAD_IP'), // fallback for the X-KM-IP-V4 header
'proxy' => env('NAGAD_PROXY'), // fixed, whitelisted egress IP
// 'api_version' => 'v-0.2.0',
// 'client_type' => 'PC_WEB', // PC_WEB, MOBILE_WEB, MOBILE_APP, WALLET_WEB_VIEW
// 'locale' => 'EN', // EN or BN
],
Keys
There are two different keys, and mixing them up is the most common Nagad integration problem:
| Key | Config | Used for |
|---|---|---|
| Nagad's payment gateway public key | public_key |
Encrypting what you send, and checking Nagad's signatures |
| Your merchant private key | private_key |
Signing what you send, and decrypting Nagad's answers |
- In sandbox, Nagad emails you both. In live, you generate your key pair in the Nagad merchant portal and upload the public half there. Every "generate" creates a new pair and invalidates the old one.
- Keys can be given as PEM text (PKCS#1 or PKCS#8), as a bare base64 body with the header lines removed, or as an absolute path to a PEM file. PEM text in
.envwith literal\nsequences works too. - Never commit keys. The tests generate throwaway pairs on the fly.
Hosts
The sandbox address differs between sources, so set base_url to whatever your onboarding email says, including the path up to /api/dfs.
- Default sandbox:
https://sandbox-ssl.mynagad.com/remote-payment-gateway-1.0/api/dfs. Other addresses seen in the wild includehttp://sandbox.mynagad.com:10080/remote-payment-gateway-1.0/api/dfsandhttp://sandbox.mynagad.com:30001/api/dfs. - Default live:
https://api.mynagad.com/api/dfs.
Signature algorithm
The algorithm used to sign requests is configurable because public sources disagree. Integrations that work in production sign with RSA-SHA256, and a signature error (16_0006_058) is commonly fixed by switching to it, so sha256 is the default. Set signature_algorithm to sha1 only if Nagad tells you to. SHA256withRSA and SHA1withRSA are accepted as aliases.
verify_response_signature checks the signature Nagad puts on its encrypted answers, using the same algorithm. It is off by default because the algorithm Nagad uses for responses is not publicly confirmed. Turn it on once you have confirmed it in the sandbox.
Callback URL
The package sends its callback URL with every order:
https://your-app.com/bdpaygate/nagad/callback
In live, Nagad also asks for the callback URL in the merchant portal and whitelists the domain. Register this URL there.
How a payment is verified
-
charge()calls initialize (POST /check-out/initialize/{merchantId}/{orderId}). It sendsmerchantId,datetime,orderIdand a random 40-characterchallengeas JSON encrypted with Nagad's public key and signed with your private key. Nagad's answer is decrypted with your private key and gives apaymentReferenceId. That id is saved asgateway_payment_idbefore the next step, because Nagad's verify API only works by this id. -
charge()then calls complete (POST /check-out/complete/{paymentReferenceId}) with the encryptedmerchantId,orderId,amount,currencyCode(050) and the challenge, plusmerchantCallbackURLandadditionalMerchantInfo(your own reference is stored there asreference; add more with$meta['additional_merchant_info']). Nagad answers with the checkout URL, stored ascheckout_url. -
After paying, Nagad redirects the customer's browser to
/bdpaygate/nagad/callbackwithorder_id,payment_ref_id,status,status_codeand more. None of this is signed. The package only usesorder_idto find the payment. -
The package calls verify (
GET /verify/payment/{paymentRefId}) with thepaymentReferenceIdit stored, never one from the callback. The answer must carry the sameorderIdandmerchantId. Thenstatusdecides:statusResult SuccessCompleted, once the amount matches ( issuerPaymentRefNostored asgateway_transaction_id)Cancelled,AbortedCancelled Failed,Fraud,InvalidRequest,UnKnownFailedFailed OrderInitiated,Ready,InProgress,OtpSent,OtpVerified,PinGiven,PartialCancelled, anything elseStays pending HTTP 404 with reason 16_0004_999(no such payment yet)Stays pending statusis used rather thanstatusCode, whose format varies. A failure callback for a payment Nagad reports as paid completes it. -
If Nagad cannot be reached, the payment stays pending and the customer goes to your
pendingpage.
Nagad returns amount as a string with no fixed format ("130", "130.00"). It is compared numerically.
The callback is a browser redirect, so it is lost when the customer closes the tab. Schedule bdpaygate:sync to settle those payments.
Refunds
Nagad publishes no refund API. Refund from the Nagad merchant portal or through Nagad. BdPayGate::refund() throws UnsupportedOperationException for this gateway. If Nagad gives you a refund specification during onboarding, please open an issue.
Limits and quirks
- IP whitelisting. In live, Nagad only accepts calls from server IPs it has whitelisted, which especially matters for servers outside Bangladesh. A rejected call typically returns an HTML page saying "The requested URL was rejected". If your app servers have no fixed IP, set
proxy(or the globalBDPAYGATE_PROXY) to a proxy with a fixed, whitelisted egress IP. - Bangladesh time. Request timestamps are sent in Asia/Dhaka time (
yyyyMMddHHmmss) whatever your app timezone is. Nagad rejects timestamps outside a short window (16_0006_076), so keep the server clock synced with NTP. - Order ids. Nagad accepts 5 to 20 letters and digits, and rejects a reused id on the same day (
16_0006_083). Each attempt gets a fresh 20-character uppercase id (gateway_reference), so a retry never reuses one. The driver refuses to send an id outside those rules. X-KM-IP-V4must be the customer's IPv4 address. The driver sends the request IP or$meta['customer_ip']when it is a public IPv4 address. Otherwise (IPv6, private addresses, local development, queued jobs andbdpaygate:sync) it sendsipfrom the config, so set that to your server's public IPv4.- Small encrypted payload. RSA-2048 can encrypt at most 245 bytes, so the encrypted JSON holds only the fields Nagad requires.
- Currency must be
BDT. - Error envelope. Errors come as
{"reason": "16_0006_xxx", "message": "..."}. Both are included in the exception message, and the body is logged on the payment.