Webhooks & Automation October 2026

Handling Razorpay Webhooks in Python: Verification, Idempotency & Zero Missed Payments

A missed payment.captured webhook means your customer paid but got nothing. Learn how to verify Razorpay HMAC signatures, prevent duplicate executions, and handle webhooks reliably in Python without running a server.

L
LiteLambda Team
9 min read

When a customer completes a checkout on Razorpay, relying solely on client-side frontend callbacks (handler in Razorpay Checkout.js) is a recipe for lost revenue. If the user closes their browser tab, loses mobile connectivity, or gets redirected improperly after entering their UPI PIN, your frontend never fires.

The only source of truth for payment status is the Razorpay Webhook.

However, handling payment webhooks comes with strict production requirements:
1. Missed events mean angry customers: If your webhook endpoint is down or crashes, the customer has paid money, but their subscription is never activated or order is never fulfilled.
2. Signature verification is non-negotiable: Anyone can forge an HTTP POST to your webhook URL claiming payment.captured. You must verify Razorpay's HMAC SHA256 signature.
3. Razorpay retries for 24 hours: If your server takes more than 5 seconds to reply or returns a non-200 code, Razorpay retries the same event multiple times with exponential backoff. Your handler must be idempotent to prevent double-crediting users.

Here is how to build and deploy an enterprise-grade, serverless Razorpay webhook handler in Python.


Razorpay Webhook Lifecycle

Customer Checkout (UPI / Card / Netbanking)
                     │
                     ▼
          Razorpay Payment Engine
                     │
         [payment.captured event]
                     │
         POST /w/razorpay-handler (HMAC Signed)
                     │
          LiteLambda Serverless Webhook
          ├─► 1. Verify X-Razorpay-Signature (HMAC-SHA256)
          ├─► 2. Check Idempotency Key (Skip if already processed)
          ├─► 3. Return 200 OK (< 100ms to avoid timeout)
          └─► 4. Grant access in DB, send WhatsApp/Email receipt

Verifying Razorpay Webhook Signatures in Python

Razorpay sends an X-Razorpay-Signature HTTP header containing an HMAC-SHA256 hash computed using your Webhook Secret and the raw JSON request body.

Here is the exact signature verification function in Python:

import hmac
import hashlib

def verify_razorpay_signature(raw_body: str, signature: str, secret: str) -> bool:
    """
    Verifies that the incoming webhook genuinely came from Razorpay.
    Prevents forged requests and replay tampering.
    """
    if not signature or not secret:
        return False

    expected_signature = hmac.new(
        key=secret.encode("utf-8"),
        msg=raw_body.encode("utf-8"),
        digestmod=hashlib.sha256
    ).hexdigest()

    # Use constant-time comparison to prevent timing attacks
    return hmac.compare_digest(expected_signature, signature)

The Complete Production Handler: Verification, Idempotency & Provisioning

The following script verifies the signature, enforces idempotency using a Key-Value store, and provisions access:

import os
import json
import requests
from verify import verify_razorpay_signature

def handler(event, context):
    """
    Handles Razorpay payment and subscription webhooks.
    """
    # 1. Extract raw body and headers
    raw_body = event.get("body", "")
    headers = event.get("headers", {})
    signature = headers.get("x-razorpay-signature") or headers.get("X-Razorpay-Signature")

    webhook_secret = os.environ.get("RAZORPAY_WEBHOOK_SECRET")
    if not webhook_secret:
        return {"status": 500, "error": "Server missing webhook secret"}

    # 2. Strict Signature Verification
    if not verify_razorpay_signature(raw_body, signature, webhook_secret):
        print(f"⚠️ Invalid signature attempt from IP: {event.get('ip')}")
        return {"status": 401, "error": "Signature verification failed"}

    # 3. Parse JSON payload
    try:
        data = json.loads(raw_body)
    except json.JSONDecodeError:
        return {"status": 400, "error": "Malformed JSON payload"}

    event_type = data.get("event")
    payload = data.get("payload", {})

    # 4. Handle 'payment.captured' (One-time payments)
    if event_type == "payment_intent.succeeded" or event_type == "payment.captured":
        payment_entity = payload.get("payment", {}).get("entity", {})
        payment_id = payment_entity.get("id")
        order_id = payment_entity.get("order_id")
        amount_paisa = payment_entity.get("amount")  # Razorpay sends amount in paisa (100 paisa = 1 INR)
        amount_inr = amount_paisa / 100 if amount_paisa else 0.0
        email = payment_entity.get("email")
        contact = payment_entity.get("contact")
        notes = payment_entity.get("notes", {})

        # ── Idempotency Check ────────────────────────────────────────────────
        # Prevent double fulfillment if Razorpay retries the same event
        idempotency_key = f"rzp_captured_{payment_id}"

        # Check built-in context KV or Redis
        if hasattr(context, "kv"):
            if context.kv.get(idempotency_key):
                print(f"ℹ️ Payment {payment_id} already processed. Skipping.")
                return {"status": 200, "message": "Already processed"}
            # Mark processed for 7 days
            context.kv.set(idempotency_key, "1", ttl=604800)

        print(f"✅ Processing payment {payment_id} for {amount_inr} INR from {email}")

        # 5. Provision User Access in your Main Database
        grant_customer_access(
            email=email,
            order_id=order_id,
            payment_id=payment_id,
            plan=notes.get("plan_id", "pro")
        )

        # 6. Send Instant Confirmation via WhatsApp / Email
        send_whatsapp_receipt(
            phone=contact,
            amount=amount_inr,
            payment_id=payment_id
        )

    # 7. Handle 'payment.failed'
    elif event_type == "payment.failed":
        payment_entity = payload.get("payment", {}).get("entity", {})
        payment_id = payment_entity.get("id")
        error_desc = payment_entity.get("error_description", "Unknown error")
        print(f"❌ Payment failed: {payment_id} - Reason: {error_desc}")
        # Log to analytics or trigger abandoned cart workflow

    # Always return 200 OK so Razorpay acknowledges delivery
    return {"status": 200, "received": True}


