๐ How I Integrated PayU Payment Gateway with GitHub Pages using Cloudflare Workers
One common challenge with static hosting (like GitHub Pages) is setting up dynamic features like a payment gateway. Payment providers likeโฆ
๐ How I Integrated PayU Payment Gateway with GitHub Pages using Cloudflare Workers
One common challenge with static hosting (like GitHub Pages) is setting up dynamic features like a payment gateway. Payment providers like PayU require server-side hashing and secure verification, which static hosting cannot handle directly.
To solve this, I used Cloudflare Workers (serverless edge functions) to act as a secure backend, while my frontend runs on GitHub Pages.
This post explains how I connected PayU with my static frontend using Cloudflare Workers, complete with code examples you can adapt.

โก Architecture Overview
- Frontend (GitHub Pages)
- Displays product/order form.
- Submits order details to a Cloudflare Worker.
- Redirects user to PayU payment page with required fields.
- Cloudflare Worker 1 (
payment-worker)
- Generates secure hash using PayUโs key + salt.
- Returns the hash to the frontend.
- Cloudflare Worker 2 (
payment-result-worker)
- Receives PayUโs payment result.
- Stores it temporarily in KV storage.
- Signs the result with HMAC to prevent tampering.
- Redirects the user back to a payment status page hosted on GitHub Pages.
- Frontend (status page)
- Fetches verification details from Worker 2.
- Displays success/failure to the user securely.
๐ Step 1: Hidden PayU Form
Add this hidden form below the actual form on your frontend so that it gets filled dynamically with transaction data and submitted to PayU:
<!-- Hidden PayU form -->
<form id="payuForm" action="https://secure.payu.in/_payment" method="post" style="display:none;">
<input type="hidden" name="key">
<input type="hidden" name="txnid">
<input type="hidden" name="amount">
<input type="hidden" name="productinfo">
<input type="hidden" name="firstname">
<input type="hidden" name="email">
<input type="hidden" name="phone">
<input type="hidden" name="surl" value="https://payment-result.example.workers.dev">
<input type="hidden" name="furl" value="https://payment-result.example.workers.dev">
<input type="hidden" name="hash">
</form>
2. ๐ Step 2: Sending Order Details to Cloudflare Worker
When the user clicks Buy Now, JavaScript collects their details, calls the Cloudflare Worker, gets the PayU hash, and submits the hidden form.
async function submitOrder() {
const name = document.getElementById("userName").value.trim();
const phone = document.getElementById("userPhone").value.trim();
const email = document.getElementById("userEmail").value.trim();
const total = parseFloat(document.getElementById("totalPrice").textContent.replace(/[^\d.]/g, "")).toFixed(2);
const txnid = "TXN" + Date.now();
const productInfo = "Custom Device - Android 14";
try {
// Step 1: Get hash from Cloudflare Worker
const res = await fetch("https://payment.example.workers.dev", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
txnid,
amount: total,
productinfo: productInfo,
firstname: name,
email: email
})
});
const data = await res.json();
// Step 2: Create PayU form dynamically
const payuForm = document.createElement("form");
payuForm.action = "https://test.payu.in/_payment";
payuForm.method = "post";
const fields = {
key: data.key,
txnid: txnid,
amount: total,
productinfo: productInfo,
firstname: name,
email: email,
phone: phone,
surl: "https://payment-result.example.workers.dev",
furl: "https://payment-result.example.workers.dev",
hash: data.hash
};
for (const key in fields) {
const input = document.createElement("input");
input.type = "hidden";
input.name = key;
input.value = fields[key];
payuForm.appendChild(input);
}
document.body.appendChild(payuForm);
payuForm.submit();
} catch (err) {
console.error("Error:", err);
alert("Something went wrong. Please try again.");
}
}
๐ Step 3: Cloudflare Worker โ Generate Hash
PayU requires a SHA-512 hash of the transaction details. Since keys and salts must not be exposed in the frontend, we use a Worker:
export default {
async fetch(request, env) {
const corsHeaders = {
"Access-Control-Allow-Origin": "https://yourwebsite.com",
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
};
if (request.method === "OPTIONS") {
return new Response(null, { headers: corsHeaders });
}
if (request.method === "POST") {
const body = await request.json();
let { txnid, amount, productinfo, firstname, email } = body;
const key = env.PAYU_KEY;
const salt = env.PAYU_SALT;
const hashString = `${key}|${txnid}|${amount}|${productinfo}|${firstname}|${email}|||||||||||${salt}`;
const hashBuffer = await crypto.subtle.digest("SHA-512", new TextEncoder().encode(hashString));
const hashHex = Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, "0")).join("");
return new Response(JSON.stringify({ key, hash: hashHex }), {
headers: { ...corsHeaders, "Content-Type": "application/json" }
});
}
return new Response("Method Not Allowed", { status: 405, headers: corsHeaders });
}
};
Environment Variables setup
๐ Setting up Environment Variables in Cloudflare Workers
Since your PayU Key and Salt must never be hardcoded, we use Worker environment variables:
- In your Worker project, open
wrangler.toml. - Add your live and test credentials:
[vars]
PAYU_KEY = "your_live_key_here"
PAYU_SALT = "your_live_salt_here"
PAYU_KEY_TEST = "your_test_key_here"
PAYU_SALT_TEST = "your_test_salt_here"
SECRET_KEY = "your_random_secret_for_signing"
DEBUG = "true"
- Deploy with:
npx wrangler deploy
Now, these values are available inside the Worker as env.PAYU_KEY, env.PAYU_SALT, etc.
KV Storage setup
๐๏ธ Setting up Cloudflare KV for Payment Data
To store temporary payment results securely (before showing them on the frontend), we use Cloudflare KV.
- Create a KV namespace:
npx wrangler kv:namespace create "PAYMENT_DATA"
- Copy the returned binding into your
wrangler.toml:
[[kv_namespaces]]
binding = "PAYMENT_DATA"
id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
- Now you can use it in Workers:
await env.PAYMENT_DATA.put("TXN12345", JSON.stringify({ status: "success" }), {
expirationTtl: 300 // auto-delete after 5 minutes
});
This ensures payment data isnโt forged or replayed.
๐ Step 4: Cloudflare Worker โ Payment Result Handling
Once PayU finishes the transaction, it calls our success/failure URL. We process it securely:
export default {
async fetch(request, env) {
const url = new URL(request.url);
// === Verification Endpoint ===
if (url.pathname === "/verify") {
const txnid = url.searchParams.get("txnid");
const sig = url.searchParams.get("sig");
const stored = await env.PAYMENT_DATA.get(txnid);
if (!stored) return jsonResponse({ verified: false, error: "No record found" });
const data = JSON.parse(stored);
const payload = `${data.txnid}|${data.amount}|${data.productinfo}|${data.status}`;
const sigHex = await hmacSha256Hex(payload, env.SECRET_KEY);
return jsonResponse({ verified: sigHex === sig, ...data });
}
// === PayU POST Notification ===
if (request.method === "POST") {
const form = await request.formData();
const txnid = form.get("txnid");
const amount = form.get("amount");
const productinfo = form.get("productinfo");
const status = form.get("status");
await env.PAYMENT_DATA.put(
txnid,
JSON.stringify({ txnid, amount, productinfo, status }),
{ expirationTtl: 300 }
);
const payload = `${txnid}|${amount}|${productinfo}|${status}`;
const sigHex = await hmacSha256Hex(payload, env.SECRET_KEY);
const redirectUrl = `https://yourwebsite.com/payment-status.html` +
`?txnid=${encodeURIComponent(txnid)}&amount=${encodeURIComponent(amount)}` +
`&productinfo=${encodeURIComponent(productinfo)}&status=${encodeURIComponent(status)}` +
`&sig=${encodeURIComponent(sigHex)}`;
return Response.redirect(redirectUrl, 302);
}
return new Response("Not Allowed", { status: 405 });
}
};
async function hmacSha256Hex(message, secret) {
const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret),
{ name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(message));
return Array.from(new Uint8Array(sig)).map(b => b.toString(16).padStart(2, "0")).join("");
}
function jsonResponse(obj) {
return new Response(JSON.stringify(obj), {
headers: { "Content-Type": "application/json" }
});
}
๐ Step 5: Frontend Status Page
The frontend loads transaction info, verifies it with our Worker, and shows a success/failure screen:
const params = new URLSearchParams(window.location.search);
const txnid = params.get("txnid");
const sig = params.get("sig");
fetch(`https://payment-result.example.workers.dev/verify?txnid=${txnid}&sig=${sig}`)
.then(res => res.json())
.then(data => {
if (data.verified && data.status === "success") {
document.getElementById("status-heading").textContent = "โ
Payment Successful";
document.getElementById("status-message").textContent = `Amount: โน${data.amount} for ${data.productinfo}`;
} else {
document.getElementById("status-heading").textContent = "โ Payment Failed";
document.getElementById("status-message").textContent = "If amount was deducted, it will be refunded.";
}
});
โ Why Cloudflare Workers?
- GitHub Pages = static only (no backend).
- PayU requires secure server-side hash & verification.
- Cloudflare Workers = free, serverless, edge compute.
- Easy to integrate with KV Storage for temporary data.
- Ensures secure signing (no exposing keys in frontend).
๐ฏ Final Thoughts
With this setup:
- My frontend (GitHub Pages) stays static and simple.
- Cloudflare Workers provide the necessary secure backend.
- Payments are verified and safe against forgery.
This approach works not just with PayU, but with any payment gateway that requires server-side signing.
๐จโ๐ปDeployment tip
๐ Deploying Workers
- To publish your Worker:
npx wrangler deploy
- To test locally:
npx wrangler dev
- To bind KV and secrets after changes:
npx wrangler secret put PAYU_KEY
npx wrangler secret put PAYU_SALT ๋ฉํ๋ฐ์ดํฐ
- post_id
- b6522016e0ae
- slug
- how-i-integrated-payu-payment-gateway-with-github-pages-using-cloudflare-workers-b6522016e0ae
- url
- https://medium.com/@IamCOD3X/how-i-integrated-payu-payment-gateway-with-github-pages-using-cloudflare-workers-b6522016e0ae
- canonical_url
- https://medium.com/@IamCOD3X/how-i-integrated-payu-payment-gateway-with-github-pages-using-cloudflare-workers-b6522016e0ae
- author_url
- https://medium.com/@IamCOD3X
- status
- ok
- fetched_at
- 2026-06-25 07:00:49