multipay-india: one Go API for Cashfree and Razorpay

multipay-india: one Go API for Cashfree and Razorpay

by Tushar Dwivedi
Ask AI about this page

multipay-india is an open-source Go library, under the MIT license, that puts Indian payment gateways behind one canonical API. You write your checkout and billing code once. Whether the money moves through Cashfree or Razorpay becomes a configuration detail.

Why it exists

Smritea needed one set of billing code, not a rewrite for each gateway. The search was for a Go library that puts Indian gateways behind one API for subscriptions, mandates, proration and webhooks. A mandate is a customer's standing approval for repeated charges.

The search turned up two kinds of libraries.

Kind of libraryExamples checkedWhat it covers
Official single-gateway SDKsrazorpay-go, cashfree-pg, PayU's web-sdk-goOne gateway each
General multi-gateway librariessleetAdyen, Stripe, Braintree and other Western processors. No Indian gateways

No importable Go library turned up that puts Cashfree and Razorpay behind one API with subscriptions, plan-change proration and webhooks. The search was not exhaustive.

How it works

The library runs in your process and is not a network service. You create one client for one provider. The client exposes the same services for either gateway: orders, payments, refunds, payment links, plans, subscriptions and webhooks.

Money is an int64 in minor units, such as paise. Cashfree takes major units on the wire, so the adapter converts the amount. Razorpay takes minor units unchanged. Your code never has to handle the difference.

The library validates each request before any call to a provider. If a provider lacks an operation, the call fails before it reaches the gateway's SDK.

Create a subscription

This sample creates a subscription and handles a gateway error. The customer approves the mandate through AuthLink (Razorpay) or AuthSessionID (Cashfree). Cashfree also requires a customer email.

req := &domain.CreateSubscriptionRequest{
	SubscriptionID: "sub_1001",
	PlanID:         "plan_pro_monthly",
	CustomerEmail:  "[email protected]",
	CustomerPhone:  "9876543210",
	ReturnURL:      "https://app.example.com/billing/done",
}
sub, err := mp.Subscriptions().CreateSubscription(ctx, req)
if err != nil {
	pe := &domain.ProviderAPIError{}
	if errors.As(err, &pe) {
		return fmt.Errorf("%s said %d: %w",
			pe.Provider, pe.StatusCode, err)
	}
	return fmt.Errorf("create subscription: %w", err)
}
// Send the customer to sub.AuthLink (Razorpay), or start the
// JS SDK with sub.AuthSessionID (Cashfree), to approve the mandate.

Handle webhooks

The library verifies each webhook signature, which differs by vendor, and it deduplicates events by the SHA-256 hash of the body. You supply the store that holds raw payloads and processed events.

onEvent := func(ctx context.Context, e *domain.WebhookEvent) error {
	if e.ParseError != "" {
		log.Printf("parse error: %s", e.ParseError)
	}
	switch e.EventType {
	case domain.EventPaymentCaptured:
		// mark the order paid; keep this idempotent
	case domain.EventPaymentFailed:
		// notify the customer
	}
	return nil
}
for _, t := range []domain.WebhookEventType{
	domain.EventPaymentCaptured,
	domain.EventPaymentFailed,
} {
	mp.Webhooks().RegisterHandler(t, onEvent)
}
mux := http.NewServeMux()
mux.Handle("/webhooks/", mp.Webhooks().Handler("/webhooks"))

The endpoint answers in these ways:

CaseResponse
Bad signature400 SIGNATURE_INVALID
Duplicate body200 DUPLICATE_ACK
Your handler returns an error200 ACK, and the event is not marked processed, so a replay can retry it
Storage or dedup failure500

Register your handlers before you call Handler, because it copies the handlers registered at that moment.

Plan changes and proration

PreviewPlanChange is a pure function. It returns a quote with the amount to charge now, the prorated credit and the new recurring amount, and it makes no external call. An upgrade then follows what each gateway supports. On Cashfree, it creates a new re-authorized subscription. On Razorpay, it calls the native plan change at once.

Status

The README tracks the status of each provider.

ProviderStatus
CashfreeGo and frontend code exist. The README reports a sandbox test
RazorpayGo and frontend code exist. Untested
PayUTo do
Dodo PaymentsUnder consideration

Smritea's billing uses the library with Cashfree. Only the Go port exists today. The TypeScript and Python ports are still plans.

Try it

Install the Go module with go get github.com/Bytonomics/multipay-india/multipay-go. It needs Go 1.26 or later. The source and the README are at github.com/Bytonomics/multipay-india.

Related posts

Comments