def grant_customer_access(email: str, order_id: str, payment_id: str, plan: str):
    """Call your main application API or direct DB to activate membership."""
    api_url = os.environ.get("APP_INTERNAL_API_URL")
    api_key = os.environ.get("APP_INTERNAL_API_KEY")

    if api_url and api_key:
        requests.post(
            f"{api_url}/v1/users/upgrade",
            headers={"Authorization": f"Bearer {api_key}"},
            json={"email": email, "order_id": order_id, "payment_id": payment_id, "plan": plan},
            timeout=8
        )

def send_whatsapp_receipt(phone: str, amount: float, payment_id: str):
    """Send transaction confirmation via Interakt / WATI / Gupshup."""
    # Custom WhatsApp BSP integration logic here
    pass

Why You Shouldn't Run Payment Webhooks on a Shared VPS

Payment webhooks require 99.99% availability. If your VPS rebooted for security patches or your Nginx proxy encountered a memory leak when a flash sale started, you lose customer payments.

On LiteLambda, webhook endpoints are isolated in serverless micro-sandboxes. They scale to thousands of simultaneous checkouts without server bottlenecks, verify signatures automatically, and store execution history with full request logs.

Deploy your Razorpay webhook on LiteLambda in 60s →


Razorpay Webhook Configuration: Step-by-Step

1. Deploy the Handler to LiteLambda

pip install litelambda-cli
litelambda login

litelambda deploy razorpay-webhook handler.py \
  --type webhook \
  --env RAZORPAY_WEBHOOK_SECRET="whsec_9vK3pLm8Nx2Q..." \
  --env APP_INTERNAL_API_URL="https://api.yourdomain.com" \
  --env APP_INTERNAL_API_KEY="sec_app_prod_xxxx"

You will get your unique live HTTPS endpoint:

https://run.litelambda.in/w/razorpay-webhook/live_sec_09fa84b

2. Configure Webhook in Razorpay Dashboard

  1. Log into your Razorpay Dashboard.
  2. In the left navigation, navigate to Account & Settings → Webhooks.
  3. Click + Add New Webhook.
  4. Paste your LiteLambda HTTPS URL into the Webhook URL field.
  5. In Secret, enter the identical secret string you set in your RAZORPAY_WEBHOOK_SECRET environment variable.
  6. Under Active Events, select:
  7. payment.authorized
  8. payment.captured
  9. payment.failed
  10. order.paid
  11. (Optional) subscription.charged if using Razorpay Subscriptions.
  12. Click Create Webhook.

Critical Best Practices for Payment Webhooks

1. Respect the 5-Second Timeout Window

Razorpay expects an HTTP 200 response within 5 seconds. If your handler takes longer because you are generating a complex PDF invoice synchronously or calling slow third-party APIs, Razorpay flags the delivery as failed and retries.

The Fix: Use LiteLambda's asynchronous execution or run PDF generation in a background job. LiteLambda returns an instant acknowledgement to Razorpay while processing the business logic safely in the background.

2. Always Store the payment_id for Idempotency

Razorpay guarantees at-least-once delivery. If a network blip occurs after your handler runs but before Razorpay receives your HTTP response, Razorpay sends the same webhook again. Always check if the payment_id has already been recorded before fulfilling the order.

3. Handle Amount in Paisa

Razorpay passes amounts in the smallest currency unit. For Indian Rupee (INR), amount: 49900 means ₹499.00. Never credit a user with ₹49,900 by accident.


Comparison: Where Should You Host Razorpay Webhooks?

Platform Setup Complexity Cold Starts Idempotency Support Cost
LiteLambda 60 seconds (litelambda deploy) Zero (< 30ms latency) Built-in KV store $4.99/mo flat
AWS Lambda + API Gateway High (IAM, Route53, API Gateway) 800ms–2s cold start Requires DynamoDB setup Pay-per-ms + Gateway fees
Self-Hosted VPS (EC2/Droplet) High (Nginx, SSL, UFW, Systemd) No cold start Requires Redis container $10–$20/mo idle cost
Heroku / Render Medium (Procfile, Dynos) Free tier sleeps (unacceptable) Requires Redis add-on $7–$25/mo

Summary

Handling payment events properly is the difference between smooth, automated operations and endless support tickets asking "I was debited ₹999, why is my account still on free?".

By combining Python's native HMAC verification with LiteLambda's zero-maintenance serverless execution, you get an unshakeable payment processing pipeline ready for any volume of transactions.

Deploy your payment webhook on LiteLambda →

Skip the infrastructure setup.

Run this exact code in our secure, isolated Docker sandbox. It takes 10 seconds to deploy.

Deploy this script in 60s →

No DevOps required.