Skip to Content
ProvidersCashfree

Cashfree

Category: Payments
Integration type: Platform-level (single Cashfree account)
External SDK: axios (Cashfree Orders API v2022-09-01)


Purpose

Cashfree is a second Indian payment gateway alongside Razorpay. It is offered as an alternative for tenants or platform operators who have existing Cashfree accounts or prefer Cashfree’s fee structure. Both Razorpay and Cashfree support the same billing flows — credit top-ups and plan subscriptions.

The platform uses an IPaymentGatewayProvider abstraction so the gateway can be swapped per-tenant or per-deployment without changing billing logic.

Cashfree vs Razorpay

FeatureCashfreeRazorpay
Indian marketYesYes
Subscription billingYesYes
Credit top-ups (one-time orders)YesYes
UPI / QRYesYes
International cardsYesYes
Fee structure~1.75% domestic~2% domestic
Platform defaultNo (opt-in)Yes (default)

Config Structure

Platform config (env vars)

CASHFREE_APP_ID=your_app_id CASHFREE_SECRET_KEY=your_secret_key CASHFREE_ENV=production # 'sandbox' | 'production' CASHFREE_API_VERSION=2022-09-01

Base URLs:

  • Sandbox: https://sandbox.cashfree.com/pg
  • Production: https://api.cashfree.com/pg

Integration Pattern

Payment gateway abstraction

Both Cashfree and Razorpay implement a shared IPaymentGatewayProvider interface, identical to what the .NET codebase uses but adapted for TypeScript:

interface PaymentGatewayOrder { orderId: string; amount: number; // In lowest currency unit (paise for INR) currency: string; status: 'ACTIVE' | 'PAID' | 'EXPIRED' | 'CANCELLED'; paymentUrl?: string; // Hosted payment page URL expiresAt?: string; } interface IPaymentGatewayProvider { createOrder(request: CreateOrderRequest, config: PaymentProviderConfig): Promise<PaymentGatewayOrder>; fetchOrder(orderId: string, config: PaymentProviderConfig): Promise<PaymentGatewayOrder>; getOrderIdFromCallback( signature: string, requestBody: string, timestamp: string, config: PaymentProviderConfig, ): string; }

Cashfree provider (packages/billing/src/providers/cashfree.ts)

import axios from 'axios'; import crypto from 'crypto'; class CashfreeProvider implements IPaymentGatewayProvider { private baseUrl: string; constructor( private appId: string, private secretKey: string, private env: 'sandbox' | 'production' = 'production', ) { this.baseUrl = env === 'production' ? 'https://api.cashfree.com/pg' : 'https://sandbox.cashfree.com/pg'; } private headers() { return { 'x-api-version': '2022-09-01', 'x-client-id': this.appId, 'x-client-secret': this.secretKey, 'Content-Type': 'application/json', }; } async createOrder( request: CreateOrderRequest, config: PaymentProviderConfig, ): Promise<PaymentGatewayOrder> { const response = await axios.post( `${this.baseUrl}/orders`, { order_id: request.orderId ?? `lm-${Date.now()}`, order_amount: request.amount / 100, // Cashfree takes rupees, not paise order_currency: request.currency ?? 'INR', customer_details: { customer_id: request.customerId, customer_email: request.customerEmail, customer_phone: request.customerPhone, }, order_meta: { return_url: request.returnUrl, notify_url: request.notifyUrl, }, order_note: request.description, }, { headers: this.headers() }, ); return { orderId: response.data.order_id, amount: Math.round(response.data.order_amount * 100), // Back to paise currency: response.data.order_currency, status: this.mapStatus(response.data.order_status), paymentUrl: response.data.payment_link, expiresAt: response.data.order_expiry_time, }; } async fetchOrder( orderId: string, config: PaymentProviderConfig, ): Promise<PaymentGatewayOrder> { const response = await axios.get( `${this.baseUrl}/orders/${orderId}`, { headers: this.headers() }, ); return { orderId: response.data.order_id, amount: Math.round(response.data.order_amount * 100), currency: response.data.order_currency, status: this.mapStatus(response.data.order_status), }; } getOrderIdFromCallback( signature: string, requestBody: string, timestamp: string, config: PaymentProviderConfig, ): string { // Verify Cashfree webhook signature const payload = timestamp + requestBody; const computed = crypto .createHmac('sha256', this.secretKey) .update(payload) .digest('base64'); if (computed !== signature) { throw new UnauthorizedError('Invalid Cashfree webhook signature'); } const body = JSON.parse(requestBody); return body.data?.order?.order_id ?? ''; } private mapStatus(cashfreeStatus: string): PaymentGatewayOrder['status'] { const map: Record<string, PaymentGatewayOrder['status']> = { ACTIVE: 'ACTIVE', PAID: 'PAID', EXPIRED: 'EXPIRED', CANCELLED: 'CANCELLED', TERMINATED: 'CANCELLED', }; return map[cashfreeStatus] ?? 'ACTIVE'; } }

Provider factory

The billing service resolves the correct gateway at runtime:

// packages/billing/src/gateway-factory.ts function resolvePaymentGateway(tenantConfig: TenantBillingConfig): IPaymentGatewayProvider { const gateway = tenantConfig.paymentGateway ?? config.DEFAULT_PAYMENT_GATEWAY; switch (gateway) { case 'cashfree': return new CashfreeProvider(config.CASHFREE_APP_ID, config.CASHFREE_SECRET_KEY, config.CASHFREE_ENV); case 'razorpay': default: return new RazorpayProvider(config.RAZORPAY_KEY_ID, config.RAZORPAY_KEY_SECRET); } }

Webhook Events

Cashfree sends webhooks for payment events to POST /api/webhooks/cashfree:

EventTrigger
PAYMENT_SUCCESS_WEBHOOKPayment captured successfully
PAYMENT_FAILED_WEBHOOKPayment attempt failed
PAYMENT_USER_DROPPED_WEBHOOKUser abandoned the payment page
ORDER_PAIDOrder fully paid

Webhook signature verification uses HMAC-SHA256:

signature = base64(HMAC-SHA256(timestamp + rawBody, secretKey))

The x-webhook-timestamp and x-webhook-signature headers are sent with each webhook.


Cashfree Sandbox

Test credentials are available from Cashfree Sandbox Dashboard . Test card numbers:

CardNumberCVVExpiry
Visa (success)4111 1111 1111 1111Any 3 digitsAny future date
Mastercard (success)5104 0155 5555 5558AnyAny future date
Failure (insufficient funds)4000 0000 0000 0002AnyAny future date

Test Cases

Unit tests (packages/billing/src/providers/cashfree.test.ts)

TestApproach
createOrder() converts paise → rupees in requestMock axios.post; assert order_amount = amount / 100
createOrder() converts rupees → paise in responseMock { order_amount: 499.9 }; assert amount === 49990
fetchOrder() maps order_status to typed statusMock PAID; assert status === 'PAID'
getOrderIdFromCallback() verifies HMAC-SHA256 signatureCompute valid signature; assert order ID extracted
getOrderIdFromCallback() throws on invalid signaturePass wrong signature; assert UnauthorizedError
Provider factory returns CashfreeProvider when configuredSet DEFAULT_PAYMENT_GATEWAY=cashfree; assert instance type

© 2026 Leadmetrics — Internal use only