Gareth Wilson Gareth Wilson

Guide to Cashfree Webhooks: Features and Best Practices

Published


Cashfree webhooks tell your systems when money moves or a verification finishes: a payment succeeds or fails, a refund or settlement changes status, a payout reaches the beneficiary bank or is reversed, a Video KYC session is reviewed, or a wallet is credited. If you collect payments, send payouts or verify customers with Cashfree in India, webhooks are how those asynchronous outcomes reach your order system, ledger or onboarding flow.

This guide covers how Cashfree webhooks are configured and signed across its products, how to tell events apart, how retries and resends work, and the practices that keep a Cashfree receiver reliable in production.

What are Cashfree webhooks?

Cashfree sends a JSON POST to a URL you register in the Merchant Dashboard when an event happens in one of its products. Four product families share one signing scheme:

  • Payment Gateway: payments, refunds, settlements, subscriptions, payment links, the token vault and disputes.
  • Payouts V2: transfers, beneficiary incidents and balance alerts.
  • Secure ID: Video KYC, bank account verification and reverse penny drop.
  • PPI: prepaid wallet credits.

Each delivery carries an x-webhook-timestamp header and an x-webhook-signature header. The signature is the base64 HMAC-SHA256 of the timestamp value immediately followed by the raw body, keyed with your client secret.

Two older formats don't follow that scheme. Payouts V1 Cashgram webhooks arrive as form-encoded parameters with a signature parameter computed over the sorted values. Legacy partner webhooks use x-cashfree-timestamp and x-cashfree-signature. Neither verifies with the code in this guide or with Hookdeck's Cashfree source.

Cashfree webhook features

FeatureDetails
ConfigurationMerchant Dashboard, per product: Add Webhook Endpoint, Test, then a retry policy and events
Signature headerx-webhook-signature
Timestamp headerx-webhook-timestamp
SchemeHMAC-SHA256, bare standard base64
Signed contentThe timestamp value, then the raw body, with no separator
SecretThe product's client secret. Partner webhooks use the Partner API key
Event fieldtype for Payment Gateway and Payouts V2, event_type for Secure ID and PPI
Success responseHTTP 200. Other codes, redirects included, count as failures
RetriesPayment Gateway: per-endpoint policy, default 3 retries at 2, 10 and 30 minutes. Payouts V2: a fixed schedule
Auto-disablePayouts V2 and Cashgram endpoints, after more than five consecutive failures
DeliveryAt-least-once, no ordering guarantee
ResendBatch Resend for Payment Gateway payment webhooks
Source IPsPublished for production and sandbox, on port 443

Common events

Payment Gateway's catalogue is large and versioned, so treat any list as a starting point and check the product reference for your webhook version.

ProductEventsFires when
Payment GatewayPAYMENT_SUCCESS_WEBHOOK, PAYMENT_FAILED_WEBHOOK, PAYMENT_USER_DROPPED_WEBHOOKA payment succeeds, fails, or the customer abandons it
Payment GatewayREFUND_STATUS_WEBHOOK, SETTLEMENT_STATUS_WEBHOOKA refund or settlement changes status
Payment GatewaySUBSCRIPTION_CREATED, SUBSCRIPTION_CHARGED, SUBSCRIPTION_CANCELLED, PAYMENT_LINK_EVENT, TOKEN_STATUS_WEBHOOKSubscription, payment link and token vault changes
Payouts V2TRANSFER_ACKNOWLEDGED, TRANSFER_SUCCESS, TRANSFER_FAILED, TRANSFER_REVERSED, TRANSFER_REJECTED, BULK_TRANSFER_REJECTEDA transfer moves through its lifecycle
Payouts V2BENEFICIARY_INCIDENT, CREDIT_CONFIRMATION, LOW_BALANCE_ALERTA bank or payment mode has an incident, funds are credited, or the balance falls below your threshold
Secure IDVKYC_USER_LINK_GENERATED through VKYC_AUDITOR_REVIEW_COMPLETEDA Video KYC session progresses
Secure IDBANK_ACCOUNT_VERIFICATION_SUCCESS, _REJECTED, _FAILED, and the RPD_BANK_ACCOUNT_VERIFICATION_* eventsA bank account verification or reverse penny drop finishes
PPIPPI_CREDIT_SUCCESSA wallet credit succeeds

