Nagad Payment Gateway • PHP Integration Docs

Nagad Integration Documentation

এই single-page HTML documentation দিয়ে অন্য যেকোনো PHP site-এ Nagad payment integration দ্রুত setup করা যাবে। এখানে API URL, required credentials, request flow, helper functions, example code, example DB structure, আর frontend trigger একসাথে দেওয়া আছে।

কি কি লাগবে

Required Credentials
  • merchant_id
  • merchant_number
  • pg_public_key
  • merchant_private_key
  • callback_url
  • mode = sandbox / live
Required PHP Setup
  • PHP 7.4+ / 8+
  • curl extension
  • openssl extension
  • pdo + MySQL
  • Session enabled
Unique order / invoice id Pending payment row Logged-in user Callback handler Wallet or balance update logic

API Base URL

Sandbox

http://sandbox.mynagad.com:10080/remote-payment-gateway-1.0/

Live

https://api.mynagad.com/
Production-এ live mode ব্যবহার করো এবং SSL verification অবশ্যই on রাখো।

Payment Flow

1. User payment button click করবে
2. System pending transaction তৈরি বা load করবে
3. Nagad Initialize API call করবে
4. Initialize response থেকে paymentReferenceIdchallenge নিবে
5. Nagad Complete API call করবে
6. Response-এর callBackUrl এ redirect করবে
7. Callback file এ payment_ref_id নিয়ে Verify call করবে
8. Verify success হলে DB update + wallet credit করবে

PHP Helper Functions

<?php
function nagadGenerateRandomString(int $length = 40): string
{
    $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
    $max = strlen($characters) - 1;
    $random = '';

    for ($i = 0; $i < $length; $i++) {
        $random .= $characters[random_int(0, $max)];
    }

    return $random;
}

function nagadEncryptDataWithPublicKey(string $data, string $pgPublicKey): string
{
    $publicKey = "-----BEGIN PUBLIC KEY-----\n" . trim($pgPublicKey) . "\n-----END PUBLIC KEY-----";
    $keyResource = openssl_pkey_get_public($publicKey);

    if (!$keyResource) {
        throw new RuntimeException('Invalid Nagad public key.');
    }

    if (!openssl_public_encrypt($data, $encrypted, $keyResource)) {
        throw new RuntimeException('Public key encryption failed.');
    }

    return base64_encode($encrypted);
}

function nagadSignatureGenerate(string $data, string $merchantPrivateKey): string
{
    $privateKey = "-----BEGIN RSA PRIVATE KEY-----\n" . trim($merchantPrivateKey) . "\n-----END RSA PRIVATE KEY-----";

    if (!openssl_sign($data, $signature, $privateKey, OPENSSL_ALGO_SHA256)) {
        throw new RuntimeException('Signature generation failed.');
    }

    return base64_encode($signature);
}

function nagadDecryptDataWithPrivateKey(string $crypttext, string $merchantPrivateKey): string
{
    $privateKey = "-----BEGIN RSA PRIVATE KEY-----\n" . trim($merchantPrivateKey) . "\n-----END RSA PRIVATE KEY-----";

    if (!openssl_private_decrypt(base64_decode($crypttext), $plainText, $privateKey)) {
        throw new RuntimeException('Private key decrypt failed.');
    }

    return $plainText;
}

function nagadHttpPostMethod(string $url, array $postData): array
{
    $ch = curl_init($url);

    curl_setopt_array($ch, [
        CURLOPT_HTTPHEADER => [
            'Content-Type: application/json',
            'X-KM-Api-Version: v-0.2.0',
            'X-KM-IP-V4: ' . ($_SERVER['REMOTE_ADDR'] ?? '127.0.0.1'),
            'X-KM-Client-Type: PC_WEB'
        ],
        CURLOPT_CUSTOMREQUEST => 'POST',
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POSTFIELDS => json_encode($postData),
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_TIMEOUT => 45,
        CURLOPT_SSL_VERIFYHOST => 2,
        CURLOPT_SSL_VERIFYPEER => 1,
    ]);

    $result = curl_exec($ch);

    if ($result === false) {
        throw new RuntimeException('Curl error: ' . curl_error($ch));
    }

    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    return [
        '_http_code' => $httpCode,
        '_raw' => $result,
        '_decoded' => json_decode($result, true),
    ];
}

