HDFC Payment Gateway Integration in PHP: A Step-by-Step Guide (SmartGATEWAY)

HDFC Payment Gateway Integration in PHP: A Step-by-Step Guide (SmartGATEWAY)

September 11, 2026
HDFC SmartGATEWAY integration flow in six steps

If you are adding HDFC Bank payments to a PHP website in 2026, the product you are integrating is HDFC SmartGATEWAY, which HDFC launched with Juspay in September 2024. The flow is four calls: create a session on your server, redirect the customer to the payment link it returns, verify the signed response when they come back, and confirm the result with the order status API before you mark anything as paid.

This guide walks through each step in PHP, with the exact values that differ between sandbox and production and the mistakes that most often look like bugs. Every endpoint and parameter here comes from HDFC’s public developer documentation, linked at the end.

Which HDFC gateway is this?

Older HDFC integrations you will find in tutorials — CCAvenue-based setups, FSS/PGIS, PayZapp — are a different generation. HDFC’s current merchant gateway is SmartGATEWAY, powered by Juspay, and its documentation lives on smartgateway.hdfcbank.com.

One change catches people out: after the RBI’s 2025 domain mandate, HDFC’s official hosts moved to .bank.in. Some documentation pages still show hdfcbank.com hosts. Always copy base URLs from the current docs rather than from a tutorial, including this one.

What you need before writing code

  • Merchant ID — issued by the bank when your merchant account is approved.
  • Client ID — in production this is the same as your merchant ID.
  • API Key — you create it yourself in the SmartGATEWAY dashboard.
  • Response Key — also created in the dashboard, under Payments → Settings → Security. It is used to verify signed responses.
  • A return URL on HTTPS, with no query string.
HDFC SmartGATEWAY sandbox and production values
Sandbox and production use different hosts and a different payment page client ID.

In sandbox, payment_page_client_id is hdfcmaster. In production it is your own merchant ID. Mixing these up is the most common reason a working sandbox integration fails the moment it goes live.

Step 1 — Create a session on your server

The session call happens on your server, never in the browser, because it carries your API key. It uses HTTP Basic authentication with the API key and three headers that identify you to the gateway.

<?php
$apiKey     = getenv('HDFC_API_KEY');
$merchantId = getenv('HDFC_MERCHANT_ID');
$baseUrl    = 'https://smartgateway.hdfcuat.bank.in';   // sandbox; production: https://smartgateway.hdfc.bank.in
$clientId   = 'hdfcmaster';                            // sandbox; production: your merchant ID

$orderId = 'ORD' . time();                // under 21 characters, no special characters
$payload = [
    'order_id'               => $orderId,
    'amount'                 => '499.00', // a string, at most two decimals
    'customer_id'            => 'cust_1024',
    'customer_email'         => 'buyer@example.com',
    'customer_phone'         => '9876543210',
    'payment_page_client_id' => $clientId,
    'action'                 => 'paymentPage',
    'return_url'             => 'https://example.com/payment/return', // HTTPS, no query string
];

$ch = curl_init($baseUrl . '/session');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Basic ' . base64_encode($apiKey),
        'Content-Type: application/json',
        'x-merchantid: ' . $merchantId,
        'x-customerid: cust_1024',
        'x-resellerid: hdfc_reseller',
    ],
    CURLOPT_POSTFIELDS     => json_encode($payload),
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

// Save $orderId and the amount against your cart BEFORE redirecting.
header('Location: ' . $response['payment_links']['web']);
exit;

A successful response has status NEW, an sdk_payload for mobile integrations, and a payment_links object with an expiry. For a website, you only need payment_links.web.

Store the order ID and the expected amount in your database before the redirect. You will need both to check the result — and a customer who closes the tab mid-payment still leaves you an order to reconcile.

Step 2 — Redirect the customer

Send the customer to payment_links.web. They pay on HDFC’s page using a card, UPI or netbanking, so card details never touch your server — which keeps your PCI scope small.

Step 3 — Handle the return, but do not trust it yet

When payment finishes, the customer is sent back to your return URL with POST data describing the result. This is the step where most insecure integrations go wrong: the data arrives through the customer’s browser, so on its own it proves nothing. Anybody can post a form to your return URL.

Verifying the HDFC return URL signature and order status
Two checks before an order is marked paid: the signature, then the order status API.

Turn on Use signed response in the dashboard, create a Response Key, and verify the HMAC-SHA256 signature on every return. The documented method takes every returned parameter except signature and signature_algorithm, encodes and sorts them, and computes the HMAC with your Response Key.

The exact encoding rules matter, and a one-character difference produces a signature that never matches. Use the verification function in HDFC’s official PHP kit — PaymentHandler.php in Juspay’s Integration-kit repository, branch phpBackendKit-ApiKey — rather than rewriting it from the description.

Step 4 — Confirm with the order status API

A valid signature tells you the response came from the gateway. The order status call tells you the payment actually completed, for the amount you expected. HDFC’s docs recommend it for any business-critical decision, and the official kit does not make this call for you.

