FieldCraftPayKitDocsServicesBlogWork With Me →

Multi-Provider Guide

Route payments to different providers based on currency, region, or custom rules. Send INR transactions to Razorpay, USD to Stripe, and EUR to a European provider — automatically.

Why Multiple Providers?

  • Cost optimisation — local providers often have lower fees for domestic transactions
  • Higher success rates — UPI payments in India succeed more reliably through Razorpay than through Stripe
  • Redundancy — if one provider has an outage, route to another
  • Regulatory compliance — some regions require local payment processing

Setting Up the Router

Create a PaymentRouter with routing rules and a default fallback adapter:

src/lib/paykit.ts
import { PaymentRouter } from '@squaredr/paykit'
import { StripeAdapter } from '@squaredr/paykit/stripe'
import { RazorpayAdapter } from '@squaredr/paykit/razorpay'

const stripe = new StripeAdapter({
secretKey: process.env.STRIPE_SECRET_KEY!,
})

const razorpay = new RazorpayAdapter({
keyId: process.env.RAZORPAY_KEY_ID!,
keySecret: process.env.RAZORPAY_KEY_SECRET!,
})

export const router = new PaymentRouter({
routes: [
{ currency: 'INR', adapter: razorpay },
{ currency: 'USD', adapter: stripe },
{ currency: 'EUR', adapter: stripe },
],
default: stripe,
})

Using the Router

The router automatically selects the right adapter based on the charge parameters:

src/app/api/checkout/route.ts
import { router } from '@/lib/paykit'

export async function POST(request: Request) {
const { amount, currency, orderId } = await request.json()

// Router picks Razorpay for INR, Stripe for USD/EUR
const charge = await router.createCharge({
amount,
currency,
metadata: { orderId },
})

return Response.json({
clientSecret: charge.clientSecret,
provider: charge.provider, // "stripe" or "razorpay"
})
}

Routing Rules

Routes are evaluated in order. The first match wins. Each rule can include:

PropertyTypeDescription
currencystringISO 4217 currency code (e.g. "INR", "USD")
regionstringRegion identifier (e.g. "IN", "US", "EU")
adapterPaymentAdapterThe adapter to use when this rule matches

If no rule matches, the default adapter handles the charge.

Explicit Provider Override

You can bypass routing entirely by passing _provider in the charge params:

// Force Stripe regardless of currency
const charge = await router.createCharge({
amount: 50000,
currency: 'INR',
_provider: 'stripe', // Overrides routing rules
})

Handling Webhooks from Multiple Providers

When using multiple providers, you need separate webhook endpoints for each provider since they send different signatures and payloads:

src/app/api/webhooks/stripe/route.ts
import { PayKit } from '@squaredr/paykit'
import { StripeAdapter } from '@squaredr/paykit/stripe'

const stripePaykit = new PayKit({
adapter: new StripeAdapter({
secretKey: process.env.STRIPE_SECRET_KEY!,
}),
})

export async function POST(req: Request) {
const event = stripePaykit.webhooks.construct({
payload: await req.text(),
signature: req.headers.get('stripe-signature')!,
secret: process.env.STRIPE_WEBHOOK_SECRET!,
})

// Same handler logic regardless of provider
await handlePaymentEvent(event)
return new Response('ok')
}
src/app/api/webhooks/razorpay/route.ts
import { PayKit } from '@squaredr/paykit'
import { RazorpayAdapter } from '@squaredr/paykit/razorpay'

const razorpayPaykit = new PayKit({
adapter: new RazorpayAdapter({
keyId: process.env.RAZORPAY_KEY_ID!,
keySecret: process.env.RAZORPAY_KEY_SECRET!,
}),
})

export async function POST(req: Request) {
const event = razorpayPaykit.webhooks.construct({
payload: await req.text(),
signature: req.headers.get('x-razorpay-signature')!,
secret: process.env.RAZORPAY_WEBHOOK_SECRET!,
})

// Same handler — PayKit normalises the event shape
await handlePaymentEvent(event)
return new Response('ok')
}

Both endpoints call the same handlePaymentEvent function because PayKit normalises all webhook events into the same shape.

Frontend: Dynamic Provider Selection

Return the provider name from your checkout API so the frontend can load the correct client adapter:

src/components/DynamicCheckout.tsx
'use client'

import { PayKitProvider, CheckoutForm } from '@squaredr/paykit-react'
import { StripeClientAdapter } from '@squaredr/paykit/stripe/client'
import { RazorpayClientAdapter } from '@squaredr/paykit/razorpay/client'

const adapters = {
stripe: new StripeClientAdapter({
publishableKey: process.env.NEXT_PUBLIC_STRIPE_PK!,
}),
razorpay: new RazorpayClientAdapter({
keyId: process.env.NEXT_PUBLIC_RAZORPAY_KEY_ID!,
}),
}

export function DynamicCheckout({
clientSecret,
provider,
}: {
clientSecret: string
provider: 'stripe' | 'razorpay'
}) {
return (
<PayKitProvider
adapter={adapters[provider]}
clientSecret={clientSecret}
>
<CheckoutForm
onSuccess={(result) => console.log('Paid:', result.id)}
onError={(err) => console.error(err.message)}
/>
</PayKitProvider>
)
}