function nagadHttpGet(string $url): string
{
    $ch = curl_init();

    curl_setopt_array($ch, [
        CURLOPT_URL => $url,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_CONNECTTIMEOUT => 15,
        CURLOPT_TIMEOUT => 45,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_HEADER => false,
        CURLOPT_SSL_VERIFYHOST => 2,
        CURLOPT_SSL_VERIFYPEER => 1,
    ]);

    $result = curl_exec($ch);

    if ($result === false) {
        throw new RuntimeException('Curl error: ' . curl_error($ch));
    }

    curl_close($ch);
    return $result;
}
?>

Initialize Call Example

<?php
$merchantId         = 'YOUR_MERCHANT_ID';
$merchantNumber     = 'YOUR_MERCHANT_NUMBER';
$pgPublicKey        = 'YOUR_NAGAD_PUBLIC_KEY';
$merchantPrivateKey = 'YOUR_MERCHANT_PRIVATE_KEY';
$nagadHost          = 'https://api.mynagad.com/'; // or sandbox

$invoice  = 'NGD' . date('ymdHis') . random_int(100, 999);
$dateTime = date('YmdHis');

$sensitiveData = [
    'merchantId' => $merchantId,
    'datetime'   => $dateTime,
    'orderId'    => $invoice,
    'challenge'  => nagadGenerateRandomString()
];

$initPayload = [
    'accountNumber' => $merchantNumber,
    'dateTime'      => $dateTime,
    'sensitiveData' => nagadEncryptDataWithPublicKey(json_encode($sensitiveData), $pgPublicKey),
    'signature'     => nagadSignatureGenerate(json_encode($sensitiveData), $merchantPrivateKey)
];

$initUrl = $nagadHost . "api/dfs/check-out/initialize/{$merchantId}/{$invoice}";
$initResponse = nagadHttpPostMethod($initUrl, $initPayload);

$initDecoded = $initResponse['_decoded'] ?? [];
$plainResponse = json_decode(
    nagadDecryptDataWithPrivateKey($initDecoded['sensitiveData'], $merchantPrivateKey),
    true
);

$paymentReferenceId = $plainResponse['paymentReferenceId'] ?? '';
$challenge = $plainResponse['challenge'] ?? '';
?>

Complete Call Example

<?php
$callbackUrl = 'https://yourdomain.com/payment/nagad_callback.php?token=ORDER_TOKEN';
$payableBdt  = 1000.00;

$sensitiveOrderData = [
    'merchantId'   => $merchantId,
    'orderId'      => $invoice,
    'currencyCode' => '050',
    'amount'       => number_format($payableBdt, 2, '.', ''),
    'challenge'    => $challenge
];

$completePayload = [
    'sensitiveData' => nagadEncryptDataWithPublicKey(json_encode($sensitiveOrderData), $pgPublicKey),
    'signature'     => nagadSignatureGenerate(json_encode($sensitiveOrderData), $merchantPrivateKey),
    'merchantCallbackURL' => $callbackUrl,
    'additionalMerchantInfo' => (object)[
        'order_token' => 'ORDER_TOKEN'
    ]
];

$completeUrl = $nagadHost . "api/dfs/check-out/complete/{$paymentReferenceId}";
$completeResponse = nagadHttpPostMethod($completeUrl, $completePayload);
$completeDecoded = $completeResponse['_decoded'] ?? [];