The Payment Gateway reference names payment events with a _WEBHOOK suffix, such as PAYMENT_SUCCESS_WEBHOOK, while Cashfree's cross-product overview shows the same events as PAYMENT_SUCCESS. Accept both spellings until you have seen real deliveries for your webhook version.

Every body has the event name, an event_time and a data object. Here is Cashfree's Payouts V2 TRANSFER_SUCCESS example, trimmed:

{
  "data": {
    "transfer_id": "JUNOB2018",
    "cf_transfer_id": "123456",
    "status": "SUCCESS",
    "status_code": "SENT_TO_BENEFICIARY",
    "beneficiary_details": { "beneficiary_id": "JOHN18011" },
    "transfer_amount": 1,
    "transfer_mode": "BANK",
    "transfer_utr": "TESTR92023012200543116",
    "added_on": "2021-11-24T13:39:25Z",
    "updated_on": "2021-11-24T13:40:27Z"
  },
  "event_time": "2024-07-25T17:43:37",
  "type": "TRANSFER_SUCCESS"
}

Secure ID and PPI bodies use event_type instead of type, and Secure ID adds a version such as v1 or v2. Read type and fall back to event_type if one endpoint receives several products.

Setting up Cashfree webhooks

  1. In the Merchant Dashboard, open the developer settings of the product you're integrating, for example Payment Gateway > Developers > Webhooks.
  2. Click Add Webhook Endpoint, enter your HTTPS URL and click Test.
  3. Choose a retry policy and the events to subscribe to. For Payment Gateway, choose the webhook version too.
  4. Store the client secret for that product server-side, for example as CASHFREE_CLIENT_SECRET.

If you receive Cashfree webhooks through Hookdeck, use the Hookdeck source URL as the endpoint and create a source with the Cashfree type. The source asks you to:

Paste your Cashfree client secret (the Partner API key for partner webhooks). Legacy Payouts V1 and Cashgram webhooks use another scheme and fail verification.

The Cashfree source accepts POST only, which is how Cashfree delivers webhooks.

For Payment Gateway, orders can also carry a notify_url in order_meta on the Create Order API. Cashfree lists that as a separate NOTIFY_URL configuration whose retry settings can't be edited.

Securing Cashfree webhooks

Read x-webhook-timestamp and x-webhook-signature. Concatenate the timestamp value and the raw body bytes with nothing between them, compute an HMAC-SHA256 with your client secret as the key, base64-encode it, and compare it with the header in constant time. Base64 is case-sensitive, so compare it exactly.

const crypto = require('crypto');
const express = require('express');

function verifyCashfreeSignature(rawBody, signatureHeader, timestampHeader, clientSecret) {
  if (!signatureHeader || !timestampHeader || !clientSecret) return false;

  // The timestamp FIRST, then the raw body, with no separator.
  const expected = crypto
    .createHmac('sha256', clientSecret)
    .update(Buffer.concat([Buffer.from(timestampHeader, 'latin1'), rawBody]))
    .digest('base64');

  const received = Buffer.from(signatureHeader, 'utf8');
  const computed = Buffer.from(expected, 'utf8');
  return received.length === computed.length && crypto.timingSafeEqual(received, computed);
}

const app = express();

app.post('/webhooks/cashfree', express.raw({ type: '*/*' }), (req, res) => {
  const clientSecret = process.env.CASHFREE_CLIENT_SECRET;
  if (!clientSecret) return res.sendStatus(500); // fail closed

  const valid = verifyCashfreeSignature(
    req.body,
    req.get('x-webhook-signature'),
    req.get('x-webhook-timestamp'),
    clientSecret
  );
  if (!valid) return res.sendStatus(401);

  const event = JSON.parse(req.body.toString('utf8'));
  const eventName = event.type || event.event_type;
  // Deduplicate on the business ID, route on eventName, then process asynchronously.
  res.sendStatus(200);
});

