paymentservice/document/cashfree/05_webhooks_and_signature_verification.md
vickytechkey befd83ad53
All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 41s
docs: add Cashfree payment gateway documentation and integration guides
2026-10-18 21:11:24 +05:30

7.8 KiB

Cashfree Payments - Webhooks & Signature Verification

1. Webhooks Architecture

Webhooks are automated HTTP POST requests sent from Cashfree to your backend when critical transaction events occur. They serve as the single source of truth for transaction states (such as asynchronous UPI confirmation, Netbanking approval, or 3DS completion).

Important

Never fulfill orders solely based on frontend redirection or query parameters. Always verify the transaction status via Webhooks or the server-to-server GET /orders/{order_id} API.


2. Webhook Event Types

Cashfree emits events categorized by product:

Event Type (type) Description
PAYMENT_SUCCESS_WEBHOOK Customer successfully paid; funds captured.
PAYMENT_FAILED_WEBHOOK Transaction declined by bank, insufficient funds, or wrong PIN.
PAYMENT_USER_DROPPED_WEBHOOK Customer exited checkout without completing payment.
REFUND_STATUS_WEBHOOK Refund status changed (SUCCESS, FAILED, CANCELLED).
SETTLEMENT_SUCCESS_WEBHOOK Settlement transferred to merchant bank account.
DISPUTE_CREATED_WEBHOOK Customer or bank raised a chargeback dispute.

3. Webhook Payload Structure

Sample PAYMENT_SUCCESS_WEBHOOK

{
  "data": {
    "order": {
      "order_id": "tradhox_ord_987654",
      "order_amount": 1499.00,
      "order_currency": "INR",
      "order_tags": null
    },
    "payment": {
      "cf_payment_id": "291847101",
      "payment_status": "SUCCESS",
      "payment_amount": 1499.00,
      "payment_currency": "INR",
      "payment_message": "00::Transaction Success",
      "payment_time": "2026-10-18T21:06:12Z",
      "payment_completion_time": "2026-10-18T21:06:18Z",
      "bank_reference": "629184719283",
      "auth_id": null,
      "payment_method": {
        "upi": {
          "channel": "intent",
          "upi_id": "user@okaxis"
        }
      },
      "payment_group": "upi"
    },
    "customer_details": {
      "customer_name": "Vignesh Tech",
      "customer_id": "tradhox_cust_42",
      "customer_email": "vignesh@tradhox.com",
      "customer_phone": "9876543210"
    }
  },
  "event_time": "2026-10-18T21:06:20Z",
  "type": "PAYMENT_SUCCESS_WEBHOOK"
}

4. Cryptographic Signature Verification

Every Cashfree webhook includes two security headers:

  • x-webhook-signature: Base64 encoded HMAC-SHA256 signature string.
  • x-webhook-timestamp: Epoch timestamp in milliseconds (or ISO format) when webhook was dispatched.

Verification Algorithm

1. Read raw request body bytes (do NOT parse or re-serialize JSON before hashing!).
2. Concatenate: signed_data = x-webhook-timestamp + raw_body
3. Compute HMAC-SHA256 using your Cashfree Secret Key (client_secret).
4. Base64 encode the resulting hash.
5. Compare the computed signature with x-webhook-signature using timing-safe comparison.
flowchart LR
    A[x-webhook-timestamp] --> C[Concatenate]
    B[Raw Request Body Bytes] --> C
    C --> D[HMAC-SHA256 with Secret Key]
    D --> E[Base64 Encode]
    E --> F{Constant-time Comparison with x-webhook-signature}
    F -->|Match| G[Process Webhook]
    F -->|Mismatch| H[Reject 400 Bad Request]

5. Production-Ready Python / Django Implementation

Save this verification helper in your service (e.g. payments/utils.py):

import hmac
import hashlib
import base64
import time
import logging

logger = logging.getLogger(__name__)