if (($completeDecoded['status'] ?? '') === 'Success' && !empty($completeDecoded['callBackUrl'])) {
    header('Location: ' . $completeDecoded['callBackUrl']);
    exit;
}
?>

Verify Call Example

<?php
$paymentRefId = $_GET['payment_ref_id'] ?? '';
$nagadHost    = 'https://api.mynagad.com/';

$verifyUrl = $nagadHost . 'api/dfs/verify/payment/' . urlencode($paymentRefId);
$verifyRaw = nagadHttpGet($verifyUrl);
$verifyRes = json_decode($verifyRaw, true);

if (!is_array($verifyRes)) {
    $verifyRes = [];
}

if (strcasecmp(($verifyRes['status'] ?? ''), 'Success') === 0) {
    // payment success
    // update transaction completed
    // credit wallet / order confirm
} else {
    // payment failed or cancelled
}
?>
Verify success হলেই final database update করবে। Complete call success হলেই payment final হয়েছে ধরে নিবে না।

Example DB Table

CREATE TABLE payment_transactions (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    user_id BIGINT UNSIGNED NOT NULL,
    order_id VARCHAR(100) NOT NULL,
    payment_method VARCHAR(50) DEFAULT NULL,
    transaction_id VARCHAR(150) DEFAULT NULL,
    payment_id VARCHAR(150) DEFAULT NULL,
    payer_number VARCHAR(30) DEFAULT NULL,
    deposit_amount DECIMAL(18,4) NOT NULL DEFAULT 0.0000,
    deposit_currency VARCHAR(10) NOT NULL DEFAULT 'USD',
    usd_amount DECIMAL(18,4) NOT NULL DEFAULT 0.0000,
    exchange_rate_used DECIMAL(18,6) NOT NULL DEFAULT 0.000000,
    credited_amount DECIMAL(18,4) NOT NULL DEFAULT 0.0000,
    status ENUM('pending','completed','failed','cancelled') NOT NULL DEFAULT 'pending',
    payment_date DATETIME DEFAULT NULL,
    raw_response LONGTEXT DEFAULT NULL,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY uk_order_id (order_id),
    KEY idx_user_id (user_id),
    KEY idx_status (status)
);

Gateway Credentials Table Example

CREATE TABLE automatic_payment_gateways (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    gateway_name VARCHAR(50) NOT NULL,
    mode VARCHAR(20) NOT NULL DEFAULT 'sandbox',
    merchant_id VARCHAR(191) NOT NULL,
    merchant_number VARCHAR(50) NOT NULL,
    callback_url VARCHAR(255) NOT NULL,
    pg_public_key LONGTEXT NOT NULL,
    merchant_private_key LONGTEXT NOT NULL,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

Frontend Trigger Example

Simple HTML Button

<a href="/payment/nagad_pay.php?token=ORDER_TOKEN" class="pay-btn">Pay with Nagad</a>

Styled HTML + CSS

<style>
.pay-btn {
    display: inline-block;
    padding: 12px 20px;
    background: #f04e23;
    color: #fff;
    text-decoration: none;
    border-radius: 10px;
    font-weight: 700;
}
.pay-btn:hover {
    opacity: .92;
}
</style>

<a href="/payment/nagad_pay.php?token=ORDER_TOKEN" class="pay-btn">Pay with Nagad</a>

Production Notes

  • Callback file-এ verify success ছাড়া wallet credit করবে না
  • Transaction row update করার সময় status = 'pending' condition রাখো
  • Double credit ঠেকাতে callback logic transaction-safe রাখো
  • payment_ref_id, invoice, order_id আলাদা purpose-এ ব্যবহার করো
  • raw_response রাখলে debugging সহজ হয়
  • Production-এ file log না রাখলেও DB audit trail useful
  • Live environment-এ SSL verify off রাখবে না
চাইলে এই page-এ পরের ধাপে আমি full pay file আর full callback file এর ready-to-use version-ও যোগ করে দিতে পারি।