The same check in Python:

import base64
import hashlib
import hmac


def verify_cashfree_signature(raw_body: bytes, signature_header, timestamp_header, client_secret: str) -> bool:
    if not signature_header or not timestamp_header or not client_secret:
        return False
    # The timestamp FIRST, then the raw body, with no separator.
    signed = timestamp_header.encode("latin-1") + raw_body
    expected = base64.b64encode(hmac.new(client_secret.encode("utf-8"), signed, hashlib.sha256).digest())
    return hmac.compare_digest(signature_header.encode("utf-8"), expected)


# In FastAPI: raw_body = await request.body()
# verify_cashfree_signature(raw_body, request.headers.get("x-webhook-signature"),
#                           request.headers.get("x-webhook-timestamp"),
#                           os.environ["CASHFREE_CLIENT_SECRET"])

Cashfree's manual Node sample builds the signed string from req.rawBody and compares with !==, which isn't constant-time. Its SDKs also offer a verification method, PGVerifyWebhookSignature in cashfree-pg.

Cashfree recommends rejecting requests whose timestamp is older than five minutes. The code above leaves that out, for two reasons. The main webhook pages don't state the timestamp's unit, although the partner onboarding example shows a 13-digit value, which is milliseconds. And Hookdeck applies no timestamp tolerance, while its retries and replays resend Cashfree's original headers, so a window on your endpoint would reject late ones. If you add a check, confirm the unit from a real delivery first.

To check your verifier, use the secret cashfree-guide-client-secret, an x-webhook-timestamp of 1746427759733 and this exact body:

{"data":{"order":{"order_id":"order_1234"}},"event_time":"2024-01-15T10:30:05+05:30","type":"PAYMENT_SUCCESS_WEBHOOK"}

The header should be:

x-webhook-signature: VmRnF962vy35oJ/V8XkNA7dMNiLmlX3uAbHvd0KZeQU=

Make Cashfree webhooks production-ready. Hookdeck Event Gateway verifies the x-webhook-signature header on every delivery, returns the 200 Cashfree expects, and queues each payment, payout and verification event until your service has processed it.

Cashfree webhook limitations and pain points

One secret per product, and rotation lag

The Problem: Verification starts failing after you rotate a client secret, or fails for one product but not another.

Why It Happens: Cashfree signs each webhook with the client secret that was active when the event was sent. Events already in flight when you rotate are still signed with the old one. Each product has its own credentials, and partner webhooks use the Partner API key. For Payouts V2, Cashfree's troubleshooting advice is to "always use the oldest active secret".

Workarounds:

  • Keep the previous secret active until in-flight webhooks have been processed, as Cashfree advises.
  • Point each product at its own endpoint or source, each with its own secret.
  • If you verify on your own server, try the new and the previous secret during a rotation.

How Hookdeck Can Help: Use one Hookdeck source per product, each holding that product's secret. Deliveries signed with a secret the source doesn't hold stay in Hookdeck's request log, marked as failed verification, so you can see what a rotation affected.

Legacy formats that never verify

The Problem: Some deliveries have no x-webhook-signature header, or a body of form fields instead of JSON.

Why It Happens: Payouts V1 Cashgram webhooks are form-encoded and carry a signature parameter computed over the sorted, concatenated values. Legacy partner webhooks are signed into x-cashfree-signature. Both differ from the header scheme.

Workarounds:

  • Move Payouts integrations to V2 webhooks. Cashfree says you can enable Payouts V2 webhooks while still calling the V1 or V1.2 transfer APIs.
  • Move partner webhooks to the 2025-01-01 version, which uses the x-webhook-* headers.
  • If you must keep Cashgram, verify it on a separate endpoint with Cashfree's sorted-parameter method.

How Hookdeck Can Help: Hookdeck's Cashfree source verifies the header scheme only. A legacy delivery shows up in the request log as failed verification with its headers and body stored, so it's obvious which format it was.

Event names differ between pages and versions

