Third-Party API Integrations

Razorpay Integration in PHP and Node.js: Complete Guide

By Domain India Team · DomainIndia SupportPublished 10 min read
Knowledge base article
Contents (10 sections)

Razorpay is one of the most widely used payment gateways for Indian websites, shops and SaaS apps. Many integration guides skip the security-critical step of verifying the payment on your server. This guide builds the full flow properly, in PHP and Node.js: create the order on your server, open Razorpay Checkout in the browser, verify the signature, and handle webhooks.

Key takeaways

Create every Razorpay order on your server, open Checkout in the browser with the order ID, then verify the returned signature on your server with an HMAC-SHA256 of order_id|payment_id using your key secret. Add a webhook, verified the same way over the raw request body, so orders are marked paid even when the customer closes the browser. Keep the key secret out of your code and your front end. On shared hosting, test the PHP SDK on your own plan first, because some servers disable the cURL functions it prefers.

1. How a Razorpay payment works

text
1. The customer clicks "Pay" on your site.
2. Your server calls the Razorpay Orders API and gets an order_id.
3. Your server sends the order_id and your public key_id to the browser.
4. The browser opens Razorpay Checkout with those values.
5. The customer pays inside Razorpay's window.
6. Checkout returns payment_id, order_id and signature to your page.
7. Your server VERIFIES the signature with your key secret.
8. Your server marks the order paid and delivers the product.
9. Razorpay also sends a webhook; your server verifies it and marks
   the order paid if it isn't already.

Steps 7 and 9 are the ones that matter for security. Anything the browser reports can be faked; a signature made with your secret cannot.

2. Before you start: keys and secrets

  1. Create a Razorpay account. Test mode lets you build and test before your business verification is complete; live mode needs Razorpay's KYC.
  2. In the Razorpay Dashboard, generate test API keys. The Key ID starts with rzp_test_; the Key Secret is shown once.
  3. Store both where your code can read them but nobody else can.
ini
RAZORPAY_KEY_ID=rzp_test_xxxxxxxxxxxx
RAZORPAY_KEY_SECRET=xxxxxxxxxxxxxxxxxxxx
RAZORPAY_WEBHOOK_SECRET=set-this-when-you-add-the-webhook
No secrets in code

Never hard-code the key secret or webhook secret in a PHP or JavaScript file, never commit them to Git, and never send them to the browser. Only the Key ID is public. On shared hosting, keep them in a .env or config file outside public_html; on the App Platform, use its environment variables. See environment variables and secrets management.

Use the test card and UPI details from Razorpay's own documentation while you build; they change from time to time.

3. PHP: create the order

Install the official SDK. Composer isn't pre-installed on Domain India shared hosting, so either run it over jailed SSH as php composer.phar require razorpay/razorpay or run it on your computer and upload the project with its vendor folder.

bash
composer require razorpay/razorpay

create-order.php, with the secrets read from a config file outside the web root:

php
<?php
require __DIR__ . '/vendor/autoload.php';
$config = require '/home/youruser/private/razorpay.php'; // returns an array
use Razorpay\Api\Api;

$api = new Api($config['key_id'], $config['key_secret']);

// Take the amount from YOUR database, never from the browser.
$amountPaise = 50000; // ₹500.00

$order = $api->order->create([
    'receipt'  => 'rcpt_' . bin2hex(random_bytes(6)),
    'amount'   => $amountPaise,
    'currency' => 'INR',
]);

// Save $order->id against the customer's cart in your database here.

header('Content-Type: application/json');
echo json_encode([
    'order_id' => $order->id,
    'key_id'   => $config['key_id'],
    'amount'   => $amountPaise,
]);

The amount is in paise: ₹500 is 50000. Sending 500 asks for ₹5.

4. PHP on shared hosting: when the SDK can't connect

The Razorpay PHP SDK makes its API calls through an HTTP library that prefers cURL. On our cPanel servers curl_exec works. On our DirectAdmin servers curl_exec and curl_multi_exec are disabled on most sites, while allow_url_fopen is on. So test order creation on your own plan before you go live. If the SDK fails there, call the Orders API through PHP's HTTPS stream wrapper instead:

php
<?php
function razorpay_post(string $path, array $body, array $config): array
{
    $auth = base64_encode($config['key_id'] . ':' . $config['key_secret']);
    $ctx  = stream_context_create(['http' => [
        'method'        => 'POST',
        'header'        => "Authorization: Basic $auth\r\nContent-Type: application/json\r\n",
        'content'       => json_encode($body),
        'timeout'       => 20,
        'ignore_errors' => true, // read Razorpay's error body too
    ]]);
    $raw = file_get_contents('https://api.razorpay.com/v1' . $path, false, $ctx);
    $status = isset($http_response_header[0]) ? (int) explode(' ', $http_response_header[0])[1] : 0;
    if ($raw === false || $status < 200 || $status >= 300) {
        throw new RuntimeException('Razorpay API error ' . $status . ': ' . $raw);
    }
    return json_decode($raw, true);
}

