generated from vignesh/searchengine
All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 41s
7.8 KiB
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 OKwithin 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.