# MedusaJS Source: https://docs.usepaykit.dev/adapters/medusajs Use any PayKit provider inside a Medusa payment module. ```bash theme={null} npm install @paykit-sdk/medusajs @paykit-sdk/core @paykit-sdk/stripe ``` ## Setup ```typescript filename="medusa-config.ts" theme={null} import { defineConfig } from '@medusajs/framework/utils'; import { stripe } from '@paykit-sdk/stripe'; export default defineConfig({ modules: [ { resolve: '@medusajs/payment', options: { providers: [ { resolve: '@paykit-sdk/medusajs', options: { provider: stripe(), webhookSecret: process.env.STRIPE_WEBHOOK_SECRET, debug: process.env.NODE_ENV === 'development', }, }, ], }, }, ], }); ``` Swap `stripe()` for any PayKit provider and everything else stays the same. ## Configuration Options | Option | Type | Required | Default | Description | | ------------------------- | -------------- | -------- | ------- | ----------------------------------------------------------------------------------------------- | | `provider` | PayKitProvider | Yes | — | A PayKit provider instance (e.g. `stripe()`, `gopay()`) | | `webhookSecret` | string \| null | No | `null` | Webhook secret used to verify incoming webhook payloads | | `amountToCentsMultiplier` | number (≥ 1) | No | `1` | Multiplies the Medusa amount before sending to the provider. Use `100` for cent-based providers | | `debug` | boolean | No | `false` | Logs PayKit operations to the console | ### `amountToCentsMultiplier` Some providers (e.g. GoPay) expect amounts in the smallest currency unit — `3000` for 30 EUR. If Medusa is passing the amount in the major unit, set this option to convert automatically: ```typescript filename="medusa-config.ts" theme={null} import { gopay } from '@paykit-sdk/gopay'; { resolve: '@paykit-sdk/medusajs', options: { provider: gopay(), webhookSecret: process.env.GOPAY_WEBHOOK_SECRET, amountToCentsMultiplier: 100, // 30 EUR → sends 3000 to GoPay }, } ``` Defaults to `1` (no conversion). Minimum value is `1`. ## Example See a working Medusa + PayKit server: [GitHub](https://github.com/usepaykit/paykit-sdk/examples/medusa-shop) # API Reference Source: https://docs.usepaykit.dev/concepts/api-reference Core resource types and methods. ## Customer ```typescript theme={null} interface Customer { /** The unique identifier of the customer. */ id: string; /** The email address of the customer. */ email: string; /** The full name of the customer. */ name: string; /** The phone number of the customer. . */ phone: string | null; /** Arbitrary key-value metadata attached to the customer. */ metadata?: Record; /** Provider-specific or custom fields not covered by the unified model. */ custom_fields?: Record; /** When the customer was created. */ created_at: Date; /** When the customer was last updated. */ updated_at: Date | null; } const customer = await paykit.customers.create({ email: 'user@example.com', name: 'Jane Doe', phone: '+1234567890', metadata: { plan: 'pro' }, }); const customer = await paykit.customers.retrieve('cus_123'); const customer = await paykit.customers.update('cus_123', { name: 'Jane Smith', }); ``` ## Checkout ```typescript theme={null} interface Checkout { /** The unique identifier of the checkout session. */ id: string; /** The customer linked to this resource. */ customer: Payee | null; /** The URL to redirect the customer to for payment. */ payment_url: string; /** Arbitrary key-value metadata attached to the session. */ metadata: Record | null; /** Whether this is a one-time payment or recurring subscription. */ session_type: 'one_time' | 'recurring'; /** The products included in this checkout. */ products: Array<{ id: string; quantity: number }>; /** ISO 4217 currency code (e.g. USD, NGN). */ currency: string; /** Total amount in the smallest currency unit (e.g. cents, kobo). */ amount: number; /** Subscription billing details, if session_type is recurring. */ subscription?: CheckoutSubscription | null; } const checkout = await paykit.checkouts.create({ customer: 'cus_123', // or { email: 'user@example.com' } item_id: 'price_123', session_type: 'one_time', quantity: 1, metadata: { source: 'web' }, success_url: 'https://example.com/success', cancel_url: 'https://example.com/cancel', provider_metadata: { /* typed per provider */ }, }); const checkout = await paykit.checkouts.retrieve('cs_123'); ``` ## Subscription ```typescript theme={null} type SubscriptionBillingInterval = | 'day' | 'week' | 'month' | 'year' | { type: 'custom'; durationMs: number }; interface Subscription { /** The unique identifier of the subscription. */ id: string; /** The customer linked to this resource. */ customer: Payee | null; /** Recurring amount in the smallest currency unit. */ amount: number; /** ISO 4217 currency code. */ currency: string; /** Current lifecycle status of the subscription. */ status: 'active' | 'past_due' | 'canceled' | 'expired' | 'trialing' | 'pending'; /** Start of the current billing period. */ current_period_start: Date; /** End of the current billing period (next renewal date). */ current_period_end: Date; /** The provider item or plan ID this subscription is tied to. */ item_id: string; /** How often the subscription renews. Custom intervals use durationMs. */ billing_interval: SubscriptionBillingInterval; /** Arbitrary key-value metadata. */ metadata: Record | null; /** Provider-specific or custom fields. */ custom_fields: Record | null; /** Whether the subscription requires user action (e.g. 3DS). */ requires_action: boolean; /** Redirect URL if action is required. */ payment_url: string | null; } const subscription = await paykit.subscriptions.create({ customer: 'cus_123', item_id: 'price_123', metadata: { plan: 'pro' }, }); const subscription = await paykit.subscriptions.retrieve('sub_123'); const subscription = await paykit.subscriptions.cancel('sub_123'); ``` ## Payment ```typescript theme={null} interface Payment { /** The unique identifier of the payment (reference). */ id: string; /** Amount in the smallest currency unit (e.g. cents, kobo). */ amount: number; /** ISO 4217 currency code. */ currency: string; /** The customer linked to this resource. */ customer: Payee | null; /** * Current status of the payment. * - pending: created but not yet processed * - processing: being processed by the provider * - requires_action: needs user action (3DS, bank verification, etc.) * - requires_capture: authorized but not yet captured * - succeeded: payment completed successfully * - canceled: payment was canceled * - failed: payment failed */ status: | 'pending' | 'processing' | 'requires_action' | 'requires_capture' | 'succeeded' | 'canceled' | 'failed'; /** Arbitrary key-value metadata attached to the payment. */ metadata: Record; /** The provider item ID linked to this payment. */ item_id: string | null; /** Whether the payment is waiting for user action before it can proceed. */ requires_action: boolean; /** URL to redirect the user to if requires_action is true. */ payment_url: string | null; } const payment = await paykit.payments.retrieve('pay_123'); ``` ## Refund ```typescript theme={null} interface Refund { /** The unique identifier of the refund. */ id: string; /** Amount refunded in the smallest currency unit. */ amount: number; /** ISO 4217 currency code. */ currency: string; /** Human-readable reason for the refund */ reason: string | null; /** Arbitrary key-value metadata attached to the refund. */ metadata: Record | null; } const refund = await paykit.refunds.create({ payment_id: 'pay_123', amount: 1000, reason: 'requested_by_customer', provider_metadata: { /* typed per provider */ }, }); ``` ## Invoice ```typescript theme={null} interface Invoice { /** The unique identifier of the invoice. */ id: string; /** The customer linked to this resource. */ customer: Payee | null; /** The subscription this invoice belongs to. */ subscription_id: string | null; /** Whether this invoice is for a one-time payment or a recurring subscription. */ billing_mode: 'one_time' | 'recurring'; /** Amount paid in the smallest currency unit (e.g. cents). */ amount_paid: number; /** ISO 4217 currency code (e.g. USD, EUR). */ currency: string; /** Current status of the invoice. */ status: 'paid' | 'open'; /** ISO 8601 timestamp of when the invoice was paid. */ paid_at: string | null; /** Individual line items on the invoice. */ line_items: Array<{ id: string; quantity: number }> | null; /** Arbitrary key-value metadata. */ metadata: Record | null; /** Provider-specific or custom fields. */ custom_fields: Record | null; } ``` ## Payee A `Payee` is either an object with an email or an object with an ID. On resource responses, `customer` is `Payee | null` — null means the provider did not return customer info for that event. ```typescript theme={null} type Payee = { email: string } | { id: string | number }; // Both are valid when creating resources await paykit.checkouts.create({ customer: { email: 'user@example.com' }, ... }); await paykit.checkouts.create({ customer: { id: 'cus_123' }, ... }); ``` # Error Handling Source: https://docs.usepaykit.dev/concepts/error-handling PayKit methods throw on failure. Wrap with try/catch to handle errors. PayKit methods return the result directly. Wrap with `try/catch` to handle errors: ```typescript theme={null} try { const customer = await paykit.customers.create({ email: 'user@example.com', name: 'Jane Doe', }); console.log(customer.id); } catch (error) { console.error(error.message); } ``` ## Error types Import from `@paykit-sdk/core`: ```typescript theme={null} import { ValidationError, ConfigurationError, ProviderNotSupportedError, NotImplementedError, } from '@paykit-sdk/core'; ``` | Error | When | | --------------------------- | ------------------------------------------ | | `ValidationError` | Input fails schema validation | | `ConfigurationError` | Provider misconfigured or missing env vars | | `ProviderNotSupportedError` | Operation not supported by this provider | | `NotImplementedError` | Operation planned but not yet built | ## Example ```typescript theme={null} try { const checkout = await paykit.checkouts.create({ customer: 'cus_123', item_id: 'price_123', session_type: 'one_time', quantity: 1, success_url: 'https://example.com/success', cancel_url: 'https://example.com/cancel', }); redirect(checkout.payment_url); } catch (error) { if (error instanceof ValidationError) { return res.status(400).json({ error: error.message }); } if (error instanceof ProviderNotSupportedError) { return res.status(501).json({ error: 'Not supported by this provider' }); } return res.status(500).json({ error: 'Unexpected error' }); } ``` # HTTP Client Source: https://docs.usepaykit.dev/concepts/http-client The HTTP client PayKit exposes for providers without an official SDK. Some providers ship an official SDK, and for those `_native` hands you that SDK. For the rest, `_native` gives you PayKit's own HTTP client, already pointed at the provider's base URL and authenticated with the credentials you configured. ## Methods ```typescript theme={null} paykit._native.get(endpoint, options?); paykit._native.post(endpoint, options?); paykit._native.put(endpoint, options?); paykit._native.patch(endpoint, options?); paykit._native.delete(endpoint, options?); ``` `endpoint` is the path, relative to the provider's base URL. `options` is a normal `fetch` init object without `method`. ## Example ```typescript theme={null} const result = await paykit._native.post<{ id: string }>('/transactions', { body: JSON.stringify({ amount: 5000, currency: 'NGN' }), }); if (!result.ok) { console.error(result.error); return; } console.log(result.value.id); ``` ## Why use it * Base URL and auth headers are already set, so you only pass the path. * Sends and parses JSON for you. * Returns a `Result` instead of throwing, so you check `result.ok`. * Retries rate limits, timeouts, dropped connections and 5xx responses up to 3 times with backoff. * Classifies failures into the same error types as the rest of PayKit. * Types the response through the generic on each method. * Built on `fetch`, so there is no extra dependency and it runs wherever PayKit runs. ## Import the type ```typescript theme={null} import { HTTPClient } from '@paykit-sdk/core'; ``` # Webhooks Source: https://docs.usepaykit.dev/concepts/webhooks Handle provider events with one typed API. PayKit normalizes webhook events into a consistent set of typed events across all providers. ## Setup ```typescript theme={null} const webhook = paykit.webhooks .setup({ webhookSecret: process.env.YOUR_WEBHOOK_SECRET! }) .on('customer.created', async event => { // event.data is typed as Customer }) .on('subscription.created', async event => { // event.data is typed as Subscription }) .on('payment.created', async event => { // event.data is typed as Payment — payment initiated }) .on('payment.succeeded', async event => { // event.data is typed as Payment — payment completed successfully }) .on('payment.failed', async event => { // event.data is typed as Payment — payment failed or was canceled }) .on('refund.created', async event => { // event.data is typed as Refund }) .on('invoice.generated', async event => { // event.data is typed as Invoice }); await webhook.handle({ body: rawBody, headersAsObject: Object.fromEntries(request.headers), fullUrl: request.url, }); ``` ## Standard events | Event | Data type | | ----------------------- | -------------- | | `customer.created` | `Customer` | | `customer.updated` | `Customer` | | `customer.deleted` | `Customer` | | `subscription.created` | `Subscription` | | `subscription.updated` | `Subscription` | | `subscription.canceled` | `Subscription` | | `payment.created` | `Payment` | | `payment.updated` | `Payment` | | `payment.succeeded` | `Payment` | | `payment.failed` | `Payment` | | `refund.created` | `Refund` | | `invoice.generated` | `Invoice` | ## Raw provider events You can also listen to native provider events directly. These are prefixed with the provider name and fully typed: ```typescript theme={null} // Stripe — typed as Stripe.Checkout.Session .on('stripe.checkout.session.completed', async event => { console.log(event.data.payment_intent); }) // Paystack — typed as PaystackTransaction .on('paystack.charge.success', async event => { console.log(event.data.reference); }) ``` Standard events and raw events can be mixed freely on the same webhook instance. ## headersAsObject `webhook.handle` expects `headersAsObject` as a plain `Record`. Convert from your framework's headers object using `Object.fromEntries`: ```typescript theme={null} // Web Request (Next.js, Hono, etc.) headersAsObject: Object.fromEntries(request.headers); // Express headersAsObject: req.headers as Record; ``` # Custom Provider Source: https://docs.usepaykit.dev/custom-provider Build your own payment provider for PayKit. Any class that implements `PayKitProvider` works with PayKit. Only `@paykit-sdk/core` is needed. ## Install ```bash theme={null} npm install @paykit-sdk/core ``` ## Full example (HTTP API) ```typescript theme={null} import { AbstractPayKitProvider, PayKitProvider, HTTPClient, ProviderMetadataRegistry, PaykitProviderOptions, schema, ValidationError, NotImplementedError, ProviderNotSupportedError, createCheckoutSchema, CreateCheckoutSchema, Checkout, WebhookEventPayload, WebhookHandlerConfig, } from '@paykit-sdk/core'; import { z } from 'zod'; // 1. Define typed provider_metadata per resource interface MyMetadata extends ProviderMetadataRegistry { checkout?: { custom_reference?: string }; refund?: { reason_code?: string }; } // 2. Define typed raw events type MyRawEvents = { 'myprovider.payment.completed': { id: string; amount: number }; 'myprovider.refund.processed': { id: string }; }; // 3. Define options export interface MyProviderOptions extends PaykitProviderOptions { apiKey: string; } const optionsSchema = schema()( z.object({ apiKey: z.string().min(1, 'API key is required'), isSandbox: z.boolean(), }), ); // 4. Implement the provider export class MyProvider extends AbstractPayKitProvider implements PayKitProvider { readonly providerName = 'my-provider'; private _client: HTTPClient; constructor(protected readonly opts: MyProviderOptions) { super(optionsSchema, opts, 'my-provider'); this._client = new HTTPClient({ baseUrl: opts.isSandbox ? 'https://api.sandbox.myprovider.com' : 'https://api.myprovider.com', headers: { Authorization: `Bearer ${opts.apiKey}`, 'Content-Type': 'application/json', }, retryOptions: { max: 3, baseDelay: 1000, debug: opts.debug ?? false, }, }); } // Escape hatch: return your SDK client here, or the HTTP client get _native(): HTTPClient { return this._client; } createCheckout = async ( params: CreateCheckoutSchema, ): Promise => { const { error, data } = createCheckoutSchema.safeParse(params); if (error) throw ValidationError.fromZodError( error, this.providerName, 'createCheckout', ); const res = await this._client.post>( '/checkouts', { body: JSON.stringify(data), }, ); if (!res.ok) throw new Error('Failed to create checkout'); return res.value as unknown as Checkout; }; // Mark operations not supported by this provider updateCheckout = (_id: string) => Promise.reject( new ProviderNotSupportedError( 'updateCheckout', this.providerName, { reason: 'This provider does not support modifying checkouts after creation', }, ), ); // Mark operations planned for the future deleteCheckout = (_id: string) => Promise.reject( new NotImplementedError('deleteCheckout', this.providerName, { futureSupport: true, }), ); // ... implement remaining required methods handleWebhook = async ( payload: WebhookHandlerConfig, webhookSecret: string, ): Promise>> => { const signature = payload.headersAsObject['x-signature']; if (!signature) throw new Error('Missing signature'); // verify signature, parse body, return typed events return []; }; } ``` ## Error helpers | Class | Use for | | ---------------------------------------------------------- | ------------------------------------------ | | `ValidationError.fromZodError(err, provider, method)` | Invalid input | | `ProviderNotSupportedError(method, provider, { reason })` | Operations the provider will never support | | `NotImplementedError(method, provider, { futureSupport })` | Not built yet | ## Register it ```typescript theme={null} import { PayKit, createEndpointHandlers } from '@paykit-sdk/core'; export const paykit = new PayKit( new MyProvider({ apiKey: 'key_...', isSandbox: true }), ); export const endpoints = createEndpointHandlers(paykit); ``` # Introduction Source: https://docs.usepaykit.dev/introduction PayKit is a unified payments toolkit for TypeScript. One API across providers. PayKit lets you integrate Stripe, Paystack, Polar, PayPal, and more without rewriting your app logic when you switch providers. ## What you get * Unified API for checkouts, customers, subscriptions, payments, refunds, and invoices * Per-provider typed `provider_metadata` for provider-specific features * Fully typed raw webhook events per provider * React hooks and a UI kit for client apps * Drop-in shadcn registry blocks ## Supported providers | Provider | Package | | ------------ | -------------------------- | | Chapa | `@paykit-sdk/chapa` | | Comgate | `@paykit-sdk/comgate` | | GoPay | `@paykit-sdk/gopay` | | LemonSqueezy | `@paykit-sdk/lemonsqueezy` | | Mercado Pago | `@paykit-sdk/mercadopago` | | Monnify | `@paykit-sdk/monnify` | | PayPal | `@paykit-sdk/paypal` | | Paystack | `@paykit-sdk/paystack` | | Polar | `@paykit-sdk/polar` | | Razorpay | `@paykit-sdk/razorpay` | | Stripe | `@paykit-sdk/stripe` | | Xendit | `@paykit-sdk/xendit` | ## Adapters | Adapter | Package | | -------- | ---------------------- | | MedusaJS | `@paykit-sdk/medusajs` | # Bachs Source: https://docs.usepaykit.dev/providers/bachs Bachs provider for PayKit. ```bash theme={null} npm install @paykit-sdk/core @paykit-sdk/bachs ``` Bachs is a hosted-checkout-first payments and billing platform for African internet businesses selling globally ([docs.bachs.io](https://docs.bachs.io)). ## Setup ```typescript theme={null} import { PayKit } from '@paykit-sdk/core'; import { bachs } from '@paykit-sdk/bachs'; export const paykit = new PayKit(bachs()); ``` Required env vars: ```bash theme={null} BACHS_API_KEY=sk_sandbox_... BACHS_SANDBOX=true ``` The key's prefix decides sandbox vs live, and overrides `isSandbox` if the two disagree. ```typescript theme={null} import { PayKit } from '@paykit-sdk/core'; import { createBachs } from '@paykit-sdk/bachs'; export const paykit = new PayKit( createBachs({ apiKey: 'sk_sandbox_...', isSandbox: true, }), ); ``` ## Creating a checkout Products (and their prices) live in your Bachs product catalog. `item_id` is a Bachs `product_id`. Bachs resolves the amount/currency from the product itself, so you don't pass either: ```typescript theme={null} const checkout = await paykit.checkouts.create({ customer: { email: 'jane@example.com' }, // or { id: 'cust_...' } for an existing customer item_id: 'prod_abc123', quantity: 1, session_type: 'one_time', success_url: 'https://shop.example.com/thanks', cancel_url: 'https://shop.example.com/cart', metadata: { order_id: 'ORD-9876' }, }); // Redirect the customer to checkout.payment_url ``` If `prod_abc123` has a `billing_cycle` configured on Bachs, completing this same checkout creates a subscription automatically. You'll get both a `payment.succeeded` and a `subscription.created` webhook. ## Creating a payment directly Same underlying flow, mapped onto `Payment`. Needs `success_url` in `provider_metadata` since Bachs still redirects the customer even for a direct payment: ```typescript theme={null} const payment = await paykit.payments.create({ customer: { email: 'jane@example.com' }, amount: 50, // informational only - Bachs resolves the real amount from the product currency: 'USD', item_id: 'prod_abc123', capture_method: 'automatic', // Bachs captures automatically, no manual step provider_metadata: { success_url: 'https://shop.example.com/thanks', cancel_url: 'https://shop.example.com/cart', // optional }, }); ``` `payment.id` (and `checkout.id`) is Bachs' `checkout_id` for the lifetime of the payment. Keep using this same `checkout_id` for `paykit.payments.retrieve` and `paykit.refunds.create`. ## Safe retries `paykit.checkouts.create` and `paykit.payments.create` each make two calls under the hood: create the session, then fetch it back to resolve pricing. If your own code might retry the whole call, pass a stable `idempotencyKey` (your own order ID works well) so a retry returns the original session instead of creating a duplicate: ```typescript theme={null} await paykit.checkouts.create({ // ... provider_metadata: { idempotencyKey: `order-${order.id}` }, }); ``` `paykit.refunds.create` supports the same option. ## Customers Full support except delete, Bachs has no endpoint for it: ```typescript theme={null} const customer = await paykit.customers.create({ email: 'jane@example.com', name: 'Jane Doe', billing: null, }); await paykit.customers.update(customer.id, { name: 'Jane D.' }); ``` ## Subscriptions Bachs has no direct way to create a subscription. Create one by calling `paykit.checkouts.create` with a recurring-configured product instead. Retrieve, update, and cancel work directly: ```typescript theme={null} const subscription = await paykit.subscriptions.retrieve('sub_1a2b3c4d5e'); await paykit.subscriptions.update('sub_1a2b3c4d5e', { metadata: {}, provider_metadata: { product_id: 'prod_xyz456' }, // move to a different plan }); await paykit.subscriptions.cancel('sub_1a2b3c4d5e'); // cancels immediately ``` ## Refunds ```typescript theme={null} await paykit.refunds.create({ payment_id: 'chk_1a2b3c4d5e', // the checkout_id amount: 29, reason: 'Customer request', metadata: null, }); ``` The payment must have actually succeeded. Refunding a checkout that hasn't been paid throws `ResourceNotFoundError`. ## Webhooks Add a webhook endpoint from your Bachs Developer Portal and pass its signing secret to `webhookSecret`: ```typescript theme={null} const webhook = paykit.webhooks .setup({ webhookSecret: process.env.BACHS_WEBHOOK_SECRET! }) // whsec_... .on('payment.succeeded', async event => {}) .on('payment.failed', async event => {}) .on('payment.updated', async event => {}) .on('subscription.created', async event => {}) .on('subscription.updated', async event => {}) .on('subscription.canceled', async event => {}) .on('refund.created', async event => {}) .on('customer.created', async event => {}) .on('customer.updated', async event => {}); await webhook.handle({ body: await request.text(), headersAsObject: Object.fromEntries(request.headers), fullUrl: request.url, }); ``` ### Raw Bachs events Opt into any native Bachs event type, typed against Bachs' payload: ```typescript theme={null} paykit.webhooks .setup({ webhookSecret: process.env.BACHS_WEBHOOK_SECRET! }) .on('bachs.payout.paid', async event => { // event.data is the raw Bachs payload }); ``` All available raw events and their PayKit mappings: | Raw event | PayKit event | | ------------------------------------- | ----------------------- | | `bachs.collection.succeeded` | `payment.succeeded` | | `bachs.collection.failed` | `payment.failed` | | `bachs.collection.underpaid` | `payment.updated` | | `bachs.refund.created` | `refund.created` | | `bachs.refund.paid` | `refund.created` | | `bachs.refund.failed` | `refund.created` | | `bachs.customer.subscription.created` | `subscription.created` | | `bachs.customer.subscription.updated` | `subscription.updated` | | `bachs.customer.subscription.deleted` | `subscription.canceled` | | `bachs.customer.created` | `customer.created` | | `bachs.customer.updated` | `customer.updated` | | `bachs.checkout.*` | *(raw only)* | | `bachs.invoice.*` | *(raw only)* | | `bachs.payout.*` | *(raw only)* | | `bachs.dispute.*` | *(raw only)* | | `bachs.conversion.*` | *(raw only)* | | `bachs.account.updated` | *(raw only)* | | `bachs.capability.updated` | *(raw only)* | | `bachs.transfer.created` | *(raw only)* | ## Escape hatch `paykit._native` returns PayKit's [HTTP client](/concepts/http-client), already pointed at Bachs and authenticated with your credentials. Use it to call endpoints PayKit does not map yet. ```typescript theme={null} const result = await paykit._native.get('/v1/customers/cus_123'); if (result.ok) console.log(result.value); ``` # Chapa Source: https://docs.usepaykit.dev/providers/chapa Chapa provider for PayKit. ```bash theme={null} npm install @paykit-sdk/chapa ``` ## Setup ```typescript theme={null} import { PayKit } from '@paykit-sdk/core'; import { chapa } from '@paykit-sdk/chapa'; export const paykit = new PayKit(chapa()); ``` Required env vars: ```bash theme={null} CHAPA_SECRET_KEY=CHASECK_TEST-... ``` ```typescript theme={null} import { PayKit } from '@paykit-sdk/core'; import { createChapa } from '@paykit-sdk/chapa'; export const paykit = new PayKit( createChapa({ secretKey: 'CHASECK_TEST-...', isSandbox: true }), ); ``` ## How it works ```typescript theme={null} const checkout = await paykit.checkouts.create({ customer: { email: 'user@example.com' }, item_id: 'my-product', session_type: 'one_time', quantity: 1, success_url: 'https://example.com/success', cancel_url: 'https://example.com/cancel', provider_metadata: { amount: '400', currency: 'ETB' }, }); // Redirect the customer to checkout.payment_url // Once they're back, confirm the payment const payment = await paykit.payments.retrieve(checkout.id); // Still pending? You can cancel it await paykit.payments.cancel(checkout.id); ``` Chapa doesn't have a customer or subscription API — customer details are captured inline with each transaction instead. ## Webhooks Chapa signs webhooks using your **secret key** — there's no separate webhook signing secret to generate or configure. ```typescript theme={null} const webhook = paykit.webhooks .setup({ webhookSecret: process.env.CHAPA_SECRET_KEY! }) .on('payment.updated', async event => { /* charge.success */ }) .on('payment.failed', async event => { /* charge.failed / charge.cancelled */ }) .on('refund.created', async event => { /* charge.refunded or charge.reversed */ }) .on('invoice.generated', async event => { /* charge.success */ }); await webhook.handle({ body: rawBody, headersAsObject: Object.fromEntries(request.headers), fullUrl: request.url, }); ``` Chapa sends two signature headers on every webhook request — the handler accepts either one: * `x-chapa-signature`: `HMAC-SHA256(secretKey, JSON.stringify(payload))` * `chapa-signature`: `HMAC-SHA256(secretKey, secretKey)` ### Raw Chapa events Opt into any native Chapa event — fully typed against Chapa's payload shapes: ```typescript theme={null} paykit.webhooks .setup({ webhookSecret: process.env.CHAPA_SECRET_KEY! }) .on('chapa.charge.success', async event => { // event.data is typed as a Chapa transaction payload }) .on('chapa.payout.success', async event => { // event.data is typed as a Chapa payout payload }); ``` Every webhook always emits its raw event first, alongside the matching standard PayKit event below where one applies. Payouts have no PayKit equivalent, so they're only available as raw events. | Raw event | PayKit event | | ------------------------- | --------------------------------------- | | `chapa.charge.success` | `payment.updated` + `invoice.generated` | | `chapa.charge.*` (failed) | `payment.failed` | | `chapa.charge.refunded` | `refund.created` | | `chapa.charge.reversed` | `refund.created` | | `chapa.payout.success` | *(raw only)* | | `chapa.payout.*` (failed) | *(raw only)* | ## provider\_metadata `amount` and `currency` are required in `provider_metadata` for checkout because Chapa does not infer them from the item: ```typescript theme={null} // checkout.provider_metadata requires amount and currency await paykit.checkouts.create({ customer: { email: 'user@example.com' }, item_id: 'my-product', session_type: 'one_time', quantity: 1, success_url: 'https://example.com/success', cancel_url: 'https://example.com/cancel', provider_metadata: { amount: '400', // in the standard currency unit (e.g. Birr), as a string currency: 'ETB', // ETB or USD }, }); // refund.provider_metadata is typed as { reference?: string } await paykit.refunds.create({ payment_id: 'tx_ref_from_the_original_transaction', amount: 400, reason: 'requested_by_customer', provider_metadata: { reference: 'REF123', // your own distinctive reference for the refund request }, }); ``` ## Escape hatch `paykit._native` returns PayKit's [HTTP client](/concepts/http-client), already pointed at Chapa and authenticated with your credentials. Use it to call endpoints PayKit does not map yet. ```typescript theme={null} const result = await paykit._native.get( '/transaction/verify/tx_ref_123', ); if (result.ok) console.log(result.value); ``` # Comgate Source: https://docs.usepaykit.dev/providers/comgate Comgate provider for PayKit. ```bash theme={null} npm install @paykit-sdk/comgate ``` ## Setup ```typescript theme={null} import { PayKit } from '@paykit-sdk/core'; import { comgate } from '@paykit-sdk/comgate'; export const paykit = new PayKit(comgate()); ``` Required env vars: ```bash theme={null} COMGATE_MERCHANT=your-merchant-id COMGATE_SECRET=your-secret COMGATE_SANDBOX=true ``` ```typescript theme={null} import { PayKit } from '@paykit-sdk/core'; import { createComgate } from '@paykit-sdk/comgate'; export const paykit = new PayKit( createComgate({ merchant: 'your-merchant-id', secret: 'your-secret', isSandbox: true, }), ); ``` ## How it works ```typescript theme={null} const checkout = await paykit.checkouts.create({ customer: { email: 'user@example.com' }, item_id: 'my-product', session_type: 'one_time', quantity: 1, amount: 2900, currency: 'CZK', success_url: 'https://example.com/success', cancel_url: 'https://example.com/cancel', }); // Redirect the customer to checkout.payment_url ``` Comgate doesn't support customers, subscriptions, or updating checkouts/payments. It also doesn't support retrieving checkouts — use `paykit.payments.retrieve` to look up transaction status instead. ## Webhooks Comgate sends a POST with `merchant`, `secret`, `transId`, and `status` fields in the body. PayKit verifies the secret and merchant ID directly, then calls `/v1.0/status` to confirm the transaction status server-side. ```typescript theme={null} const webhook = paykit.webhooks .setup({ webhookSecret: process.env.COMGATE_SECRET! }) .on('payment.created', async event => { /* PENDING transaction */ }) .on('payment.updated', async event => { /* AUTHORIZED (pre-auth, awaiting capture) */ }) .on('payment.succeeded', async event => { /* PAID transaction */ }) .on('payment.failed', async event => { /* CANCELLED transaction */ }) .on('invoice.generated', async event => { /* emitted alongside payment.succeeded on PAID */ }); await webhook.handle({ body: rawBody, headersAsObject: Object.fromEntries(request.headers), fullUrl: request.url, }); ``` Comgate statuses and their PayKit event mappings: | Comgate status | PayKit events emitted | | -------------- | ----------------------------------------- | | `PAID` | `payment.succeeded` + `invoice.generated` | | `PENDING` | `payment.created` | | `AUTHORIZED` | `payment.updated` | | `CANCELLED` | `payment.failed` | ### Raw Comgate events Comgate webhooks carry a `status` field rather than named event types. You can listen for the raw status values via the `comgate.*` namespace: ```typescript theme={null} paykit.webhooks .setup({ webhookSecret: process.env.COMGATE_SECRET! }) .on('comgate.PAID', async event => { // event.data is the verified Comgate status response }) .on('comgate.CANCELLED', async event => { /* ... */ }); ``` ## provider\_metadata ```typescript theme={null} // checkout.provider_metadata — label is optional (defaults to "Order from Eshop") await paykit.checkouts.create({ customer: { email: 'user@example.com' }, item_id: 'my-product', session_type: 'one_time', quantity: 1, amount: 2900, currency: 'CZK', success_url: 'https://example.com/success', cancel_url: 'https://example.com/cancel', provider_metadata: { label: 'Order #1234', }, }); // payment.provider_metadata — email is required; paymentLabel is optional await paykit.payments.create({ customer: { id: 'payer-42' }, item_id: 'my-product', amount: 2900, currency: 'CZK', provider_metadata: { email: 'user@example.com', // required paymentLabel: 'Order from Eshop', // optional }, }); ``` ## Escape hatch `paykit._native` returns PayKit's [HTTP client](/concepts/http-client), already pointed at Comgate and authenticated with your credentials. Use it to call endpoints PayKit does not map yet. ```typescript theme={null} const result = await paykit._native.get('/v2.0/payment/pay_123.json'); if (result.ok) console.log(result.value); ``` # GoPay Source: https://docs.usepaykit.dev/providers/gopay GoPay provider for PayKit. ```bash theme={null} npm install @paykit-sdk/gopay ``` ## Setup ```typescript theme={null} import { PayKit } from '@paykit-sdk/core'; import { gopay } from '@paykit-sdk/gopay'; export const paykit = new PayKit(gopay()); ``` Required env vars: ```bash theme={null} GOPAY_CLIENT_ID=140... GOPAY_CLIENT_SECRET=n6W... GOPAY_GO_ID=864... GOPAY_SANDBOX=true GOPAY_WEBHOOK_URL=https://example.com/api/webhook ``` ```typescript theme={null} import { PayKit } from '@paykit-sdk/core'; import { createGopay } from '@paykit-sdk/gopay'; export const paykit = new PayKit( createGopay({ clientId: '140...', clientSecret: 'n6W...', goId: '864...', isSandbox: true, webhookUrl: 'https://example.com/api/webhook', }), ); ``` ## How it works ```typescript theme={null} const checkout = await paykit.checkouts.create({ customer: { email: 'user@example.com' }, item_id: 'my-product', session_type: 'one_time', quantity: 1, success_url: 'https://example.com/success', cancel_url: 'https://example.com/cancel', provider_metadata: { amount: '2900', currency: 'CZK' }, }); // Redirect the customer to checkout.payment_url ``` Recurring payments are created as an initial payment with a `recurrence` object — subsequent charges are triggered automatically by GoPay, or on demand via GoPay's `createRecurrence` API. GoPay sends webhook notifications via **GET**, not POST, so your route handler needs to accept GET requests. It also doesn't support customer management. ## Webhooks ```typescript theme={null} const webhook = paykit.webhooks .setup({ webhookSecret: '' }) // GoPay does not use a shared secret .on('payment.created', async event => { /* CREATED state */ }) .on('payment.updated', async event => { /* PAYMENT_METHOD_CHOSEN or AUTHORIZED state */ }) .on('payment.succeeded', async event => { /* PAID state */ }) .on('payment.failed', async event => { /* CANCELED or TIMEOUTED state */ }) .on('invoice.generated', async event => { /* emitted alongside payment.succeeded on PAID */ }) .on('subscription.created', async event => { /* emitted on PAID when the payment has a parent_id (recurring charge) */ }) .on('subscription.canceled', async event => { /* emitted when recurrence state is STOPPED */ }) .on('refund.created', async event => { /* REFUNDED or PARTIALLY_REFUNDED state */ }); await webhook.handle({ body: '', headersAsObject: Object.fromEntries(request.headers), fullUrl: request.url, // GoPay passes the payment ID as ?id= query param }); ``` GoPay payment states and their PayKit event mappings: | GoPay state | PayKit events emitted | | ----------------------- | --------------------------------------------------------------------------------- | | `CREATED` | `payment.created` | | `PAYMENT_METHOD_CHOSEN` | `payment.updated` | | `PAID` | `payment.succeeded` + `invoice.generated` (+ `subscription.created` if recurring) | | `AUTHORIZED` | `payment.updated` | | `CANCELED` | `payment.failed` (+ `subscription.canceled` if recurrence STOPPED) | | `TIMEOUTED` | `payment.failed` | | `REFUNDED` | `refund.created` | | `PARTIALLY_REFUNDED` | `refund.created` | ## Subscriptions GoPay maps PayKit billing intervals to its `recurrence_cycle` field: | PayKit `billing_interval` | GoPay `recurrence_cycle` | | ------------------------- | ------------------------ | | `day` | `DAY` | | `week` | `WEEK` | | `month` | `MONTH` | | `year` | `ON_DEMAND` | | `custom` | `ON_DEMAND` | `year` and `custom` intervals fall back to `ON_DEMAND` — GoPay does not natively support them. You must trigger each charge manually via GoPay's `createRecurrence` API. ## provider\_metadata GoPay requires `amount` and `currency` in `provider_metadata` for checkout, and `success_url` for direct payments and subscriptions: ```typescript theme={null} // checkout.provider_metadata — amount is required, currency defaults to CZK, language defaults to EN await paykit.checkouts.create({ customer: { email: 'user@example.com' }, item_id: 'my-product', session_type: 'one_time', quantity: 1, success_url: 'https://example.com/success', cancel_url: 'https://example.com/cancel', provider_metadata: { amount: '2900', currency: 'CZK', language: 'en', // optional, defaults to 'EN' }, }); // payment.provider_metadata — success_url is required await paykit.payments.create({ customer: { email: 'user@example.com' }, item_id: 'my-product', amount: 2900, currency: 'CZK', provider_metadata: { success_url: 'https://example.com/success', // required }, }); // subscription.provider_metadata — success_url is required await paykit.subscriptions.create({ customer: { email: 'user@example.com' }, item_id: 'my-plan', amount: 2900, currency: 'CZK', billing_interval: 'month', provider_metadata: { success_url: 'https://example.com/success', // required description: 'Monthly plan', // optional }, }); ``` ## Escape hatch `paykit._native` returns PayKit's [HTTP client](/concepts/http-client), already pointed at GoPay and authenticated with your credentials. Use it to call endpoints PayKit does not map yet. ```typescript theme={null} const result = await paykit._native.get( '/payments/payment/3000000001', ); if (result.ok) console.log(result.value); ``` # LemonSqueezy Source: https://docs.usepaykit.dev/providers/lemonsqueezy LemonSqueezy provider for PayKit. ```bash theme={null} npm install @paykit-sdk/lemonsqueezy ``` ## Setup ```typescript theme={null} import { PayKit } from '@paykit-sdk/core'; import { lemonsqueezy } from '@paykit-sdk/lemonsqueezy'; export const paykit = new PayKit(lemonsqueezy()); ``` Required env vars: ```bash theme={null} LEMONSQUEEZY_API_KEY=... LEMONSQUEEZY_WEBHOOK_SECRET=your-webhook-secret ``` ```typescript theme={null} import { PayKit } from '@paykit-sdk/core'; import { createLemonSqueezy } from '@paykit-sdk/lemonsqueezy'; export const paykit = new PayKit( createLemonSqueezy({ apiKey: '...', isSandbox: true }), ); ``` ## Supported Endpoints LemonSqueezy’s API strictly follows JSON:API standards. Certain resources like stores, products, and standard checkouts are largely managed via their hosted dashboard. The PayKit provider implements the following operational endpoints: * `customers.create` * `customers.retrieve` * `customers.update` * `checkouts.create` * `checkouts.retrieve` * `payments.retrieve` (maps to LemonSqueezy `orders`) * `refunds.create` (maps to LemonSqueezy `order refunds`) * `subscriptions.retrieve` * `subscriptions.update` * `subscriptions.cancel` * `subscriptions.delete` *Note: Operations like `checkouts.update` and `subscriptions.create` are not natively supported by the LemonSqueezy API and will intentionally throw an "Unsupported" error.* ## How it works To create a checkout session for LemonSqueezy, you must provide your LemonSqueezy `store_id` and the `variant_id` representing the product you are selling. ```typescript theme={null} const checkout = await paykit.checkouts.create({ customer: { email: 'user@example.com' }, session_type: 'one_time', quantity: 1, success_url: 'https://example.com/success', cancel_url: 'https://example.com/cancel', provider_metadata: { store_id: 12345, // Required: Your LemonSqueezy Store ID variant_id: 98765, // Required: The Product Variant ID custom_price: 500000, // Optional: override price }, }); // Redirect the customer to the checkout url returned from the API // Once they're back, confirm the checkout state const retrievedCheckout = await paykit.checkouts.retrieve( checkout.id, ); ``` ## Webhooks LemonSqueezy signs webhooks with HMAC-SHA256 via the `x-signature` header. ```typescript theme={null} const webhook = paykit.webhooks .setup({ webhookSecret: process.env.LEMONSQUEEZY_WEBHOOK_SECRET! }) .on('payment.updated', async event => { /* order_created */ }) .on('subscription.created', async event => { /* subscription_created */ }) .on('subscription.updated', async event => { /* subscription_updated */ }) .on('subscription.canceled', async event => { /* subscription_cancelled */ }); await webhook.handle({ body: rawBody, headersAsObject: Object.fromEntries(request.headers), fullUrl: request.url, }); ``` ### Raw LemonSqueezy events All webhook events fired by the LemonSqueezy provider emit the raw `event_name` provided by LemonSqueezy (e.g. `lemonsqueezy.order_created`, `lemonsqueezy.subscription_created`). You can listen to any native LemonSqueezy event. ```typescript theme={null} paykit.webhooks .setup({ webhookSecret: process.env.LEMONSQUEEZY_WEBHOOK_SECRET! }) .on('lemonsqueezy.order_created', async event => { // event.data is typed as LemonSqueezyOrder }); ``` A subset of these events also emit a standard PayKit event: | Raw event | PayKit event | | ------------------------------------- | ----------------------- | | `lemonsqueezy.order_created` | `payment.updated` | | `lemonsqueezy.subscription_created` | `subscription.created` | | `lemonsqueezy.subscription_updated` | `subscription.updated` | | `lemonsqueezy.subscription_cancelled` | `subscription.canceled` | ## provider\_metadata LemonSqueezy heavily relies on `provider_metadata` to pass required relationships like `store_id` and `variant_id` that are unique to their JSON:API architecture. ```typescript theme={null} await paykit.customers.create({ email: 'user@example.com', name: 'John Doe', provider_metadata: { store_id: 12345, // Required for creating customers }, }); ``` ### Refunds LemonSqueezy refunds are issued directly against a specific payment (order). The endpoint will issue a full refund if no amount is provided, or a partial refund if an `amount` is specified in cents. ```typescript theme={null} const refund = await paykit.refunds.create({ payment_id: '12345', // The LemonSqueezy Order ID amount: 500, // Optional: Partial refund amount in cents reason: 'Customer requested', }); ``` ## Escape hatch `paykit._native` returns PayKit's [HTTP client](/concepts/http-client), already pointed at LemonSqueezy and authenticated with your credentials. Use it to call endpoints PayKit does not map yet. ```typescript theme={null} const result = await paykit._native.get('/orders/123'); if (result.ok) console.log(result.value); ``` # Mercado Pago Source: https://docs.usepaykit.dev/providers/mercadopago Mercado Pago provider for PayKit. ```bash theme={null} npm install @paykit-sdk/mercadopago ``` ## Setup ```typescript theme={null} import { PayKit } from '@paykit-sdk/core'; import { mercadoPago } from '@paykit-sdk/mercadopago'; export const paykit = new PayKit(mercadoPago()); ``` Required env vars: ```bash theme={null} MERCADOPAGO_ACCESS_TOKEN=TEST-... ``` ```typescript theme={null} import { PayKit } from '@paykit-sdk/core'; import { createMercadoPago } from '@paykit-sdk/mercadopago'; export const paykit = new PayKit( createMercadoPago({ accessToken: 'TEST-...', isSandbox: true }), ); ``` ## How it works ```typescript theme={null} const checkout = await paykit.checkouts.create({ customer: { email: 'user@example.com' }, item_id: 'my-product', session_type: 'one_time', quantity: 1, success_url: 'https://example.com/success', cancel_url: 'https://example.com/cancel', provider_metadata: { amount: '400', currency: 'ARS' }, }); // Redirect the customer to checkout.payment_url // Once they're back, confirm the payment const payment = await paykit.payments.retrieve(checkout.id); ``` Checkouts and one-off payments are both built on Mercado Pago's Checkout Pro preferences. Subscriptions reference a pre-existing Plan instead — `item_id` maps to that plan's id: ```typescript theme={null} const subscription = await paykit.subscriptions.create({ customer: { email: 'user@example.com' }, item_id: 'plan_id_from_mercadopago', quantity: 1, billing_interval: 'month', amount: 4990, currency: 'ARS', metadata: null, }); // Redirect the customer to subscription.payment_url to authorize the mandate ``` ## Webhooks Mercado Pago signs webhooks with a **secret signature** configured separately in your integration settings — it's not the same as your access token. Notifications only carry the resource type and id; the provider fetches the full resource before mapping standard events. ```typescript theme={null} const webhook = paykit.webhooks .setup({ webhookSecret: process.env.MERCADOPAGO_WEBHOOK_SECRET! }) .on('payment.succeeded', async event => { /* payment status "approved" */ }) .on('payment.failed', async event => { /* payment status "rejected" or "cancelled" */ }) .on('refund.created', async event => { /* payment status "refunded" */ }) .on('subscription.updated', async event => { /* subscription_preapproval notifications */ }) .on('invoice.generated', async event => { /* approved payment or subscription charge */ }); await webhook.handle({ body: rawBody, headersAsObject: Object.fromEntries(request.headers), fullUrl: request.url, }); ``` Mercado Pago sends the signature in the `x-signature` header (`ts=...,v1=...`), and the notified resource id in the `data.id` query parameter — both feed into the HMAC-SHA256 manifest alongside `x-request-id`. ### Raw Mercado Pago events Opt into any native Mercado Pago notification — fully typed against Mercado Pago's webhook envelope: ```typescript theme={null} paykit.webhooks .setup({ webhookSecret: process.env.MERCADOPAGO_WEBHOOK_SECRET! }) .on('mercadopago.payment.created', async event => { // event.data is the full Mercado Pago notification envelope }) .on( 'mercadopago.subscription_authorized_payment.created', async event => { // fired for each recurring subscription charge }, ); ``` Every webhook always emits its raw event first, alongside the matching standard PayKit event below where one applies. | Raw event | PayKit event | | ------------------------------------------------------------------- | ----------------------------------------- | | `mercadopago.payment.created` | `payment.created` | | `mercadopago.payment.updated` (status `approved`) | `payment.succeeded` + `invoice.generated` | | `mercadopago.payment.updated` (status `rejected`/`cancelled`) | `payment.failed` | | `mercadopago.payment.updated` (status `refunded`) | `refund.created` | | `mercadopago.subscription_preapproval.created` | `subscription.created` | | `mercadopago.subscription_preapproval.updated` (status `cancelled`) | `subscription.canceled` | | `mercadopago.subscription_preapproval.updated` (other statuses) | `subscription.updated` | | `mercadopago.subscription_authorized_payment.*` (charge `approved`) | `payment.succeeded` + `invoice.generated` | | `mercadopago.subscription_authorized_payment.*` (charge `rejected`) | `payment.failed` | | `mercadopago.subscription_preapproval_plan.*` | *(raw only)* | Mercado Pago has no delete-preference API and no general payment-update API — use `capturePayment`/`cancelPayment` for payment state transitions instead of `updatePayment`. ## provider\_metadata ```typescript theme={null} // checkout/payment.provider_metadata requires amount and currency (checkout) // or success_url (payment) await paykit.checkouts.create({ customer: { email: 'user@example.com' }, item_id: 'my-product', session_type: 'one_time', quantity: 1, success_url: 'https://example.com/success', cancel_url: 'https://example.com/cancel', provider_metadata: { amount: '400', // in the standard currency unit, as a string currency: 'ARS', }, }); ``` ## Escape hatch `paykit._native` returns PayKit's [HTTP client](/concepts/http-client), already pointed at Mercado Pago and authenticated with your credentials. Use it to call endpoints PayKit does not map yet. ```typescript theme={null} const result = await paykit._native.get('/v1/customers/123'); if (result.ok) console.log(result.value); ``` # MoneyGram Source: https://docs.usepaykit.dev/providers/moneygram MoneyGram provider for PayKit. ```bash theme={null} npm install @paykit-sdk/core @paykit-sdk/moneygram ``` MoneyGram is a money-transfer (remittance) API, not a traditional checkout/subscription processor. There's no customer-object API and no recurring payments — `createPayment` and `createCheckout` both run MoneyGram's full **Quote → Update → Commit** transfer flow as a single call (MoneyGram has no separate hosted checkout page, so "checkout" here just means the same transfer, mapped onto PayKit's `Checkout` shape). ## Setup ```typescript theme={null} import { PayKit } from '@paykit-sdk/core'; import { moneygram } from '@paykit-sdk/moneygram'; export const paykit = new PayKit(moneygram()); ``` Required env vars: ```bash theme={null} MONEYGRAM_CLIENT_ID=... MONEYGRAM_CLIENT_SECRET=... MONEYGRAM_AGENT_PARTNER_ID=30150519 MONEYGRAM_OPERATOR_ID=paykit-web MONEYGRAM_SANDBOX=true ``` ```typescript theme={null} import { PayKit } from '@paykit-sdk/core'; import { createMoneygram } from '@paykit-sdk/moneygram'; export const paykit = new PayKit( createMoneygram({ clientId: '...', clientSecret: '...', agentPartnerId: '30150519', operatorId: 'paykit-web', isSandbox: true, }), ); ``` ## How it works ```typescript theme={null} const payment = await paykit.payments.create({ customer: { email: 'sender@example.com' }, amount: 100, currency: 'USD', item_id: 'transfer-to-jane', capture_method: 'automatic', // MoneyGram commits immediately - manual capture isn't supported provider_metadata: { destinationCountryCode: 'PHL', // ISO alpha-3 serviceOptionCode: 'WILL_CALL', // cash pickup; omit to let MoneyGram pick a default sender: { name: { firstName: 'John', lastName: 'Doe' }, address: { line1: '123 Main St', city: 'Dallas', countryCode: 'USA', }, mobilePhone: { number: '5551234567', countryDialCode: '1' }, personalDetails: { dateOfBirth: '1990-01-01' }, primaryIdentification: { typeCode: 'PPT', id: 'X1234567', issueCountryCode: 'USA', }, }, receiver: { name: { firstName: 'Jane', lastName: 'Smith' }, }, }, }); ``` Under the hood this makes 3 calls to MoneyGram's Transfer API: 1. `POST /transfer/v1/transactions/quote` — reserves a `transactionId` and locks fees/FX for 30 minutes 2. `PUT /transfer/v1/transactions/{id}` — attaches the sender/receiver/compliance data from `provider_metadata` 3. `PUT /transfer/v1/transactions/{id}/commit` — actually moves the money `sender` and `receiver` are required on every call — MoneyGram has no customer-object API, so full KYC data must be supplied per-transfer. It doesn't support customer management or subscriptions. ## Checkouts Same flow, mapped onto `Checkout` instead of `Payment` — useful if your code already calls `paykit.checkouts`. Since `Checkout` has no top-level `amount`/`currency`, both go in `provider_metadata` too: ```typescript theme={null} const checkout = await paykit.checkouts.create({ customer: { email: 'sender@example.com' }, item_id: 'transfer-to-jane', quantity: 1, session_type: 'one_time', // MoneyGram transfers are never recurring success_url: 'https://example.com/success', // unused - no redirect step cancel_url: 'https://example.com/cancel', // unused - no redirect step provider_metadata: { amount: 100, currency: 'USD', destinationCountryCode: 'PHL', serviceOptionCode: 'WILL_CALL', sender: { /* same shape as createPayment above */ }, receiver: { /* same shape as createPayment above */ }, }, }); ``` `checkout.payment_url` is a receipt link (valid 5 minutes), not a "go pay here" redirect — the transfer is already committed by the time `createCheckout` returns. `retrieveCheckout`/`updateCheckout` behave the same as `retrievePayment`/`updatePayment` below, since MoneyGram has one transaction resource, not separate payment/checkout resources. ## Webhooks MoneyGram doesn't use a shared secret for webhooks, and publishes exactly one fixed public key per environment (sandbox/production) with no per-partner issuance — so `webhookSecret` is unused. Pass `null`: ```typescript theme={null} const webhook = paykit.webhooks .setup({ webhookSecret: null }) // unused - always verified against MoneyGram's published sandbox/production key .on('payment.created', async event => { /* UNFUNDED - customer must fund at a MoneyGram store */ }) .on('payment.succeeded', async event => { /* SENT - committed, funded, and accepted */ }) .on('payment.updated', async event => { /* AVAILABLE / IN_TRANSIT / RECEIVED / DELIVERED / PROCESSING / CLOSED */ }) .on('payment.failed', async event => { /* REJECTED */ }) .on('refund.created', async event => { /* REFUNDED */ }); await webhook.handle({ body: await request.text(), headersAsObject: Object.fromEntries(request.headers), fullUrl: request.url, }); ``` MoneyGram transaction statuses and their PayKit event mappings: | MoneyGram status | PayKit event | | ---------------- | ------------------- | | `UNFUNDED` | `payment.created` | | `SENT` | `payment.succeeded` | | `AVAILABLE` | `payment.updated` | | `IN_TRANSIT` | `payment.updated` | | `RECEIVED` | `payment.updated` | | `DELIVERED` | `payment.updated` | | `PROCESSING` | `payment.updated` | | `REJECTED` | `payment.failed` | | `REFUNDED` | `refund.created` | | `CLOSED` | `payment.updated` | ## Refunds ```typescript theme={null} await paykit.refunds.create({ payment_id: 'txn_1', amount: 100, reason: 'requested_by_customer', metadata: null, provider_metadata: { refundReasonCode: 'CUSTOMER_REQUEST', // required - MoneyGram's enumerated refund reason }, }); ``` This retrieves the transaction for refund eligibility (`GET /refund/v2/transactions/{id}`), then commits the refund (`PUT /refund/v2/transactions/{id}/commit`). ## Amending a receiver's name MoneyGram's only supported post-commit edit is correcting the receiver's name (e.g. to match their ID for payout), via `updatePayment` (or `updateCheckout` — same behavior, mapped onto `Checkout`): ```typescript theme={null} await paykit.payments.update('txn_1', { metadata: {}, provider_metadata: { receiverFirstName: 'Jane', receiverLastName: 'Smith Corrected', }, }); ``` Without `receiverFirstName`/`receiverLastName`, `updatePayment` just re-fetches the current transaction. ## Unsupported operations `createCustomer`, `createSubscription`, `capturePayment`, `cancelPayment`, and `deleteCheckout`/`deletePayment` throw `ProviderNotSupportedError` — MoneyGram has no customer storage, manual capture, recurring transfers, or a way to delete/void a committed transfer. Use `createPayment` / `createCheckout` / `createRefund` directly instead. ## Escape hatch `paykit._native` returns PayKit's [HTTP client](/concepts/http-client), already pointed at MoneyGram and authenticated with your credentials. Use it to call endpoints PayKit does not map yet. ```typescript theme={null} const result = await paykit._native.get( '/status/v1/transactions/tx_123', ); if (result.ok) console.log(result.value); ``` # Monnify Source: https://docs.usepaykit.dev/providers/monnify Monnify provider for PayKit. ```bash theme={null} npm install @paykit-sdk/monnify ``` ## Setup ```typescript theme={null} import { PayKit } from '@paykit-sdk/core'; import { monnify } from '@paykit-sdk/monnify'; export const paykit = new PayKit(monnify()); ``` Required env vars: ```bash theme={null} MONNIFY_API_KEY=MK_TEST_... MONNIFY_SECRET_KEY=your-secret-key MONNIFY_SANDBOX=true ``` ```typescript theme={null} import { PayKit } from '@paykit-sdk/core'; import { createMonnify } from '@paykit-sdk/monnify'; export const paykit = new PayKit( createMonnify({ apiKey: 'MK_TEST_...', secretKey: 'your-secret-key', isSandbox: true, }), ); ``` ## How it works ```typescript theme={null} const checkout = await paykit.checkouts.create({ customer: { email: 'user@example.com' }, item_id: 'my-product', session_type: 'one_time', quantity: 1, success_url: 'https://example.com/success', cancel_url: 'https://example.com/cancel', provider_metadata: { amount: 5000, // in the standard currency unit (e.g. Naira) currency: 'NGN', }, }); // Redirect the customer to checkout.payment_url // Once they're back, confirm the payment const payment = await paykit.payments.retrieve(checkout.id); ``` Monnify only has one way to move money — a hosted redirect transaction — so `paykit.payments.create` uses the same flow as `paykit.checkouts.create`, just returns a `Payment` instead of a `Checkout`; it needs a `success_url` in `provider_metadata` since there's no other field for it to redirect to. Monnify doesn't support customer management or subscriptions. ## Webhooks Monnify signs webhook payloads with HMAC-SHA512 via the `monnify-signature` header. Add a webhook endpoint in your Monnify dashboard that points to your handler. ```typescript theme={null} const webhook = paykit.webhooks .setup({ webhookSecret: process.env.MONNIFY_WEBHOOK_SECRET! }) .on('payment.created', async event => { /* SUCCESSFUL_TRANSACTION */ }) .on('payment.failed', async event => { /* REJECTED_PAYMENT */ }) .on('payment.updated', async event => { /* SETTLEMENT */ }) .on('refund.created', async event => { /* refund succeeded or failed */ }); await webhook.handle({ body: rawBody, headersAsObject: Object.fromEntries(request.headers), fullUrl: request.url, }); ``` ### Raw Monnify events Opt into any native Monnify event type — typed against Monnify's API payload: ```typescript theme={null} paykit.webhooks .setup({ webhookSecret: process.env.MONNIFY_WEBHOOK_SECRET! }) .on('monnify.SUCCESSFUL_TRANSACTION', async event => { // event.data is the raw Monnify transaction payload }) .on('monnify.MANDATE_UPDATE', async event => { // event.data is the raw Monnify mandate payload }); ``` All available raw events and their PayKit mappings: | Raw event | PayKit event | | ---------------------------------------- | ------------------------------------------------------------------------- | | `monnify.SUCCESSFUL_TRANSACTION` | `payment.created` | | `monnify.SUCCESSFUL_TRANSACTION_OFFLINE` | `payment.created` | | `monnify.REJECTED_PAYMENT` | `payment.failed` | | `monnify.SETTLEMENT` | `payment.updated` | | `monnify.SUCCESSFUL_REFUND` | `refund.created` | | `monnify.FAILED_REFUND` | `refund.created` | | `monnify.MANDATE_UPDATE` | `subscription.created` / `subscription.canceled` / `subscription.updated` | | `monnify.CUSTOMER_CREATED` | *(ignored)* | | `monnify.CUSTOMER_UPDATED` | *(ignored)* | | `monnify.CUSTOMER_DELETED` | *(ignored)* | | `monnify.SUCCESSFUL_DISBURSEMENT` | *(ignored)* | | `monnify.FAILED_DISBURSEMENT` | *(ignored)* | | `monnify.REVERSED_DISBURSEMENT` | *(ignored)* | ## provider\_metadata `amount` and `currency` are required in `provider_metadata` for checkout because Monnify does not infer them from the plan or item: ```typescript theme={null} // checkout.provider_metadata requires amount and currency await paykit.checkouts.create({ customer: { email: 'user@example.com' }, item_id: 'my-product', session_type: 'one_time', quantity: 1, success_url: 'https://example.com/success', cancel_url: 'https://example.com/cancel', provider_metadata: { amount: 5000, // in the standard currency unit (e.g. Naira) currency: 'NGN', }, }); // payment.provider_metadata requires success_url — Monnify still redirects // the customer even for a "direct" payment await paykit.payments.create({ customer: { email: 'user@example.com' }, item_id: 'my-product', amount: 5000, currency: 'NGN', provider_metadata: { success_url: 'https://example.com/success', // required }, }); // refund.provider_metadata — any extra fields are forwarded to Monnify's refund endpoint await paykit.refunds.create({ payment_id: 'MNFY|...', amount: 1000, reason: 'Customer request', }); ``` ## Escape hatch `paykit._native` returns PayKit's [HTTP client](/concepts/http-client), already pointed at Monnify and authenticated with your credentials. Use it to call endpoints PayKit does not map yet. ```typescript theme={null} const result = await paykit._native.get( '/v2/merchant/transactions/query', ); if (result.ok) console.log(result.value); ``` # PayPal Source: https://docs.usepaykit.dev/providers/paypal PayPal provider for PayKit. ```bash theme={null} npm install @paykit-sdk/paypal ``` ## Setup ```typescript theme={null} import { PayKit } from '@paykit-sdk/core'; import { paypal } from '@paykit-sdk/paypal'; export const paykit = new PayKit(paypal()); ``` Required env vars: ```bash theme={null} PAYPAL_CLIENT_ID=... PAYPAL_CLIENT_SECRET=... PAYPAL_SANDBOX=true PAYPAL_WEBHOOK_SECRET=... ``` ```typescript theme={null} import { PayKit } from '@paykit-sdk/core'; import { createPayPal } from '@paykit-sdk/paypal'; export const paykit = new PayKit( createPayPal({ clientId: '...', clientSecret: '...', isSandbox: true, }), ); ``` ## How it works ```typescript theme={null} const checkout = await paykit.checkouts.create({ customer: { email: 'user@example.com' }, item_id: 'sku_abc123', session_type: 'one_time', quantity: 2, success_url: 'https://example.com/success', cancel_url: 'https://example.com/cancel', provider_metadata: { currency: 'USD', amount: '29.00', itemName: 'T-Shirt', }, }); // Customer approves the order via PayPal's UI, then capture it const payment = await paykit.payments.capture(checkout.id, { amount: 2900, }); ``` PayPal doesn't support standalone customer management — use `payer` information within orders instead. Metadata is stored as the order's `customId`, so the combined length of all metadata keys and values must stay under 127 characters after JSON serialization. ## Webhooks Enable these events in your PayPal dashboard: * `CHECKOUT.ORDER.APPROVED` * `CHECKOUT.ORDER.COMPLETED` * `PAYMENT.CAPTURE.COMPLETED` * `PAYMENT.CAPTURE.REFUNDED` * `BILLING.SUBSCRIPTION.CREATED` * `BILLING.SUBSCRIPTION.UPDATED` * `BILLING.SUBSCRIPTION.ACTIVATED` * `BILLING.SUBSCRIPTION.SUSPENDED` * `BILLING.SUBSCRIPTION.CANCELLED` * `BILLING.SUBSCRIPTION.EXPIRED` ```typescript theme={null} const webhook = paykit.webhooks .setup({ webhookSecret: process.env.PAYPAL_WEBHOOK_SECRET! }) // Webhook ID from dashboard .on('payment.created', async event => { /* CHECKOUT.ORDER.APPROVED */ }) .on('payment.succeeded', async event => { /* CHECKOUT.ORDER.COMPLETED or PAYMENT.CAPTURE.COMPLETED */ }) .on('refund.created', async event => { /* PAYMENT.CAPTURE.REFUNDED */ }) .on('subscription.created', async event => { /* BILLING.SUBSCRIPTION.CREATED */ }) .on('subscription.updated', async event => { /* BILLING.SUBSCRIPTION.UPDATED / ACTIVATED / SUSPENDED */ }) .on('subscription.canceled', async event => { /* BILLING.SUBSCRIPTION.CANCELLED / EXPIRED */ }); await webhook.handle({ body: rawBody, headersAsObject: Object.fromEntries(request.headers), fullUrl: request.url, }); ``` ### Raw PayPal events Opt into any native PayPal event — typed against the full PayPal event catalog: ```typescript theme={null} paykit.webhooks .setup({ webhookSecret: process.env.PAYPAL_WEBHOOK_SECRET! }) .on('paypal.PAYMENT.CAPTURE.COMPLETED', async event => { // event.data is typed as PayPalWebhookEvent<'PAYMENT.CAPTURE.COMPLETED'> }) .on('paypal.BILLING.SUBSCRIPTION.PAYMENT.FAILED', async event => { // event.data is typed as PayPalWebhookEvent<'BILLING.SUBSCRIPTION.PAYMENT.FAILED', PayPalSubscription> }); ``` All available raw events: | PayPal event | PayKit event emitted | | -------------------------------------------- | ----------------------- | | `paypal.CHECKOUT.ORDER.APPROVED` | `payment.created` | | `paypal.CHECKOUT.ORDER.COMPLETED` | `payment.succeeded` | | `paypal.PAYMENT.CAPTURE.COMPLETED` | `payment.succeeded` | | `paypal.PAYMENT.CAPTURE.REFUNDED` | `refund.created` | | `paypal.BILLING.SUBSCRIPTION.CREATED` | `subscription.created` | | `paypal.BILLING.SUBSCRIPTION.UPDATED` | `subscription.updated` | | `paypal.BILLING.SUBSCRIPTION.ACTIVATED` | `subscription.updated` | | `paypal.BILLING.SUBSCRIPTION.SUSPENDED` | `subscription.updated` | | `paypal.BILLING.SUBSCRIPTION.CANCELLED` | `subscription.canceled` | | `paypal.BILLING.SUBSCRIPTION.EXPIRED` | `subscription.canceled` | | `paypal.PAYMENT.AUTHORIZATION.CREATED` | *(raw only)* | | `paypal.PAYMENT.AUTHORIZATION.VOIDED` | *(raw only)* | | `paypal.PAYMENT.CAPTURE.DECLINED` | *(raw only)* | | `paypal.PAYMENT.CAPTURE.PENDING` | *(raw only)* | | `paypal.PAYMENT.CAPTURE.REVERSED` | *(raw only)* | | `paypal.PAYMENT.REFUND.PENDING` | *(raw only)* | | `paypal.PAYMENT.REFUND.FAILED` | *(raw only)* | | `paypal.BILLING.SUBSCRIPTION.PAYMENT.FAILED` | *(raw only)* | | `paypal.CUSTOMER.DISPUTE.CREATED` | *(raw only)* | | `paypal.CUSTOMER.DISPUTE.RESOLVED` | *(raw only)* | | `paypal.CUSTOMER.DISPUTE.UPDATED` | *(raw only)* | ## provider\_metadata `paykit.checkouts.create` requires `currency`, `amount`, and `itemName` in `provider_metadata` because PayPal orders need explicit line-item data: ```typescript theme={null} // checkout.provider_metadata — currency, amount, and itemName are required await paykit.checkouts.create({ customer: { email: 'user@example.com' }, item_id: 'sku_abc123', session_type: 'one_time', quantity: 2, success_url: 'https://example.com/success', cancel_url: 'https://example.com/cancel', provider_metadata: { currency: 'USD', // required amount: '29.00', // required — total order amount as string itemName: 'T-Shirt', // required — display name shown in PayPal UI }, }); // refunds — amount is optional (defaults to full capture amount) await paykit.refunds.create({ payment_id: 'order_abc123', amount: 1000, // in currency units (e.g. cents) }); ``` ## Escape hatch `paykit._native` returns the raw PayPal SDK client, so you can reach anything PayKit does not map yet without leaving PayKit. Providers without an official SDK return [our HTTP client](/concepts/http-client) instead. ```typescript theme={null} import { OrdersController } from '@paypal/paypal-server-sdk'; const orders = new OrdersController(paykit._native); ``` # Paystack Source: https://docs.usepaykit.dev/providers/paystack Paystack provider for PayKit. ```bash theme={null} npm install @paykit-sdk/paystack ``` ## Setup ```typescript theme={null} import { PayKit } from '@paykit-sdk/core'; import { paystack } from '@paykit-sdk/paystack'; export const paykit = new PayKit(paystack()); ``` Required env vars: ```bash theme={null} PAYSTACK_SECRET_KEY=sk_test_... PAYSTACK_WEBHOOK_SECRET=your-webhook-secret ``` ```typescript theme={null} import { PayKit } from '@paykit-sdk/core'; import { createPaystack } from '@paykit-sdk/paystack'; export const paykit = new PayKit( createPaystack({ secretKey: 'sk_test_...', isSandbox: true }), ); ``` ## How it works ```typescript theme={null} const checkout = await paykit.checkouts.create({ customer: { email: 'user@example.com' }, item_id: 'PLN_abc123', session_type: 'one_time', quantity: 1, success_url: 'https://example.com/success', cancel_url: 'https://example.com/cancel', metadata: null, provider_metadata: { amount: 500000, // in kobo (NGN) or smallest currency unit currency: 'NGN', }, }); // Redirect the customer to checkout.payment_url // Once they're back, confirm the payment const payment = await paykit.payments.retrieve(checkout.id); ``` ## Webhooks Enable these events in your Paystack dashboard: * `charge.success` * `charge.failed` * `customer.create` * `customeridentification.success` * `customeridentification.failed` * `subscription.create` * `subscription.not_renew` * `subscription.disable` * `invoice.create` * `invoice.update` * `invoice.payment_failed` * `refund.pending` * `refund.processed` * `refund.failed` Paystack signs webhooks with HMAC-SHA512 via the `x-paystack-signature` header. ```typescript theme={null} const webhook = paykit.webhooks .setup({ webhookSecret: process.env.PAYSTACK_WEBHOOK_SECRET! }) .on('payment.created', async event => { /* charge.success */ }) .on('payment.failed', async event => { /* charge.failed or invoice.payment_failed */ }) .on('subscription.created', async event => { /* ... */ }) .on('customer.created', async event => { /* ... */ }) .on('invoice.generated', async event => { /* ... */ }); await webhook.handle({ body: rawBody, headersAsObject: Object.fromEntries(request.headers), fullUrl: request.url, }); ``` | Raw event | PayKit event | | ------------------------------------------------ | --------------------------------------- | | `paystack.charge.success` | `payment.updated` + `invoice.generated` | | `paystack.charge.failed` | `payment.failed` | | `paystack.customer.create` | `customer.created` | | `paystack.customeridentification.success/failed` | `customer.updated` | | `paystack.subscription.create` | `subscription.created` | | `paystack.subscription.not_renew/disable` | `subscription.canceled` | | `paystack.invoice.create/update` | `payment.created` | | `paystack.invoice.payment_failed` | `payment.failed` | | `paystack.refund.pending/processed/failed` | `refund.created` | Everything else — disputes, dedicated accounts, payment requests, transfers — is available as a raw event only. ### Raw Paystack events Opt into any native Paystack event — fully typed against Paystack's API types: ```typescript theme={null} paykit.webhooks .setup({ webhookSecret: process.env.PAYSTACK_WEBHOOK_SECRET! }) .on('paystack.charge.success', async event => { // event.data is typed as PaystackTransaction }) .on('paystack.subscription.create', async event => { // event.data is typed as PaystackSubscription }) .on('paystack.refund.processed', async event => { // event.data is typed as PaystackRefund }); ``` All available raw events: | Event | Data type | | ------------------------------------------------------- | -------------------------------- | | `paystack.charge.success` | `PaystackTransaction` | | `paystack.charge.dispute.create/remind/resolve` | `PaystackDispute` | | `paystack.customeridentification.success/failed` | `PaystackCustomerIdentification` | | `paystack.dedicatedaccount.assign.success/failed` | `PaystackDVAAssignment` | | `paystack.invoice.create/update/payment_failed` | `PaystackInvoice` | | `paystack.paymentrequest.pending/success` | `PaystackPaymentRequest` | | `paystack.refund.failed/pending/processed/processing` | `PaystackRefund` | | `paystack.subscription.create/enable/disable/not_renew` | `PaystackSubscription` | | `paystack.transfer.success/failed/reversed` | `PaystackTransfer` | ## provider\_metadata ```typescript theme={null} // checkout.provider_metadata is typed as { amount?: number; currency?: string } await paykit.checkouts.create({ customer: { email: 'user@example.com' }, item_id: 'PLN_abc123', session_type: 'one_time', quantity: 1, success_url: '...', cancel_url: '...', provider_metadata: { amount: 500000, // override amount in kobo currency: 'NGN', }, }); // refund.provider_metadata is typed as { merchant_note?: string; customer_note?: string } // Paystack requires a merchant_note. The SDK will prioritize provider_metadata.merchant_note, // fall back to the universal `reason` field, or default to "Duplicate charge". await paykit.refunds.create({ payment_id: 'ref_abc123', amount: 5000, reason: 'Customer requested cancellation', metadata: null, provider_metadata: { customer_note: 'Sorry for the inconvenience', }, }); ``` ## Escape hatch `paykit._native` returns PayKit's [HTTP client](/concepts/http-client), already pointed at Paystack and authenticated with your credentials. Use it to call endpoints PayKit does not map yet. ```typescript theme={null} const result = await paykit._native.get( '/transaction/verify/ref_123', ); if (result.ok) console.log(result.value); ``` # Polar Source: https://docs.usepaykit.dev/providers/polar Polar provider for PayKit. ```bash theme={null} npm install @paykit-sdk/polar ``` ## Setup ```typescript theme={null} import { PayKit } from '@paykit-sdk/core'; import { polar } from '@paykit-sdk/polar'; export const paykit = new PayKit(polar()); ``` Required env vars: ```bash theme={null} POLAR_ACCESS_TOKEN=polar_oat_... POLAR_WEBHOOK_SECRET=your-webhook-secret ``` ```typescript theme={null} import { PayKit } from '@paykit-sdk/core'; import { createPolar } from '@paykit-sdk/polar'; export const paykit = new PayKit( createPolar({ accessToken: 'polar_oat_...', isSandbox: true }), ); ``` ## How it works ```typescript theme={null} const checkout = await paykit.checkouts.create({ customer: { email: 'user@example.com' }, item_id: 'product_abc123', session_type: 'one_time', quantity: 1, success_url: 'https://example.com/success', cancel_url: 'https://example.com/cancel', }); // Redirect the customer to checkout.payment_url ``` Subscriptions are created through checkouts, not `paykit.subscriptions.create` directly — Polar fires `subscription.created` once the customer completes a subscription checkout. ## Webhooks Enable these events in your Polar dashboard: * `order.paid` * `order.created` * `customer.created` * `customer.updated` * `customer.deleted` * `subscription.created` * `subscription.updated` * `subscription.revoked` * `refund.created` ```typescript theme={null} const webhook = paykit.webhooks .setup({ webhookSecret: process.env.POLAR_WEBHOOK_SECRET! }) .on('payment.created', async event => { /* order.created */ }) .on('payment.succeeded', async event => { /* order.paid */ }) .on('subscription.created', async event => { /* subscription.created */ }) .on('subscription.updated', async event => { /* subscription.updated */ }) .on('subscription.canceled', async event => { /* subscription.revoked */ }) .on('customer.created', async event => { /* customer.created */ }) .on('customer.updated', async event => { /* customer.updated */ }) .on('customer.deleted', async event => { /* customer.deleted */ }) .on('invoice.generated', async event => { /* emitted alongside payment.succeeded on order.paid */ }) .on('refund.created', async event => { /* refund.created */ }); await webhook.handle({ body: rawBody, headersAsObject: Object.fromEntries(request.headers), fullUrl: request.url, }); ``` ### Raw Polar events Every incoming Polar event is also emitted as a `polar.` raw event — typed via the `@polar-sh/sdk` webhook types: ```typescript theme={null} paykit.webhooks .setup({ webhookSecret: process.env.POLAR_WEBHOOK_SECRET! }) .on('polar.order.paid', async event => { // event.data is typed as Polar Order }) .on('polar.subscription.revoked', async event => { // event.data is typed as Polar Subscription }) .on('polar.refund.created', async event => { // event.data is typed as Polar Refund }); ``` Polar event to PayKit event mappings: | Polar event | PayKit events emitted | | ---------------------------- | ----------------------------------------- | | `polar.order.created` | `payment.created` | | `polar.order.paid` | `payment.succeeded` + `invoice.generated` | | `polar.customer.created` | `customer.created` | | `polar.customer.updated` | `customer.updated` | | `polar.customer.deleted` | `customer.deleted` | | `polar.subscription.created` | `subscription.created` | | `polar.subscription.updated` | `subscription.updated` | | `polar.subscription.revoked` | `subscription.canceled` | | `polar.refund.created` | `refund.created` | ## provider\_metadata `provider_metadata` for Polar is passed through directly to the underlying Polar SDK call, giving you full access to any field the Polar API accepts: ```typescript theme={null} // checkout.provider_metadata is spread into Polar's CheckoutCreate await paykit.checkouts.create({ customer: { email: 'user@example.com' }, item_id: 'product_abc123', session_type: 'one_time', quantity: 1, success_url: 'https://example.com/success', cancel_url: 'https://example.com/cancel', provider_metadata: { allowDiscountCodes: true, discountId: 'discount_xyz', }, }); // customer.provider_metadata is spread into Polar's CustomerCreate / CustomerUpdate await paykit.customers.create({ email: 'user@example.com', provider_metadata: { externalId: 'usr_123', }, }); // updateSubscription.provider_metadata is REQUIRED and must be one of these shapes: await paykit.subscriptions.update('sub_123', { provider_metadata: { productId: 'product_456' }, // change product }); await paykit.subscriptions.update('sub_123', { provider_metadata: { discountId: 'discount_789' }, // apply discount }); await paykit.subscriptions.update('sub_123', { provider_metadata: { trialEnd: new Date('2025-12-31') }, // extend trial }); // refund.provider_metadata is spread into Polar's refund create await paykit.refunds.create({ payment_id: 'order_abc123', amount: 1000, reason: 'customer_request', }); ``` ## Escape hatch `paykit._native` returns the raw Polar SDK client, so you can reach anything PayKit does not map yet without leaving PayKit. Providers without an official SDK return [our HTTP client](/concepts/http-client) instead. ```typescript theme={null} const checkout = await paykit._native.checkouts.get({ id: 'chk_123', }); ``` # Razorpay Source: https://docs.usepaykit.dev/providers/razorpay Razorpay provider for PayKit. ```bash theme={null} npm install @paykit-sdk/razorpay ``` ## Setup ```typescript theme={null} import { PayKit } from '@paykit-sdk/core'; import { razorpay } from '@paykit-sdk/razorpay'; export const paykit = new PayKit(razorpay()); ``` Required env vars: ```bash theme={null} RAZORPAY_KEY_ID=rzp_test_... RAZORPAY_KEY_SECRET=... ``` ```typescript theme={null} import { PayKit } from '@paykit-sdk/core'; import { createRazorpay } from '@paykit-sdk/razorpay'; export const paykit = new PayKit( createRazorpay({ keyId: 'rzp_test_...', keySecret: '...', isSandbox: true }), ); ``` ## How it works ```typescript theme={null} const checkout = await paykit.checkouts.create({ customer: { email: 'user@example.com' }, item_id: 'my-product', session_type: 'one_time', quantity: 1, success_url: 'https://example.com/success', cancel_url: 'https://example.com/cancel', provider_metadata: { amount: '40000', currency: 'INR' }, }); // Redirect the customer to checkout.payment_url // Once they're back, confirm the payment const payment = await paykit.payments.retrieve(checkout.id); ``` Checkouts and one-off payments are both built on Razorpay's Payment Links. Subscriptions use a pre-existing Razorpay Plan instead — `item_id` maps to that plan's `plan_id`: ```typescript theme={null} const subscription = await paykit.subscriptions.create({ customer: { email: 'user@example.com' }, item_id: 'plan_00000000000001', quantity: 1, billing_interval: 'month', amount: 49900, currency: 'INR', metadata: null, provider_metadata: { total_count: 12 }, }); // Redirect the customer to subscription.payment_url to authorize the mandate ``` ## Webhooks Razorpay signs webhooks with a **webhook secret** you configure separately in the Razorpay dashboard — it's not the same as your API key secret. ```typescript theme={null} const webhook = paykit.webhooks .setup({ webhookSecret: process.env.RAZORPAY_WEBHOOK_SECRET! }) .on('payment.succeeded', async event => { /* payment.captured, order.paid, subscription.charged */ }) .on('payment.failed', async event => { /* payment.failed */ }) .on('refund.created', async event => { /* refund.created or refund.processed */ }) .on('subscription.updated', async event => { /* subscription.authenticated, activated, updated, resumed */ }) .on('subscription.canceled', async event => { /* subscription.completed or subscription.cancelled */ }); await webhook.handle({ body: rawBody, headersAsObject: Object.fromEntries(request.headers), fullUrl: request.url, }); ``` Razorpay sends the signature in the `x-razorpay-signature` header: `HMAC-SHA256(webhookSecret, rawRequestBody)`. ### Raw Razorpay events Opt into any native Razorpay event — fully typed against Razorpay's webhook payload shape: ```typescript theme={null} paykit.webhooks .setup({ webhookSecret: process.env.RAZORPAY_WEBHOOK_SECRET! }) .on('razorpay.payment.captured', async event => { // event.data is the full Razorpay webhook envelope }) .on('razorpay.payment_link.paid', async event => { // event.data.payload.payment_link.entity is typed as RazorpayPaymentLink }); ``` Every webhook always emits its raw event first, alongside the matching standard PayKit event below where one applies. | Raw event | PayKit event | | --------------------------------------------------------------------- | ----------------------------------------- | | `razorpay.payment.authorized` | `payment.updated` | | `razorpay.payment.captured` | `payment.succeeded` + `invoice.generated` | | `razorpay.payment.failed` | `payment.failed` | | `razorpay.order.paid` | `payment.succeeded` + `invoice.generated` | | `razorpay.refund.created` / `razorpay.refund.processed` | `refund.created` | | `razorpay.payment_link.paid` | `payment.succeeded` | | `razorpay.payment_link.cancelled` / `razorpay.payment_link.expired` | *(raw only)* | | `razorpay.subscription.authenticated/activated/updated/resumed` | `subscription.updated` | | `razorpay.subscription.charged` | `payment.succeeded` + `invoice.generated` | | `razorpay.subscription.completed` / `razorpay.subscription.cancelled` | `subscription.canceled` | | `razorpay.subscription.pending/halted/paused` | *(raw only)* | Razorpay has no delete-customer API and no cancel-payment API — use `createRefund` to reverse a captured payment instead of canceling it. ## provider\_metadata ```typescript theme={null} // checkout/payment.provider_metadata requires amount and currency (checkout) // or success_url (payment) await paykit.checkouts.create({ customer: { email: 'user@example.com' }, item_id: 'my-product', session_type: 'one_time', quantity: 1, success_url: 'https://example.com/success', cancel_url: 'https://example.com/cancel', provider_metadata: { amount: '40000', // in the smallest currency unit (e.g. paise), as a string currency: 'INR', }, }); // subscription.provider_metadata requires total_count or end_at await paykit.subscriptions.create({ customer: { email: 'user@example.com' }, item_id: 'plan_00000000000001', quantity: 1, billing_interval: 'month', amount: 49900, currency: 'INR', metadata: null, provider_metadata: { total_count: 12, // number of billing cycles }, }); ``` ## Escape hatch `paykit._native` returns PayKit's [HTTP client](/concepts/http-client), already pointed at Razorpay and authenticated with your credentials. Use it to call endpoints PayKit does not map yet. ```typescript theme={null} const result = await paykit._native.get('/payments/pay_123'); if (result.ok) console.log(result.value); ``` # Redsys Source: https://docs.usepaykit.dev/providers/redsys Redsys inSite provider for PayKit. ```bash theme={null} npm install @paykit-sdk/redsys ``` ## Setup ```typescript theme={null} import { PayKit } from '@paykit-sdk/core'; import { redsys } from '@paykit-sdk/redsys'; export const paykit = new PayKit(redsys()); ``` Required env vars: ```bash theme={null} REDSYS_MERCHANT_CODE=your-merchant-code REDSYS_TERMINAL=your-terminal-number REDSYS_SECRET_KEY=your-hmac-secret-key REDSYS_TRANSACTION_TYPE=0 ``` `isSandbox` is inferred from `NODE_ENV` — set `NODE_ENV=production` for live mode. ```typescript theme={null} import { PayKit } from '@paykit-sdk/core'; import { createRedsys } from '@paykit-sdk/redsys'; export const paykit = new PayKit( createRedsys({ merchantCode: 'your-merchant-code', terminal: 'your-terminal-number', secretKey: 'your-hmac-secret-key', isSandbox: true, transactionType: '0', // '0' = immediate capture, '1' = pre-authorization }), ); ``` ## How it works Redsys uses an **inSite** iframe flow — customers enter card details directly on your page via secure iframes hosted by Redsys, with no redirect to an external payment page. 1. **`paykit.checkouts.create`** — generates HMAC-signed merchant parameters for the inSite iframe. 2. **Frontend loads inSite** — your page loads `redsysV3.js` and renders the card input form. 3. **User enters card** — Redsys returns an `operationId` via callback. 4. **`paykit.payments.create`** — your backend calls the Redsys REST API with the `operationId` to execute the charge. 5. **Webhooks** — Redsys POSTs server-side notifications to confirm payment status. Redsys doesn't support customers, subscriptions, or retrieving/updating checkouts and payments. Capture is only available when `transactionType` is set to `'1'` (pre-authorization). Supported currencies: `EUR`, `USD`, `GBP`, `JPY`. ## Frontend integration After `paykit.checkouts.create`, the checkout metadata includes everything the inSite iframe needs: ```typescript theme={null} const checkout = await paykit.checkouts.create({ customer: { email: 'user@example.com' }, item_id: 'prod_123', session_type: 'one_time', quantity: 1, metadata: { amount: 2500, // amount in cents currency: 'EUR', }, }); const { redsys_merchant_params, redsys_signature, redsys_signature_version, redsys_merchant_code, redsys_terminal, } = checkout.metadata; ``` Load the Redsys inSite script and render the form: ```html theme={null}
``` Then on your backend, pass `operationId` in `provider_metadata`: ```typescript theme={null} await paykit.payments.create({ customer: { email: 'user@example.com' }, item_id: 'prod_123', amount: 2500, currency: 'EUR', metadata: checkout.metadata, // pass through from createCheckout provider_metadata: { operationId: 'the-operation-id-from-insite', }, }); ``` ## Webhooks Redsys sends a POST with `Ds_MerchantParameters`, `Ds_Signature`, and `Ds_SignatureVersion`. PayKit verifies the HMAC signature automatically before emitting events. ```typescript theme={null} const webhook = paykit.webhooks .setup({ webhookSecret: process.env.REDSYS_SECRET_KEY! }) .on('payment.succeeded', async event => { /* Ds_Response === '0000' or starts with '00' */ }) .on('payment.failed', async event => { /* any non-success Ds_Response code */ }); await webhook.handle({ body: rawBody, headersAsObject: Object.fromEntries(request.headers), fullUrl: request.url, }); ``` Redsys response codes and their PayKit event mappings: | Ds\_Response | PayKit event emitted | | --------------- | -------------------- | | `0000` / `00xx` | `payment.succeeded` | | Any other code | `payment.failed` | ### Raw Redsys events Listen for native Redsys webhook data via the `redsys.*` namespace: ```typescript theme={null} paykit.webhooks .setup({ webhookSecret: process.env.REDSYS_SECRET_KEY! }) .on('redsys.payment.succeeded', async event => { // event.data: { order_id, amount, response_code, customer_id } }) .on('redsys.payment.failed', async event => { // event.data: { order_id, response_code, error_message } }); ``` ## Refunds ```typescript theme={null} await paykit.refunds.create({ payment_id: 'order-id', amount: 2500, metadata: { currency: 'EUR', // required }, }); ``` The `metadata` passed to `paykit.refunds.create` must include `currency`. The `orderId` is automatically extracted from the payment's stored metadata. ## Escape hatch Redsys has no public API client to hand back, so `_native` throws. Other providers return either their official SDK or [our HTTP client](/concepts/http-client). # Remita Source: https://docs.usepaykit.dev/providers/remita Remita provider for PayKit. ```bash theme={null} npm install @paykit-sdk/core @paykit-sdk/remita ``` Remita's "Invoice Generation" API is a Remita Retrieval Reference (RRR) based collection flow, not a hosted checkout — `createPayment` generates an RRR that the payer completes through Remita's own channels (bank transfer, USSD, card, agent), outside of this integration. There is no `payment_url`, no customer-object API, and no recurring/subscription API (Remita's recurring billing is a separate Direct Debit product with its own auth/endpoints, out of scope here). ## Setup ```typescript theme={null} import { PayKit } from '@paykit-sdk/core'; import { remita } from '@paykit-sdk/remita'; export const paykit = new PayKit(remita()); ``` Required env vars: ```bash theme={null} REMITA_MERCHANT_ID=... REMITA_API_KEY=... REMITA_SERVICE_TYPE_ID=... REMITA_SANDBOX=true ``` `REMITA_BASE_URL` is required once `REMITA_SANDBOX=false` — Remita does not publish a fixed production base URL for this API; it's issued per-merchant after KYC/go-live, from the Administration Menu → "API Keys and Webhooks" page. ```typescript theme={null} import { PayKit } from '@paykit-sdk/core'; import { createRemita } from '@paykit-sdk/remita'; export const paykit = new PayKit( createRemita({ merchantId: '...', apiKey: '...', serviceTypeId: '...', isSandbox: true, }), ); ``` ## How it works ```typescript theme={null} const payment = await paykit.payments.create({ customer: { email: 'payer@example.com' }, amount: 20000, currency: 'NGN', item_id: 'invoice-1', capture_method: 'automatic', // Remita has no manual capture provider_metadata: { payerName: 'John Doe', // required — Remita has no customer API to source this from payerPhone: '09062067384', // required // serviceTypeId: 'override', // optional — overrides the provider-level default // expiryDate: '31/12/2026', // optional, format DD/MM/YYYY }, }); // payment.id is the RRR - direct your payer to complete payment // against it through Remita's channels. payment.payment_url is // always null; there is no hosted redirect. // Later, check status const status = await paykit.payments.retrieve(payment.id); ``` `payerName` and `payerPhone` must be supplied in `provider_metadata` because PayKit's customer object only carries an email/id, and Remita's Invoice Generation API requires both. Remita's status API (`status.reg`) returns only `{ amount, RRR, orderId, message, transactiontime, status, paymentDate? }` — it does not echo back the payer's email, `item_id`, or `metadata`. Those fields are only populated on the object returned synchronously from `createPayment`; a subsequent `retrievePayment` always returns `customer: null`, `item_id: null`, `metadata: {}`. Only status codes `00` and `01` mean the RRR has been paid (per Remita's own docs) — everything else is treated as `pending` unless it matches one of Remita's documented failure codes. `sender`/customer details and subscriptions aren't supported — Remita has no customer-object API, so full payer data must be supplied per payment, and recurring billing is a separate Remita product (Direct Debit mandates) out of scope for this integration. ## Canceling a payment ```typescript theme={null} await paykit.payments.cancel(payment.id); // RRR ``` Cancels an **unpaid** RRR via Remita's "Cancel Invoice" endpoint — the only post-creation mutation this API supports. There's no way to amend payer details or partially update a payment reference. ## Webhooks Configure a "listening URL" in Remita's dashboard (Administration Menu → API Keys and Webhooks). Remita POSTs a JSON **array** of notifications to it — there is no signature or shared secret on this payload, so `webhookSecret` is unused. Every notification is re-verified against the status API before a standardized event is emitted, since the payload itself can't be authenticated: ```typescript theme={null} const webhook = paykit.webhooks .setup({ webhookSecret: null }) // unused - Remita has no signature .on('payment.succeeded', async event => { /* re-verified status 00/01 */ }) .on('payment.failed', async event => { /* re-verified as a documented failure code */ }) .on('payment.updated', async event => { /* re-verified as still pending */ }); await webhook.handle({ body: await request.text(), headersAsObject: Object.fromEntries(request.headers), fullUrl: request.url, }); ``` ### Raw Remita events ```typescript theme={null} paykit.webhooks .setup({ webhookSecret: null }) .on('remita.notification', async event => { // event.data is the raw Remita notification payload, unverified }); ``` **Caveat:** Remita's docs ask your endpoint to reply with the literal text `"Ok"` (or `"Not Ok"`). `webhook.handle()` always sends its own response, so if strict compliance with that contract matters, send the literal text from your own route handler after calling `webhook.handle()`. ## Unsupported operations `createCheckout` / `retrieveCheckout` / `updateCheckout` / `deleteCheckout`, all customer operations, all subscription operations, `updatePayment`, `deletePayment`, `capturePayment`, and `createRefund` throw `ProviderNotSupportedError` — Remita's Invoice Generation API has no hosted checkout URL, no customer-object API, no subscription API, no amend/delete endpoint beyond cancellation, no manual capture step, and no refund endpoint. Use `createPayment` / `cancelPayment` directly instead. ## Escape hatch `paykit._native` returns PayKit's [HTTP client](/concepts/http-client), already pointed at Remita and authenticated with your credentials. Use it to call endpoints PayKit does not map yet. ```typescript theme={null} const result = await paykit._native.get( '/echannelsvc/rrr_123/status', ); if (result.ok) console.log(result.value); ``` # Stripe Source: https://docs.usepaykit.dev/providers/stripe Stripe provider for PayKit. ```bash theme={null} npm install @paykit-sdk/stripe ``` ## Setup ```typescript theme={null} import { PayKit } from '@paykit-sdk/core'; import { stripe } from '@paykit-sdk/stripe'; export const paykit = new PayKit(stripe()); ``` Required env vars: ```bash theme={null} STRIPE_API_KEY=sk_test_... STRIPE_WEBHOOK_SECRET=whsec_... ``` ```typescript theme={null} import { PayKit } from '@paykit-sdk/core'; import { createStripe } from '@paykit-sdk/stripe'; export const paykit = new PayKit( createStripe({ apiKey: 'sk_test_...', isSandbox: true }), ); ``` ## Webhooks Enable these events in your Stripe dashboard: * `checkout.session.completed` * `customer.created` * `customer.updated` * `customer.deleted` * `customer.subscription.created` * `customer.subscription.updated` * `customer.subscription.deleted` * `payment_intent.created` * `payment_intent.succeeded` * `payment_intent.canceled` * `payment_intent.processing` * `payment_intent.requires_action` * `payment_intent.amount_capturable_updated` * `payment_intent.payment_failed` * `invoice.paid` * `refund.created` ```typescript theme={null} const webhook = paykit.webhooks .setup({ webhookSecret: process.env.STRIPE_WEBHOOK_SECRET! }) .on('payment.created', async event => { /* payment_intent.created */ }) .on('payment.succeeded', async event => { /* payment_intent.succeeded */ }) .on('payment.failed', async event => { /* payment_intent.payment_failed or payment_intent.canceled */ }) .on('subscription.created', async event => { /* ... */ }) .on('customer.created', async event => { /* ... */ }) .on('invoice.generated', async event => { /* ... */ }); await webhook.handle({ body: rawBody, headersAsObject: Object.fromEntries(request.headers), fullUrl: request.url, }); ``` ### Raw Stripe events Opt into any native Stripe event — fully typed against the Stripe SDK types: ```typescript theme={null} paykit.webhooks .setup({ webhookSecret: process.env.STRIPE_WEBHOOK_SECRET! }) .on('stripe.checkout.session.completed', async event => { // event.data is typed as Stripe.Checkout.Session console.log(event.data.payment_intent); }) .on('stripe.customer.subscription.trial_will_end', async event => { // event.data is typed as Stripe.Subscription }); ``` All Stripe event types (`stripe.`) are available and typed. A subset also emit a standard PayKit event: | Raw event | PayKit event | | ------------------------------------------------- | ------------------------------------------------ | | `stripe.checkout.session.completed` | `invoice.generated` (one-time checkouts only) | | `stripe.invoice.paid` | `invoice.generated` (new/renewing subscriptions) | | `stripe.customer.created` | `customer.created` | | `stripe.customer.updated` | `customer.updated` | | `stripe.customer.deleted` | `customer.deleted` | | `stripe.customer.subscription.created` | `subscription.created` | | `stripe.customer.subscription.updated` | `subscription.updated` | | `stripe.customer.subscription.deleted` | `subscription.canceled` | | `stripe.payment_intent.created` | `payment.created` | | `stripe.payment_intent.succeeded` | `payment.succeeded` | | `stripe.payment_intent.canceled` | `payment.failed` | | `stripe.payment_intent.payment_failed` | `payment.failed` | | `stripe.payment_intent.processing` | `payment.updated` | | `stripe.payment_intent.requires_action` | `payment.updated` | | `stripe.payment_intent.amount_capturable_updated` | `payment.updated` | | `stripe.payment_intent.partially_funded` | `payment.updated` | | `stripe.refund.created` | `refund.created` | Everything else — including `stripe.charge.*` — is available as a raw event only. ## provider\_metadata `provider_metadata` for Stripe operations is typed directly against the Stripe SDK params: ```typescript theme={null} // checkout.provider_metadata is typed as Stripe.Checkout.SessionCreateParams await paykit.checkouts.create({ customer: 'cus_123', item_id: 'price_123', session_type: 'one_time', quantity: 1, success_url: '...', cancel_url: '...', provider_metadata: { tax_id_collection: { enabled: true }, payment_method_collection: 'always', allow_promotion_codes: true, }, }); // customer.provider_metadata is typed as Stripe.CustomerCreateParams await paykit.customers.create({ email: 'user@example.com', provider_metadata: { preferred_locales: ['en'], tax_exempt: 'exempt', }, }); // refund.provider_metadata is typed as Stripe.RefundCreateParams await paykit.refunds.create({ payment_id: 'pi_123', amount: 1000, provider_metadata: { reason: 'fraudulent', }, }); ``` ## Escape hatch `paykit._native` returns the raw Stripe SDK client, so you can reach anything PayKit does not map yet without leaving PayKit. Providers without an official SDK return [our HTTP client](/concepts/http-client) instead. ```typescript theme={null} const session = await paykit._native.checkout.sessions.retrieve('cs_123'); ``` # Xendit Source: https://docs.usepaykit.dev/providers/xendit Xendit provider for PayKit. ```bash theme={null} npm install @paykit-sdk/xendit ``` ## Setup ```typescript theme={null} import { PayKit } from '@paykit-sdk/core'; import { xendit } from '@paykit-sdk/xendit'; export const paykit = new PayKit(xendit()); ``` Required env vars: ```bash theme={null} XENDIT_SECRET_KEY=xnd_development_... ``` ```typescript theme={null} import { PayKit } from '@paykit-sdk/core'; import { createXendit } from '@paykit-sdk/xendit'; export const paykit = new PayKit( createXendit({ secretKey: 'xnd_development_...', isSandbox: true }), ); ``` ## How it works ```typescript theme={null} const checkout = await paykit.checkouts.create({ customer: { email: 'user@example.com' }, item_id: 'my-product', session_type: 'one_time', quantity: 1, success_url: 'https://example.com/success', cancel_url: 'https://example.com/cancel', provider_metadata: { amount: '400', currency: 'IDR' }, }); // Redirect the customer to checkout.payment_url // Once they're back, confirm the payment const payment = await paykit.payments.retrieve(checkout.id); ``` Checkouts and one-off payments are both built on Xendit's hosted Invoices. Xendit's Recurring Plans API has no separate reusable plan template — each plan already carries its own amount/currency/schedule, and requires a pre-created Xendit customer id plus at least one saved payment token: ```typescript theme={null} const subscription = await paykit.subscriptions.create({ customer: { id: 'cust_xendit_id' }, item_id: 'My Newspaper Subscription', quantity: 1, billing_interval: 'month', amount: 50000, currency: 'IDR', metadata: null, provider_metadata: { payment_tokens: [{ payment_token_id: 'pt-...', rank: 1 }], }, }); ``` ## Webhooks Xendit verifies webhooks with a **Callback Verification Token** copied from your dashboard — a plain string comparison via the `x-callback-token` header, not an HMAC signature. ```typescript theme={null} const webhook = paykit.webhooks .setup({ webhookSecret: process.env.XENDIT_CALLBACK_TOKEN! }) .on('payment.succeeded', async event => { /* invoice status PAID/SETTLED, or a succeeded recurring cycle */ }) .on('payment.failed', async event => { /* invoice status EXPIRED, or a failed recurring cycle */ }) .on('subscription.updated', async event => { /* recurring.plan.activated */ }) .on('subscription.canceled', async event => { /* recurring.plan.inactivated */ }); await webhook.handle({ body: rawBody, headersAsObject: Object.fromEntries(request.headers), fullUrl: request.url, }); ``` ### Raw Xendit events Opt into any native Xendit notification — fully typed against Xendit's callback payload shape: ```typescript theme={null} paykit.webhooks .setup({ webhookSecret: process.env.XENDIT_CALLBACK_TOKEN! }) .on('xendit.invoice.paid', async event => { // event.data is the full Xendit invoice resource }) .on('xendit.recurring.cycle.succeeded', async event => { // event.data.data is the recurring cycle resource }); ``` Every webhook always emits its raw event first, alongside the matching standard PayKit event below where one applies. Invoice callbacks are sent as the raw invoice resource itself (no `event`/`data` wrapper); Recurring webhooks use a wrapping `{ event, business_id, created, data }` envelope instead — Xendit's two product lines don't share a webhook shape. | Raw event | PayKit event | | ----------------------------------------- | ----------------------------------------- | | `xendit.invoice.pending` | `payment.created` | | `xendit.invoice.paid` | `payment.succeeded` + `invoice.generated` | | `xendit.invoice.settled` | `payment.succeeded` + `invoice.generated` | | `xendit.invoice.expired` | `payment.failed` | | `xendit.recurring.plan.activated` | `subscription.updated` | | `xendit.recurring.plan.inactivated` | `subscription.canceled` | | `xendit.recurring.cycle.succeeded` | `payment.succeeded` + `invoice.generated` | | `xendit.recurring.cycle.failed` | `payment.failed` | | `xendit.recurring.cycle.created/retrying` | *(raw only)* | Xendit has no delete-customer API and no manual payment-capture API — `deleteCustomer` and `capturePayment` throw `ProviderNotSupportedError`. Use `cancelPayment` to expire a pending invoice instead. ## provider\_metadata ```typescript theme={null} // checkout/payment.provider_metadata requires amount and currency (checkout) // or success_url (payment) await paykit.checkouts.create({ customer: { email: 'user@example.com' }, item_id: 'my-product', session_type: 'one_time', quantity: 1, success_url: 'https://example.com/success', cancel_url: 'https://example.com/cancel', provider_metadata: { amount: '400', // in the standard currency unit, as a string currency: 'IDR', }, }); // subscription.provider_metadata requires payment_tokens await paykit.subscriptions.create({ customer: { id: 'cust_xendit_id' }, item_id: 'My Newspaper Subscription', quantity: 1, billing_interval: 'month', amount: 50000, currency: 'IDR', metadata: null, provider_metadata: { payment_tokens: [{ payment_token_id: 'pt-...', rank: 1 }], }, }); ``` ## Escape hatch `paykit._native` returns PayKit's [HTTP client](/concepts/http-client), already pointed at Xendit and authenticated with your credentials. Use it to call endpoints PayKit does not map yet. ```typescript theme={null} const result = await paykit._native.get('/invoices/inv_123'); if (result.ok) console.log(result.value); ``` # Quick Start Source: https://docs.usepaykit.dev/quickstart Up and running in under a minute. ## 1. Install ```bash theme={null} npm install @paykit-sdk/core @paykit-sdk/stripe ``` ## 2. Create a PayKit instance ```typescript filename="lib/paykit.ts" theme={null} import { PayKit, createEndpointHandlers } from '@paykit-sdk/core'; import { stripe } from '@paykit-sdk/stripe'; export const paykit = new PayKit(stripe()); export const endpoints = createEndpointHandlers(paykit); ``` ## 3. Mount the catch-all route `createEndpointHandlers` generates handlers for every PayKit resource. Mount them behind a single catch-all API route: ```typescript filename="app/api/paykit/[...endpoint]/route.ts" theme={null} import { endpoints } from '@/lib/paykit'; import type { EndpointArgs, EndpointHandler, EndpointPath, } from '@paykit-sdk/core'; import { NextRequest, NextResponse } from 'next/server'; export async function POST( request: NextRequest, { params }: { params: Promise<{ endpoint: string[] }> }, ) { const { endpoint: endpointArray } = await params; const endpoint = ('/' + endpointArray.join('/')) as EndpointPath; const handler = endpoints[endpoint] as EndpointHandler< typeof endpoint >; if (!handler) { return NextResponse.json( { message: 'Endpoint not found' }, { status: 404 }, ); } const body = await request.json(); const { args } = body as { args: EndpointArgs }; try { const result = await handler(...args); return NextResponse.json({ result }); } catch (error) { return NextResponse.json( { message: error instanceof Error ? error.message : 'Internal server error', }, { status: 500 }, ); } } ``` ## 4. Handle webhooks ```typescript filename="app/api/paykit/webhook/route.ts" theme={null} const webhook = paykit.webhooks .setup({ webhookSecret: process.env.STRIPE_WEBHOOK_SECRET! }) .on('payment.created', async event => { console.log(event.data); }); await webhook.handle({ body: rawBody, headersAsObject: Object.fromEntries(request.headers), fullUrl: request.url, }); ``` ## Swap providers Change one import and everything else stays the same: ```typescript theme={null} import { paystack } from '@paykit-sdk/paystack'; export const paykit = new PayKit(paystack()); ``` See [Providers](/providers/overview) for the full list.