$order = razorpay_post('/orders', [
    'amount' => 50000, 'currency' => 'INR', 'receipt' => 'rcpt_123',
], $config);

Signature checks (section 6) need no network access at all, so they work on every plan. If neither route works on your plan, run the payment code on the App Platform or a VPS, where nothing is disabled. Background: PHP disabled functions on shared hosting.

5. The browser: open Checkout

html
<script src="https://checkout.razorpay.com/v1/checkout.js"></script>
<button id="pay-btn">Pay ₹500</button>
<script>
document.getElementById('pay-btn').addEventListener('click', async () => {
  const data = await (await fetch('/create-order.php', { method: 'POST' })).json();
  new Razorpay({
    key: data.key_id,
    amount: data.amount,
    currency: 'INR',
    name: 'Your Company',
    order_id: data.order_id,
    handler: async (response) => {
      // razorpay_payment_id, razorpay_order_id, razorpay_signature
      const r = await fetch('/verify-payment.php', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(response),
      });
      const result = await r.json();
      alert(result.verified ? 'Payment received. Thank you!' : 'We could not confirm the payment yet.');
    },
  }).open();
});
</script>

The page only shows a message. It never marks anything as paid; your server does that after verifying.

6. Verify the payment signature on your server

Razorpay signs order_id + "|" + payment_id with HMAC-SHA256 using your key secret. Recompute it and compare in constant time.

PHP, with hash_hmac and hash_equals:

php
<?php
$config = require '/home/youruser/private/razorpay.php';
$in = json_decode(file_get_contents('php://input'), true) ?: [];

$orderId   = (string) ($in['razorpay_order_id'] ?? '');
$paymentId = (string) ($in['razorpay_payment_id'] ?? '');
$signature = (string) ($in['razorpay_signature'] ?? '');

// The order must be one YOU created for this customer and is still unpaid.
// if (!orderBelongsToCurrentCart($orderId)) { http_response_code(400); exit; }

$expected = hash_hmac('sha256', $orderId . '|' . $paymentId, $config['key_secret']);

header('Content-Type: application/json');
if ($signature !== '' && hash_equals($expected, $signature)) {
    markOrderPaid($orderId, $paymentId); // idempotent
    echo json_encode(['verified' => true]);
} else {
    http_response_code(400);
    echo json_encode(['verified' => false]);
}

The SDK's $api->utility->verifyPaymentSignature() does the same calculation if you prefer it.

Node.js (install with npm install razorpay):

javascript
import crypto from 'node:crypto';

function safeEqualHex(a, b) {
  const x = Buffer.from(a, 'utf8');
  const y = Buffer.from(String(b || ''), 'utf8');
  return x.length === y.length && crypto.timingSafeEqual(x, y);
}

app.post('/verify-payment', express.json(), (req, res) => {
  const { razorpay_order_id, razorpay_payment_id, razorpay_signature } = req.body;
  const expected = crypto
    .createHmac('sha256', process.env.RAZORPAY_KEY_SECRET)
    .update(`${razorpay_order_id}|${razorpay_payment_id}`)
    .digest('hex');

  if (safeEqualHex(expected, razorpay_signature)) {
    markOrderPaid(razorpay_order_id, razorpay_payment_id); // idempotent
    return res.json({ verified: true });
  }
  res.status(400).json({ verified: false });
});

crypto.timingSafeEqual throws if the two buffers differ in length, so check the length first as shown.

7. Webhooks: the safety net

If the customer closes the browser straight after paying, your handler never runs, but Razorpay has the money. A webhook tells your server anyway.

