FieldCraftPayKitDocsServicesBlogWork With Me →

Core Concepts

A look at the ideas that shape PayKit's design — the adapter pattern, unified types, server/client separation, provider routing, webhook normalisation, and the open-core licensing model.

The Adapter Pattern

At its core, PayKit is a thin orchestration layer that delegates every provider-specific call to an adapter. Your application talks to PayKit; PayKit talks to the adapter; the adapter talks to the provider SDK.

Architecture
┌─────────────────────────────────┐
│ Your Application │
└──────────────┬──────────────────┘


┌─────────────────────────────────┐
│ @squaredr/paykit (Core) │
│ • Unified types │
│ • PayKit client │
│ • Provider routing │
└──────────────┬──────────────────┘

┌─────────┴─────────┐
▼ ▼
┌──────────┐ ┌──────────┐
│ Stripe │ │ Razorpay │
│ Adapter │ │ Adapter │
└──────────┘ └──────────┘

This gives you three concrete benefits:

  • Write once — your charge-creation, webhook, and checkout code is identical regardless of provider.
  • Swap providers — change a single adapter import and the rest of your codebase stays untouched.
  • Test with mocks — inject a mock adapter in your test suite to verify business logic without hitting any external API.

Unified Types

Every adapter maps provider responses into a shared set of TypeScript interfaces. The most important one is UnifiedCharge:

UnifiedCharge
interface UnifiedCharge {
/** PayKit-generated identifier */
id: string

/** Amount in smallest currency unit (e.g. cents) */
amount: number

/** ISO 4217 currency code */
currency: string

/** Current status of the charge */
status: 'pending' | 'succeeded' | 'failed' | 'refunded'

/** Secret used by the frontend to confirm payment */
clientSecret: string

/** Arbitrary key-value pairs you attached at creation */
metadata: Record<string, string>

/** Raw provider response — escape hatch for advanced use */
_raw: unknown
}

The _raw field is an escape hatch. If you need something provider-specific that PayKit does not normalise, you can cast _raw to the provider's native type and access it directly. This keeps the unified interface clean while still giving you full access when you need it.

Server vs Client Code

PayKit separates server-side and client-side adapters into different entry points. This is important for two reasons: security and bundle size.

Server-side adapters

Server adapters hold your secret key and must never be imported in browser code. They handle charge creation, refunds, and webhook verification.

Server import
// Server only — contains your secret key
import { StripeAdapter } from '@squaredr/paykit/stripe'

Client-side adapters

Client adapters contain only the browser-safe code needed to confirm payments and render checkout UIs. They use your publishable key.

Client import
// Client only — contains your publishable key
import { StripeClientAdapter } from '@squaredr/paykit/stripe/client'

Because these are separate entry points, your bundler can tree-shake them independently. The server adapter and its dependencies will never end up in your client bundle, and vice-versa. This keeps your frontend lean and your secrets safe.

Provider Routing

If you accept payments in multiple currencies or regions, you can use PaymentRouter to automatically select the best adapter for each transaction.

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

const router = new PaymentRouter({
routes: [
{
currency: 'INR',
adapter: new RazorpayAdapter({
keyId: process.env.RAZORPAY_KEY_ID!,
keySecret: process.env.RAZORPAY_KEY_SECRET!,
}),
},
{
currency: 'USD',
adapter: new StripeAdapter({
secretKey: process.env.STRIPE_SECRET_KEY!,
}),
},
],
// Fallback when no route matches
default: new StripeAdapter({
secretKey: process.env.STRIPE_SECRET_KEY!,
}),
})

// Automatically selects the right adapter
const charge = await router.createCharge({
amount: 50000,
currency: 'INR', // → RazorpayAdapter
})

Each route can also include region and condition properties for more advanced matching. Routes are evaluated in order; the first match wins. If nothing matches, the default adapter handles the charge.

Webhook Normalisation

Different providers send different event names and payload shapes. PayKit maps them into a single set of event types so your handler code does not need provider-specific branches.

PayKit EventStripe EventRazorpay Event
payment.succeededpayment_intent.succeededpayment.captured
payment.failedpayment_intent.payment_failedpayment.failed
refund.createdcharge.refundedrefund.processed

Your webhook handler always receives the same shape regardless of which provider fired the event:

webhooks.ts
const event = paykit.webhooks.construct({
payload: body,
signature,
secret: process.env.STRIPE_WEBHOOK_SECRET!,
})

// event.type is always a PayKit event name
if (event.type === 'payment.succeeded') {
await fulfillOrder(event.data.id)
}

Open Core Model

PayKit follows an open-core licensing model. The free tier covers everything you need to start accepting payments:

  • Charge creation and confirmation
  • Webhook verification and normalisation
  • All provider adapters (Stripe, Razorpay, and more)
  • React checkout components

PayKit Pro extends the free adapters with advanced features for teams that need more:

  • Refund management
  • Payouts
  • Subscription lifecycle
  • Pre-built analytics dashboards

Pro will be available as a one-time purchase — no recurring fees. It is coming soon.