The Problem: A handler written against one page never matches real Payment Gateway events.

Why It Happens: The Payment Gateway reference uses names such as PAYMENT_SUCCESS_WEBHOOK, the cross-product overview shows PAYMENT_SUCCESS, and Secure ID and PPI put the name in event_type rather than type. Field shapes also depend on the webhook version.

Workarounds:

  • Read type, fall back to event_type, and accept both spellings of the payment events.
  • Log the name and body of the first delivery of each event you handle.
  • Pin your Payment Gateway webhook version and migrate deliberately.

How Hookdeck Can Help: Hookdeck stores every request, so you can build against what Cashfree actually sends, and filters can match either field to route each product to the right destination.

Strict 200 and limited retries

The Problem: A slow or unhealthy endpoint loses events, or gets switched off.

Why It Happens: Anything other than HTTP 200 counts as a failure, including redirects. Payment Gateway's default policy retries three times over about 40 minutes. Payouts V2 and Cashgram endpoints are disabled after more than five consecutive failures, and stay off until you re-enable them in the dashboard.

Workarounds:

  • Return 200 as soon as the request is verified and stored.
  • Configure a longer Payment Gateway retry policy (Fixed, Exponential or Custom, up to 10 retries).
  • Use Batch Resend under Developers > Webhooks > Logs to replay Payment Gateway payment webhooks after an outage. It accepts transaction IDs, a file, or a time range of up to 24 hours.
  • Reconcile against Cashfree's APIs, such as Get Order, for anything business-critical.

How Hookdeck Can Help: Hookdeck acknowledges each delivery as soon as it arrives and stores it, then delivers to your service with retries you configure, so your downtime doesn't count against Cashfree's failure limit.

Transfer success isn't completion

The Problem: Payouts are marked as paid before the beneficiary has the money.

Why It Happens: TRANSFER_SUCCESS means the transfer was debited and sent to the beneficiary bank, and confirmation there may still be pending. Cashfree says to treat a payout as fully successful only when status is SUCCESS and status_code is COMPLETED.

Workarounds:

  • Check both status and status_code before marking a payout complete.
  • Keep handling TRANSFER_REVERSED, which can arrive after a success.

How Hookdeck Can Help: Hookdeck filters can match on data.status_code, so only completed transfers reach the service that closes out payouts.

Best practices

Verify against the raw body

Cashfree signs the raw body string. In Express, use express.raw({ type: '*/*' }). In Next.js, use Buffer.from(await request.arrayBuffer()). In FastAPI, use await request.body(). Re-serialised JSON fails.

Deduplicate on business IDs

Delivery is at-least-once. Cashfree suggests a stable business identifier such as cf_payment_id, transfer_id or a verification ID as the idempotency key, and, on Payment Gateway webhook version 2025-01-01, the idempotency header as well. Cashfree's pages name that header both x-idempotency-key and x-idempotency-header, so check a real delivery. See our guide to webhook idempotency and Hookdeck's deduplication.

Don't rely on order

Cashfree doesn't guarantee delivery order. Compare event_time or the object's own timestamps before overwriting state, and re-read the order or transfer from the API when order matters.

Migrate webhook versions with an overlap

When you move a Payment Gateway endpoint to a newer webhook version, add the new endpoint first. Cashfree sends each event to both during the overlap. Update your parser, confirm it, then disable the old endpoint.

Allowlist IPs only if you must

Cashfree publishes production and sandbox source IPs and calls allowlisting optional. Signature verification is what proves a request came from Cashfree.

Conclusion

Cashfree signs Payment Gateway, Payouts V2, Secure ID and PPI webhooks the same way: a base64 HMAC-SHA256 of the x-webhook-timestamp value followed by the raw body, keyed with the product's client secret. Keep a secret per product, plan rotations around in-flight events, read the event name from type or event_type, and answer with a fast 200 so retries and auto-disable rarely come into play.

Hookdeck Event Gateway sits in front of your receiver, checks every Cashfree signature, and holds each event until your service has processed it.

Get started with Hookdeck for free and handle Cashfree webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.