<?php
function hdfcOrderStatus(string $orderId): array {
    $ch = curl_init(getenv('HDFC_BASE_URL') . '/orders/' . rawurlencode($orderId));
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER     => [
            'Authorization: Basic ' . base64_encode(getenv('HDFC_API_KEY')),
            'x-merchantid: ' . getenv('HDFC_MERCHANT_ID'),
            'x-customerid: cust_1024',
            'version: 2023-06-30',
        ],
    ]);
    $status = json_decode(curl_exec($ch), true);
    curl_close($ch);
    return $status;
}

$status = hdfcOrderStatus($orderId);
$expected = $db->expectedAmount($orderId);        // what you saved before the redirect

if (($status['status'] ?? '') === 'CHARGED' && (float) $status['amount'] === (float) $expected) {
    $db->markPaid($orderId);                       // the only place an order becomes paid
} else {
    $db->markFailedOrPending($orderId, $status['status'] ?? 'UNKNOWN');
}

CHARGED means success. You will also see statuses such as PENDING_VBV, AUTHENTICATION_FAILED, AUTHORIZATION_FAILED and AUTHORIZED. Treat everything that is not CHARGED as not paid, and compare the amount — a signed response for a different amount is still a problem.

Step 5 — Webhooks, for the customers who never come back

Some customers pay and close the tab before the redirect. Webhooks cover them. Configure them in Dashboard → Payments → Settings → Webhook, with Basic auth credentials and, if you like, custom headers.

  • Check the Basic auth header on every webhook request.
  • Allowlist HDFC’s published IP addresses — copy them from the webhooks page of the docs.
  • Make the handler idempotent. SmartGATEWAY retries until you return HTTP 200, so the same event can arrive more than once. Marking an already-paid order as paid again must do nothing.
  • Still call the order status API before fulfilling, exactly as on the return URL.

Testing in sandbox

HDFC SmartGATEWAY sandbox test amounts, cards and UPI IDs
Sandbox amounts decide the outcome, so every path can be tested deliberately.

In sandbox, the amount decides the result: below ₹500 succeeds, ₹500–700 fails, and above ₹700 goes pending before succeeding. That lets you test the success, failure and pending paths on purpose. The docs also publish test cards (4012 0000 0000 1097 and 5204 7305 4100 0464, OTP 000000) and UPI IDs (success@upi, failure@upi).

Test the pending path properly. It is the one most integrations handle badly, and it is the one that produces “I paid but the order says unpaid” support tickets.

Going live

HDFC SmartGATEWAY go-live checklist
Most go-live failures are one of these six.

Production starts in a restricted mode: each transaction is capped at ₹10 and you get 200 transactions a day until the bank completes its QA sign-off. It is normal, and it looks exactly like a bug if nobody told you.

  • Switch the base URL to production and payment_page_client_id to your merchant ID.
  • Create a fresh API Key and Response Key for production — sandbox keys do not carry over.
  • Confirm Use signed response is on in production too.
  • Run one real ₹1–₹10 payment end to end and confirm the order is marked paid by the status check, not the redirect.
  • Confirm a webhook arrives and a repeated delivery changes nothing.

Using WooCommerce instead?

HDFC publishes an official WooCommerce plugin for SmartGATEWAY. It is not on wordpress.org — it is a zip in Juspay’s Integration-kit repository (branch Smartgateway-Woocommerce), installed through Plugins → Add New → Upload, and it needs your Merchant ID, Client ID and API key. Test it in sandbox against your theme and checkout plugins before going live; supported WordPress and WooCommerce versions are not stated in the docs.

Keeping keys and logs safe

The official PHP kit reads its settings from a config.jsonAPI_KEY, MERCHANT_ID, PAYMENT_PAGE_CLIENT_ID, BASE_URL, RESPONSE_KEY, and two logging options, ENABLE_LOGGING and LOGGING_PATH.

  • Keep that file outside the web root, or load the values from environment variables as in the examples above. A config.json inside public_html can be downloaded by anybody who guesses its path.
  • Point LOGGING_PATH outside the web root too, and make sure nothing logs the API key or the full request headers.
  • Use separate keys for sandbox and production, and rotate the production API key if it has ever been in a repository or a chat message.

Frequently asked questions

Why does production only accept ₹10 payments?

New production accounts start in restricted mode: ₹10 per transaction and 200 transactions a day. The limits are lifted after the bank’s QA sign-off. It is expected behaviour, not a bug in your code.

Can I mark the order paid on the return URL?

Not on its own. The return URL arrives through the customer’s browser. Verify the HMAC signature, then confirm with the order status API, and only mark the order paid when the status is CHARGED and the amount matches what you saved before the redirect.

What should happen when the status is pending?

Leave the order unpaid, tell the customer the payment is being confirmed, and let the webhook or a later order status check settle it. Never ask the customer to pay again while a payment is pending — that is how double charges happen.

Where do I find the Response Key?

In the SmartGATEWAY dashboard under Payments → Settings → Security. Then turn on Use signed response under Settings → General, otherwise responses arrive unsigned.

Want it done for you?

Happy Coders builds and maintains payment integrations for PHP sites and WooCommerce stores — HDFC SmartGATEWAY, Razorpay and others — including the signature checks, order status reconciliation and webhook handling that decide whether payments can be trusted. If you would rather hand it over, message us on WhatsApp at +91 87789 05772 or get in touch.

Sources