Skip to content

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.

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:

Terminal window
npm install stripe
import 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!,
});

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); // false
requiresDeposit({ depositPercentage: 25 }, 10000); // true
computeDepositAmount({ depositPercentage: 25 }, 10000); // 2500
computeDepositAmount({ depositCents: 5000 }, 10000); // 5000
computeDepositAmount({ 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.

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 cancellations
const 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 structure
try {
validateCancellationPolicy(policy);
console.log("Policy is valid");
} catch (error) {
console.error("Invalid policy:", error.message);
}
// Compute the fee for a specific cancellation
const 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%)
import {
requiresPayment,
hasNoShowFee,
validatePaymentAmount,
validateCurrency,
formatPaymentAmount,
computePaymentSummary,
} from "@thebookingkit/core";
import type { PaymentRecord, CancellationPolicy } from "@thebookingkit/core";
// Check if an event type requires payment
requiresPayment({ priceCents: 5000 }); // true
requiresPayment({ priceCents: 0 }); // false
requiresPayment({ priceCents: null }); // false
// Check if policy has no-show fees
const policy: CancellationPolicy = [
{ hoursBefore: 24, feePercentage: 25 },
{ hoursBefore: 0, feePercentage: 100 }, // Full fee for no-shows
];
hasNoShowFee(policy); // true
// Validate payment amounts
validatePaymentAmount(5000); // true
validatePaymentAmount(0); // true
validatePaymentAmount(-100); // throws PaymentValidationError
// Validate currency codes
validateCurrency("usd"); // true
validateCurrency("gbp"); // true
validateCurrency("xyz"); // throws PaymentValidationError
// Format amounts for display
formatPaymentAmount(5000, "usd"); // "$50.00"
formatPaymentAmount(1250, "gbp"); // "£12.50"
formatPaymentAmount(100, "jpy"); // "¥100" (no decimal places)
// Compute payment summary for a provider's dashboard
const 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
// }

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:

app/api/webhooks/stripe/route.ts
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 });
}
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} />