Hosted Checkout
Redirect customers to the secure Paymora-hosted payment page.
Universal hosted-payment integration documentation for PHP, Node.js, Python, Java, .NET, and any backend capable of sending HTTP redirects and receiving HTTPS webhooks.
Redirect customers to the secure Paymora-hosted payment page.
Confirm payments using signed server-to-server notifications.
Protect your system from duplicate webhooks and double fulfilment.
How the Paymora hosted gateway works.
Paymora is a hosted payment gateway. Your backend creates an order, stores the payable amount, and redirects the customer to the Paymora payment page.
After payment processing, Paymora sends a signed server-to-server webhook to your backend. Your application must verify that webhook before marking an order as paid.
https://paymora.co.in/gateway/pay.php
Required merchant configuration values.
| Credential | Purpose | Exposure |
|---|---|---|
client_id |
Identifies your Paymora merchant account when starting a payment. | May be included in the payment request. |
webhook_secret |
Used to verify webhook HMAC SHA256 signatures. | Private. Backend only. Never expose it publicly. |
webhook_url |
Public HTTPS endpoint that receives Paymora notifications. | Public URL, but must verify every request. |
PAYMORA_PAYMENT_URL=https://paymora.co.in/gateway/pay.php
PAYMORA_CLIENT_ID=YOUR_CLIENT_ID
PAYMORA_WEBHOOK_SECRET=YOUR_WEBHOOK_SECRET
PAYMORA_SUCCESS_URL=https://example.com/payment/success
PAYMORA_FAIL_URL=https://example.com/payment/failed
Recommended end-to-end processing sequence.
Required parameters for starting a payment.
| Parameter | Type | Required | Description |
|---|---|---|---|
client_id |
String | Required | Merchant/client ID issued by Paymora. |
order_id |
String | Required | Unique merchant-generated order identifier. |
amount |
Number | Required | Final backend-calculated payable amount. |
success_url |
HTTPS URL | Required | Customer-facing redirect after payment flow. |
fail_url |
HTTPS URL | Required | Customer-facing failure or expiry redirect. |
https://paymora.co.in/gateway/pay.php?client_id=YOUR_CLIENT_ID&order_id=ORDER_123456&amount=1500.00&success_url=https%3A%2F%2Fexample.com%2Fpayment%2Fsuccess&fail_url=https%3A%2F%2Fexample.com%2Fpayment%2Ffailed
order_id and calculate
amount on the backend. Save both before
redirecting the customer.
Receive trusted server-to-server payment updates.
https://yourdomain.com/payments/paymora-webhook
{
"txn_id": "TXN2609YsH01Hs54Y6d587F8D73a1JYuvC",
"order_id": "ORDER_123456",
"amount": 1500,
"status": "approved"
}
| Field | Type | Description |
|---|---|---|
txn_id |
String | Unique Paymora transaction identifier. |
order_id |
String | Merchant order ID originally sent to Paymora. |
amount |
Number | Processed payment amount. |
status |
String | Payment processing status. |
| Status | Meaning | Required action |
|---|---|---|
| approved | Payment approved. | Verify order and amount, then mark paid. |
| declined | Payment declined. | Do not provide paid services. |
| expired | Payment session expired. | Do not provide paid services. |
Authenticate incoming Paymora webhook requests.
Paymora sends the signature in this HTTP header:
X-Genext-Signature: SIGNATURE_VALUE
The signature is calculated as:
HMAC-SHA256(raw_webhook_payload, webhook_secret)
X-Genext-Signature header.Safe payment confirmation sequence.
rawBody = readExactRawRequestBody()
receivedSignature = getHeader("X-Genext-Signature")
if receivedSignature is empty:
return HTTP 401
expectedSignature = HMAC_SHA256_HEX(
rawBody,
WEBHOOK_SECRET
)
if constantTimeCompare(
receivedSignature,
expectedSignature
) is false:
return HTTP 401
payload = parseJSON(rawBody)
validate:
txn_id
order_id
amount
status
begin database transaction
order = findAndLockOrder(payload.order_id)
if order does not exist:
rollback
return HTTP 404
if order.payment_status == "paid":
commit
return HTTP 200 "OK"
if payload.status != "approved":
save gateway status
commit
return HTTP 200 "OK"
if payload.amount != order.payable_amount:
mark for manual review
commit
return HTTP 200 "OK"
if payload.txn_id belongs to another order:
rollback
return HTTP 409
mark order paid
save txn_id
save paid_amount
deliver purchased service
insert audit ledger
commit
return HTTP 200 "OK"
Prevent duplicate crediting and fulfilment.
Paymora may send the same webhook more than once. Your
webhook must safely return OK without repeating
a payment operation.
Compare the webhook amount with your saved order.
Always compare the verified webhook amount against the
payable_amount saved before redirecting to
Paymora.
| Source | Amount |
|---|---|
| Merchant database | ₹1,500.00 |
| Verified webhook | ₹1,400.00 |
For reliable comparisons, use decimal money values or convert both amounts to integer paise.
Create a payment and process the webhook in PHP.
<?php
define(
'PAYMORA_PAYMENT_URL',
'https://paymora.co.in/gateway/pay.php'
);
define(
'PAYMORA_CLIENT_ID',
getenv('PAYMORA_CLIENT_ID')
);
$orderId =
'ORDER_' .
date('YmdHis') .
'_' .
bin2hex(random_bytes(4));
$payableAmount = '1500.00';
/*
Save before redirecting:
order_id = $orderId
payable_amount = $payableAmount
payment_status = pending
*/
$paymentUrl =
PAYMORA_PAYMENT_URL .
'?' .
http_build_query([
'client_id' => PAYMORA_CLIENT_ID,
'order_id' => $orderId,
'amount' => $payableAmount,
'success_url' =>
'https://example.com/payment/success.php',
'fail_url' =>
'https://example.com/payment/failed.php'
]);
header('Location: ' . $paymentUrl);
exit;
<?php
$webhookSecret =
getenv('PAYMORA_WEBHOOK_SECRET');
$rawPayload =
file_get_contents('php://input');
$receivedSignature =
$_SERVER['HTTP_X_GENEXT_SIGNATURE'] ?? '';
$expectedSignature = hash_hmac(
'sha256',
$rawPayload,
$webhookSecret
);
if (
$receivedSignature === '' ||
!hash_equals(
$expectedSignature,
$receivedSignature
)
) {
http_response_code(401);
exit('Invalid signature');
}
$data = json_decode($rawPayload, true);
if (
!is_array($data) ||
!isset(
$data['txn_id'],
$data['order_id'],
$data['amount'],
$data['status']
)
) {
http_response_code(400);
exit('Invalid payload');
}
$txnId = trim((string)$data['txn_id']);
$orderId = trim((string)$data['order_id']);
$status = trim((string)$data['status']);
$paidAmount = (string)$data['amount'];
if (
$txnId === '' ||
$orderId === '' ||
!is_numeric($paidAmount)
) {
http_response_code(400);
exit('Invalid fields');
}
/*
Use a database transaction here:
1. SELECT order FOR UPDATE.
2. Check whether already paid.
3. Verify status = approved.
4. Compare amount.
5. Save txn_id and paid_amount.
6. Mark paid.
7. Deliver service.
8. COMMIT.
*/
http_response_code(200);
exit('OK');
Express example using a raw webhook request body.
const crypto = require('crypto');
const express = require('express');
const app = express();
const paymentUrl =
'https://paymora.co.in/gateway/pay.php';
app.post('/payments/create', async (req, res) => {
const orderId =
'ORDER_' +
Date.now() +
'_' +
crypto.randomBytes(4).toString('hex');
const amount = '1500.00';
// Save orderId, amount and pending status first.
const params = new URLSearchParams({
client_id: process.env.PAYMORA_CLIENT_ID,
order_id: orderId,
amount: amount,
success_url:
'https://example.com/payment/success',
fail_url:
'https://example.com/payment/failed'
});
res.redirect(
paymentUrl + '?' + params.toString()
);
});
app.post(
'/payments/paymora-webhook',
express.raw({
type: 'application/json'
}),
async (req, res) => {
const rawPayload = req.body;
const received =
req.get('X-Genext-Signature') || '';
const expected = crypto
.createHmac(
'sha256',
process.env.PAYMORA_WEBHOOK_SECRET
)
.update(rawPayload)
.digest('hex');
const receivedBuffer =
Buffer.from(received, 'utf8');
const expectedBuffer =
Buffer.from(expected, 'utf8');
if (
receivedBuffer.length !==
expectedBuffer.length ||
!crypto.timingSafeEqual(
receivedBuffer,
expectedBuffer
)
) {
return res
.status(401)
.send('Invalid signature');
}
let payload;
try {
payload = JSON.parse(
rawPayload.toString('utf8')
);
} catch (error) {
return res
.status(400)
.send('Invalid JSON');
}
/*
Validate fields and process inside
a database transaction.
*/
return res.status(200).send('OK');
}
);
express.raw() before any global
express.json() middleware that could parse the
body.
Flask create-payment and webhook examples.
import os
import secrets
import time
from urllib.parse import urlencode
from flask import Flask, redirect
app = Flask(__name__)
PAYMORA_PAYMENT_URL = (
"https://paymora.co.in/gateway/pay.php"
)
@app.post("/payments/create")
def create_payment():
order_id = (
f"ORDER_{int(time.time())}_"
f"{secrets.token_hex(4)}"
)
amount = "1500.00"
# Save order before redirecting.
parameters = {
"client_id":
os.environ["PAYMORA_CLIENT_ID"],
"order_id": order_id,
"amount": amount,
"success_url":
"https://example.com/payment/success",
"fail_url":
"https://example.com/payment/failed"
}
return redirect(
PAYMORA_PAYMENT_URL +
"?" +
urlencode(parameters)
)
import os
import hmac
import hashlib
import json
from flask import request, Response
@app.post("/payments/paymora-webhook")
def paymora_webhook():
raw_payload = request.get_data(
cache=False,
as_text=False
)
received = request.headers.get(
"X-Genext-Signature",
""
)
expected = hmac.new(
os.environ[
"PAYMORA_WEBHOOK_SECRET"
].encode("utf-8"),
raw_payload,
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(
received,
expected
):
return Response(
"Invalid signature",
status=401
)
try:
data = json.loads(raw_payload)
except Exception:
return Response(
"Invalid JSON",
status=400
)
# Validate and process in a transaction.
return Response("OK", status=200)
Spring Boot webhook signature verification.
@PostMapping("/payments/paymora-webhook")
public ResponseEntity<String> webhook(
@RequestBody byte[] rawPayload,
@RequestHeader(
value = "X-Genext-Signature",
required = false
) String receivedSignature
) {
try {
if (
receivedSignature == null ||
receivedSignature.isEmpty()
) {
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body("Invalid signature");
}
String webhookSecret =
System.getenv(
"PAYMORA_WEBHOOK_SECRET"
);
Mac mac = Mac.getInstance(
"HmacSHA256"
);
mac.init(
new SecretKeySpec(
webhookSecret.getBytes(
StandardCharsets.UTF_8
),
"HmacSHA256"
)
);
byte[] hash =
mac.doFinal(rawPayload);
StringBuilder hex =
new StringBuilder();
for (byte value : hash) {
hex.append(
String.format("%02x", value)
);
}
byte[] expected =
hex.toString().getBytes(
StandardCharsets.UTF_8
);
byte[] received =
receivedSignature.getBytes(
StandardCharsets.UTF_8
);
if (
!MessageDigest.isEqual(
expected,
received
)
) {
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body("Invalid signature");
}
// Decode JSON only after verification.
// Validate order, status and amount.
// Process inside a transaction.
return ResponseEntity.ok("OK");
} catch (Exception exception) {
return ResponseEntity
.status(
HttpStatus.INTERNAL_SERVER_ERROR
)
.body("Processing error");
}
}
Customer-facing redirect page requirements.
The success page may display:
success_url does not prove payment was
approved. The customer can manually open this page.
The failure page may display:
Do not update the order as paid from either redirect page.
Auditable payment-order storage.
CREATE TABLE payment_orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
order_id VARCHAR(100) NOT NULL UNIQUE,
payable_amount DECIMAL(12,2) NOT NULL,
paid_amount DECIMAL(12,2) NULL,
txn_id VARCHAR(150) NULL UNIQUE,
payment_status VARCHAR(30)
NOT NULL DEFAULT 'pending',
gateway_status VARCHAR(30) NULL,
mismatch_reason VARCHAR(255) NULL,
created_at DATETIME
NOT NULL DEFAULT CURRENT_TIMESTAMP,
paid_at DATETIME NULL,
updated_at DATETIME
NOT NULL DEFAULT CURRENT_TIMESTAMP
ON UPDATE CURRENT_TIMESTAMP
);
order_id = ORDER_123456
payable_amount = 1500.00
paid_amount = NULL
txn_id = NULL
payment_status = pending
order_id = ORDER_123456
payable_amount = 1500.00
paid_amount = 1500.00
txn_id = TXN2609YsH01Hs54Y6d587F8D73a1JYuvC
payment_status = paid
gateway_status = approved
paid_at = 2026-08-17 12:30:05
DECIMAL for stored money. Do not use
FLOAT or DOUBLE for financial
values.
Mandatory production safeguards.
Validate payment behavior before production.
| Test | Expected result |
|---|---|
| Approved payment | Order is marked paid exactly once. |
| Duplicate webhook | No duplicate credit or fulfilment. |
| Invalid signature | HTTP 401; database remains unchanged. |
| Missing signature | HTTP 401. |
| Invalid JSON | HTTP 400. |
| Unknown order ID | No payment update; event is logged. |
| Amount mismatch | Order is not marked paid. |
| Declined payment | No service is delivered. |
| Expired payment | No service is delivered. |
| Database failure | Transaction is rolled back. |
| Manual success-page access | Order is not marked paid. |
Common integration problems and solutions.
X-Genext-Signature header.Final review before enabling live payments.