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
| Feature | Cashfree | Razorpay |
|---|---|---|
| Indian market | Yes | Yes |
| Subscription billing | Yes | Yes |
| Credit top-ups (one-time orders) | Yes | Yes |
| UPI / QR | Yes | Yes |
| International cards | Yes | Yes |
| Fee structure | ~1.75% domestic | ~2% domestic |
| Platform default | No (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-01Base 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:
| Event | Trigger |
|---|---|
PAYMENT_SUCCESS_WEBHOOK | Payment captured successfully |
PAYMENT_FAILED_WEBHOOK | Payment attempt failed |
PAYMENT_USER_DROPPED_WEBHOOK | User abandoned the payment page |
ORDER_PAID | Order 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:
| Card | Number | CVV | Expiry |
|---|---|---|---|
| Visa (success) | 4111 1111 1111 1111 | Any 3 digits | Any future date |
| Mastercard (success) | 5104 0155 5555 5558 | Any | Any future date |
| Failure (insufficient funds) | 4000 0000 0000 0002 | Any | Any future date |
Test Cases
Unit tests (packages/billing/src/providers/cashfree.test.ts)
| Test | Approach |
|---|---|
createOrder() converts paise → rupees in request | Mock axios.post; assert order_amount = amount / 100 |
createOrder() converts rupees → paise in response | Mock { order_amount: 499.9 }; assert amount === 49990 |
fetchOrder() maps order_status to typed status | Mock PAID; assert status === 'PAID' |
getOrderIdFromCallback() verifies HMAC-SHA256 signature | Compute valid signature; assert order ID extracted |
getOrderIdFromCallback() throws on invalid signature | Pass wrong signature; assert UnauthorizedError |
Provider factory returns CashfreeProvider when configured | Set DEFAULT_PAYMENT_GATEWAY=cashfree; assert instance type |
Related
- Razorpay Provider — default payment gateway
- Credits Overview — credit top-up flow
- Credit Tracking —
credit_topup_orderstable