โ† Back to list

๐Ÿš€ 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โ€ฆ

Sourabh Panchal ยท 2025-08-17 02:36 ยท 0 claps ยท 4.7 min read
#web-development #payment-gateway #static-site #secure-web-gateway #cloudflare
Open on Medium โ†—
Wiki topics: FIN ยท Fintech & Banking ๐ŸŒ ยท Web Development ๐Ÿ”“ ยท Open Source

๐Ÿš€ 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

  1. Frontend (GitHub Pages)
  • Displays product/order form.
  • Submits order details to a Cloudflare Worker.
  • Redirects user to PayU payment page with required fields.
  1. Cloudflare Worker 1 (payment-worker)
  • Generates secure hash using PayUโ€™s key + salt.
  • Returns the hash to the frontend.
  1. 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.
  1. 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:

  1. In your Worker project, open wrangler.toml.
  2. 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"
  1. 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.

  1. Create a KV namespace:
npx wrangler kv:namespace create "PAYMENT_DATA"
  1. Copy the returned binding into your wrangler.toml:
[[kv_namespaces]]
binding = "PAYMENT_DATA"
id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  1. 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