Payments & Stripe
The Booking Kit provides payment logic through the PaymentAdapter interface and pure computation functions for deposits, cancellation policies, and fees.
For a step-by-step deposit walkthrough, see the Deposits guide.
Payment adapter
Section titled “Payment adapter”The PaymentAdapter interface abstracts payment processing. connectedAccountId is honored on every method for Stripe Connect routing:
interface PaymentAdapter { createPaymentIntent(options: CreatePaymentIntentOptions): Promise<CreatePaymentIntentResult>; createSetupIntent(options: CreateSetupIntentOptions): Promise<CreateSetupIntentResult>; capturePaymentIntent(paymentIntentId: string, amountCents?: number, connectedAccountId?: string): Promise<CaptureResult>; cancelPaymentIntent(paymentIntentId: string, connectedAccountId?: string): Promise<void>; refund(paymentIntentId: string, amountCents?: number, connectedAccountId?: string): Promise<RefundResult>; createConnectOnboardingUrl(options: { returnUrl: string; refreshUrl: string; connectedAccountId?: string }): Promise<string>; disconnectAccount(connectedAccountId: string): Promise<void>;}@thebookingkit/server ships StripePaymentAdapter as a concrete implementation. stripe is an optional peer dependency — install it only if you use this adapter:
npm install stripeimport Stripe from "stripe";import { StripePaymentAdapter } from "@thebookingkit/server";
export const paymentAdapter = new StripePaymentAdapter({ stripe: new Stripe(process.env.STRIPE_SECRET_KEY!), webhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,});Deposits
Section titled “Deposits”Configure a partial upfront charge per event type via deposit_cents (fixed) or deposit_percentage (variable). When both are set, percentage wins.
import { computeDepositAmount, requiresDeposit } from "@thebookingkit/core";
requiresDeposit({}, 10000); // falserequiresDeposit({ depositPercentage: 25 }, 10000); // truecomputeDepositAmount({ depositPercentage: 25 }, 10000); // 2500computeDepositAmount({ depositCents: 5000 }, 10000); // 5000computeDepositAmount({ depositCents: 99999 }, 10000); // 10000 (capped at price)The booking flow uses initiateDeposit to create the PaymentIntent and refundDeposit to release it on cancellation per the policy below:
import { initiateDeposit, refundDeposit } from "@thebookingkit/server";
const result = await initiateDeposit(paymentAdapter, { bookingId: booking.id, deposit: { depositPercentage: eventType.depositPercentage }, priceCents: eventType.priceCents, currency: eventType.currency, connectedAccountId: provider.stripeAccountId,});// result.intent.clientSecret is what PaymentGate confirms in the browser.Cancellation policies
Section titled “Cancellation policies”Define tiered fee schedules based on how far in advance a booking is cancelled:
import { evaluateCancellationFee, validateCancellationPolicy } from "@thebookingkit/core";import type { CancellationPolicy, CancellationFeeResult } from "@thebookingkit/core";
// Free cancellation if 48+ hours notice, escalating fees for closer cancellationsconst policy: CancellationPolicy = [ { hoursBefore: 48, feePercentage: 0 }, // Free cancellation 48h+ { hoursBefore: 24, feePercentage: 25 }, // 25% fee 24-48h before { hoursBefore: 0, feePercentage: 50 }, // 50% fee same day];
// Validate the policy structuretry { validateCancellationPolicy(policy); console.log("Policy is valid");} catch (error) { console.error("Invalid policy:", error.message);}
// Compute the fee for a specific cancellationconst fee: CancellationFeeResult = evaluateCancellationFee( policy, 5000, // booking price in cents ($50.00) new Date("2026-03-10T14:00:00.000Z"), // appointment start time (UTC) new Date("2026-03-09T10:00:00.000Z") // cancellation time (28 hours before));
console.log(fee);// Output: {// feeCents: 1250, // 25% of $50 = $12.50// feePercentage: 25,// refundCents: 3750, // Customer gets $37.50 back// matchedTier: { hoursBefore: 24, feePercentage: 25 }// }Tiered cancellation scenarios:
import { evaluateCancellationFee } from "@thebookingkit/core";import type { CancellationPolicy } from "@thebookingkit/core";
const policy: CancellationPolicy = [ { hoursBefore: 48, feePercentage: 0 }, { hoursBefore: 24, feePercentage: 25 }, { hoursBefore: 0, feePercentage: 50 },];
const appointmentStart = new Date("2026-03-10T14:00:00.000Z");const priceCents = 10000; // $100
// Scenario 1: Cancel 72 hours before (free)const fee1 = evaluateCancellationFee( policy, priceCents, appointmentStart, new Date("2026-03-07T14:00:00.000Z") // 72 hours before);console.log(`Fee: $${fee1.feeCents / 100} (${fee1.feePercentage}%)`); // Fee: $0 (0%)
// Scenario 2: Cancel 36 hours before (25% fee)const fee2 = evaluateCancellationFee( policy, priceCents, appointmentStart, new Date("2026-03-09T02:00:00.000Z") // 36 hours before);console.log(`Fee: $${fee2.feeCents / 100} (${fee2.feePercentage}%)`); // Fee: $25 (25%)
// Scenario 3: Cancel 6 hours before (50% fee)const fee3 = evaluateCancellationFee( policy, priceCents, appointmentStart, new Date("2026-03-10T08:00:00.000Z") // 6 hours before);console.log(`Fee: $${fee3.feeCents / 100} (${fee3.feePercentage}%)`); // Fee: $50 (50%)Payment utilities
Section titled “Payment utilities”import { requiresPayment, hasNoShowFee, validatePaymentAmount, validateCurrency, formatPaymentAmount, computePaymentSummary,} from "@thebookingkit/core";import type { PaymentRecord, CancellationPolicy } from "@thebookingkit/core";
// Check if an event type requires paymentrequiresPayment({ priceCents: 5000 }); // truerequiresPayment({ priceCents: 0 }); // falserequiresPayment({ priceCents: null }); // false
// Check if policy has no-show feesconst policy: CancellationPolicy = [ { hoursBefore: 24, feePercentage: 25 }, { hoursBefore: 0, feePercentage: 100 }, // Full fee for no-shows];hasNoShowFee(policy); // true
// Validate payment amountsvalidatePaymentAmount(5000); // truevalidatePaymentAmount(0); // truevalidatePaymentAmount(-100); // throws PaymentValidationError
// Validate currency codesvalidateCurrency("usd"); // truevalidateCurrency("gbp"); // truevalidateCurrency("xyz"); // throws PaymentValidationError
// Format amounts for displayformatPaymentAmount(5000, "usd"); // "$50.00"formatPaymentAmount(1250, "gbp"); // "£12.50"formatPaymentAmount(100, "jpy"); // "¥100" (no decimal places)
// Compute payment summary for a provider's dashboardconst paymentRecords: PaymentRecord[] = [ { id: "pay-1", bookingId: "booking-1", stripePaymentIntentId: "pi_123abc", amountCents: 5000, currency: "usd", status: "succeeded", paymentType: "prepayment", refundAmountCents: 0, createdAt: new Date("2026-03-01"), }, { id: "pay-2", bookingId: "booking-2", stripePaymentIntentId: "pi_456def", amountCents: 8000, currency: "usd", status: "refunded", paymentType: "prepayment", refundAmountCents: 8000, createdAt: new Date("2026-03-05"), },];
const summary = computePaymentSummary(paymentRecords);console.log(summary);// {// totalRevenueCents: 5000,// totalRefundedCents: 8000,// netRevenueCents: -3000,// depositRevenueCents: 0,// countByStatus: { succeeded: 1, refunded: 1 },// countByType: { prepayment: 2, deposit: 0, no_show_hold: 0, cancellation_fee: 0 },// totalPayments: 2// }Webhooks
Section titled “Webhooks”handleStripeWebhook is a framework-agnostic handler — verify the signature, dispatch payment_intent.* events, and persist event.id for idempotency. Mount it from a Next.js route handler:
import { handleStripeWebhook } from "@thebookingkit/server";
export async function POST(req: Request) { const result = await handleStripeWebhook( { rawBody: await req.text(), signature: req.headers.get("stripe-signature") ?? "", }, { stripe, webhookSecret, store }, ); return new Response(null, { status: result.status });}UI components
Section titled “UI components”import { PaymentGate } from "./components/payment-gate";import { PaymentHistory } from "./components/payment-history";import { EventTypeDepositFields } from "./components/event-type-deposit-fields";
// Collect a deposit during checkout — shows "Pay deposit of $X" copy and// "Balance of $Y due at the appointment".<PaymentGate mode="deposit" amountCents={2500} totalPriceCents={10000} currency="usd" clientSecret={clientSecret} onPaymentSuccess={handlePaymentComplete} renderPaymentElement={() => <PaymentElement />}/>
// Configure the deposit on an event type<EventTypeDepositFields depositCents={form.depositCents} depositPercentage={form.depositPercentage} priceCents={form.priceCents} currency={form.currency} onDepositCentsChange={form.setDepositCents} onDepositPercentageChange={form.setDepositPercentage}/>
// Show payment history with type filtering<PaymentHistory payments={bookingPayments} />