In the Razorpay Dashboard, add a webhook with your HTTPS URL (for example https://yourdomain.com/webhooks/razorpay.php), a long random secret saved as RAZORPAY_WEBHOOK_SECRET, and the events you need, such as payment.captured, payment.failed, order.paid and refund.processed.

PHP handler:

php
<?php
$config    = require '/home/youruser/private/razorpay.php';
$payload   = file_get_contents('php://input');           // raw body
$signature = $_SERVER['HTTP_X_RAZORPAY_SIGNATURE'] ?? '';
$expected  = hash_hmac('sha256', $payload, $config['webhook_secret']);

if ($signature === '' || !hash_equals($expected, $signature)) {
    http_response_code(400);
    exit('invalid signature');
}

$event = json_decode($payload, true);
if ($event['event'] === 'payment.captured') {
    $p = $event['payload']['payment']['entity'];
    markOrderPaid($p['order_id'], $p['id']); // safe to call twice
}
http_response_code(200);
echo 'ok';

Node.js handler. Use the raw body, not express.json(), because the signature covers the exact bytes Razorpay sent:

javascript
app.post('/webhooks/razorpay', express.raw({ type: 'application/json' }), (req, res) => {
  const expected = crypto
    .createHmac('sha256', process.env.RAZORPAY_WEBHOOK_SECRET)
    .update(req.body) // Buffer
    .digest('hex');
  if (!safeEqualHex(expected, req.headers['x-razorpay-signature'])) {
    return res.status(400).send('invalid signature');
  }
  const event = JSON.parse(req.body.toString());
  if (event.event === 'payment.captured') {
    const p = event.payload.payment.entity;
    markOrderPaid(p.order_id, p.id);
  }
  res.status(200).send('ok');
});

Razorpay retries a webhook that does not get a 2xx answer, so the same event can arrive more than once. Make markOrderPaid idempotent: check the order's state and update it only if it is not already paid.

8. Going live

  1. Finish Razorpay KYC
    and add your settlement bank account.
  2. Generate live keys
    (rzp_live_) and put them in your server-side config, not in code.
  3. Add a production webhook
    with a new secret, and confirm deliveries in the Dashboard's webhook logs.
  4. Check payment capture settings
    in the Dashboard, so payments are captured automatically or by your code.
  5. Check the amount on the server
    against your own order record, never a value sent by the browser.
  6. Make a small real payment
    end to end, then refund it.

9. Common mistakes

Rupees instead of paise
500 means ₹5. Multiply rupees by 100 and store amounts as integers.
Trusting the browser
A success message in the browser proves nothing. Verify the signature on the server.
Secret in the front end
Only the Key ID goes to the browser. The key secret and webhook secret stay on the server.
No webhook
Customers who close the tab after paying leave orders stuck as unpaid.
Parsed webhook body
Verifying a re-serialised JSON body fails. Use the raw request body.
Double processing
Retried webhooks deliver the same event twice. Make order updates idempotent.
Test and live keys mixed
Check the rzp_test_ or rzp_live_ prefix in each environment.

10. Running this on Domain India

  • Shared hosting (cPanel, DirectAdmin, Webuzo): PHP integrations run well. Build vendor on your computer, keep secrets outside public_html, and test the SDK on your plan as described in section 4. Free SSL is included, which the webhook URL needs.
  • App Platform: Node.js apps are detected and built automatically; PHP needs a Dockerfile. Set the keys as environment variables. See App Platform: getting started.
  • VPS: full root access and no disabled functions, for larger shops or custom stacks.

Prices on the cards are live and exclude 18% GST.

cPanel Starter
₹125/mo + GST
  • 25 GB NVMe SSD Storage
  • 50 GB Monthly Bandwidth
  • 1 Website
  • 10 Email Accounts
See plan details
App Starter
₹100/mo + GST
  • 512 MB RAM per app
  • 1 vCPU
  • 5 GB NVMe SSD
  • PostgreSQL Database
See plan details
How do I verify a Razorpay payment signature in PHP?

Compute hash_hmac('sha256', order_id . '|' . payment_id, key_secret) on your server and compare it with the razorpay_signature value using hash_equals. Use the order ID you saved when you created the order, and mark the order paid only if the two match.

Why does Razorpay's PHP SDK fail on my shared hosting?

The SDK prefers cURL for its API calls, and on Domain India DirectAdmin servers curl_exec is disabled on most sites. Test on your plan. If it fails, call the Orders API with file_get_contents and a stream context, since allow_url_fopen is on, or run the code on the App Platform or a VPS.

Do I need webhooks if I already verify the signature?

Yes. If the customer closes the browser after paying, your verification request never runs. A webhook tells your server about the payment anyway. Verify the webhook signature over the raw request body and make your order update safe to run twice.

Where should I keep my Razorpay key secret?

On the server only, in a .env or config file outside public_html, or in your platform's environment variables. Never commit it to Git and never send it to the browser. Only the Key ID is public.

Is the Razorpay amount in rupees or paise?

In paise, the smallest currency unit. ₹500 is sent as 50000. Take the amount from your own database, not from the browser.

Can I use Razorpay without a registered business?

Test mode works while you build. Live payments need Razorpay's KYC, and the documents required depend on your business type; check Razorpay's current requirements.

What does Razorpay charge?

Fees depend on the payment method and your plan, and they change. Check Razorpay's pricing page and your Razorpay Dashboard for the current rates.

Ready to take payments? Compare cPanel hosting, the App Platform or a VPS for your app, and open a ticket if you want help choosing where your integration should run.

Host your payment-ready site

Free SSL for your webhook URL, a choice of PHP versions and weekly backups on every shared hosting plan.

See hosting plans

Was this article helpful?

Your answer helps us decide what to improve next.

Still need help? Open a support ticket and our team will reply.

Prefer an app? Add this site to your home screen.Get the app