def verify_cashfree_webhook_signature(
    raw_body: bytes,
    signature: str,
    timestamp: str,
    secret_key: str,
    max_drift_seconds: int = 300
) -> bool:
    """
    Verifies Cashfree webhook signature using HMAC-SHA256.
    
    Args:
        raw_body: Exact raw bytes from request.body
        signature: Value from x-webhook-signature header
        timestamp: Value from x-webhook-timestamp header
        secret_key: Cashfree Merchant Client Secret Key
        max_drift_seconds: Allowed time difference in seconds (anti-replay)
        
    Returns:
        bool: True if signature is authentic and timestamp is fresh, False otherwise
    """
    if not signature or not timestamp or not secret_key:
        logger.warning("Missing required fields for webhook signature verification")
        return False

    # 1. Anti-Replay Check (5 minutes threshold)
    try:
        ts_val = int(timestamp)
        # If timestamp is in milliseconds, convert to seconds
        if ts_val > 10000000000:
            ts_val = ts_val // 1000
        current_time = int(time.time())
        if abs(current_time - ts_val) > max_drift_seconds:
            logger.warning(f"Webhook timestamp expired: drift={abs(current_time - ts_val)}s")
            return False
    except ValueError:
        logger.warning(f"Invalid timestamp format: {timestamp}")
        return False

    # 2. Build signed payload: timestamp + raw_body
    ts_bytes = str(timestamp).encode('utf-8')
    payload = ts_bytes + raw_body

    # 3. Compute HMAC-SHA256
    computed_hmac = hmac.new(
        key=secret_key.encode('utf-8'),
        msg=payload,
        digestmod=hashlib.sha256
    ).digest()

    computed_signature = base64.b64encode(computed_hmac).decode('utf-8')

    # 4. Constant-time comparison
    is_valid = hmac.compare_digest(computed_signature, signature)
    if not is_valid:
        logger.warning("Webhook signature mismatch!")
    return is_valid

Complete Django Webhook View Example

from django.views.decorators.csrf import csrf_exempt
from django.http import JsonResponse, HttpResponse
from django.conf import settings
import json
import logging
from .utils import verify_cashfree_webhook_signature

logger = logging.getLogger(__name__)

@csrf_exempt
def cashfree_webhook_view(request):
    if request.method != "POST":
        return HttpResponse(status=405)

    signature = request.headers.get("x-webhook-signature")
    timestamp = request.headers.get("x-webhook-timestamp")
    secret_key = getattr(settings, "CASHFREE_SECRET_KEY", "")

    # Verify signature
    if not verify_cashfree_webhook_signature(request.body, signature, timestamp, secret_key):
        return JsonResponse({"error": "Invalid signature"}, status=400)

    try:
        payload = json.loads(request.body.decode("utf-8"))
        event_type = payload.get("type")
        data = payload.get("data", {})

        order_info = data.get("order", {})
        payment_info = data.get("payment", {})
        order_id = order_info.get("order_id")

        logger.info(f"Received Cashfree Webhook: {event_type} for order {order_id}")

        if event_type == "PAYMENT_SUCCESS_WEBHOOK":
            # Idempotent check: Ensure order is not already marked paid
            # fulfill_order(order_id, payment_info)
            pass
        elif event_type == "PAYMENT_FAILED_WEBHOOK":
            # record_payment_failure(order_id, payment_info)
            pass
        elif event_type == "REFUND_STATUS_WEBHOOK":
            # update_refund_status(data)
            pass

        # Return 200 OK immediately
        return JsonResponse({"status": "acknowledged"}, status=200)

    except Exception as e:
        logger.error(f"Error processing webhook: {e}", exc_info=True)
        return JsonResponse({"status": "internal_error"}, status=500)

6. Retries and Delivery Guarantee

  • Cashfree requires your server to respond with HTTP 200 OK within 5 seconds.
  • If your server responds with an error code (5xx, 4xx) or times out, Cashfree retries up to 7 times with exponential backoff:
    • 1st retry: after 1 minute
    • 2nd retry: after 5 minutes
    • 3rd retry: after 15 minutes
    • 4th retry: after 30 minutes
    • 5th retry: after 1 hour
    • 6th retry: after 2 hours
    • 7th retry: after 4 hours
  • Always design webhook handlers to be strictly idempotent by verifying against the database before executing side-effects.