Booking platform payments: practical tips and trade-offs
Payments for reservations and services operate under fundamentally different rules than one-off purchases. You're holding money before the service happens, managing cancellations that can happen minutes before delivery, and coordinating payouts to providers who may operate across different jurisdictions. We've learned through production experience that a single "standard" payment flow doesn't work—you need to design around your cancellation policy, regulatory environment, and provider network.
Deposit or full prepay — Decide by cancellation policy
The first decision shapes your entire payment architecture: do you charge a deposit upfront or collect the full amount before the service? This isn't a payment question alone—it's a business question that determines your payment state machine, your reserve calculations, and your customer communication strategy.
Most platforms we've built fall into two camps. Home services and accommodation platforms often collect a deposit (25–50% of total value) to confirm intent while reducing friction and chargeback risk. High-value services like executive coaching or consulting often go full prepay because the provider's calendar is their inventory—a cancelled slot generates zero revenue either way. Ride-hailing sits in a different category: they collect the full amount at the end of the trip, after service delivery.
Here's a simplified decision tree:
- Use deposit if: cancellations are frequent, service is flexible (can be rescheduled), customers book far in advance, or you're managing marketplace risk.
- Use full prepay if: service is scarce, cancellations are rare, provider availability is committed immediately, or your margin is thin.
From a payment processing perspective, the difference is significant. A deposit creates a partial hold; you'll need to capture or void the auth separately. Full prepay simplifies your Stripe webhook logic—one auth, one capture, one source of truth.
// Simplified deposit vs. full prepay logic
async function chargeForBooking(
booking: Booking,
paymentMethod: Stripe.PaymentMethod
): Promise {
const amount = booking.totalPrice;
const chargeAmount = booking.paymentPolicy === 'deposit'
? Math.round(amount * 0.25 * 100) // 25% deposit in cents
: Math.round(amount * 100); // Full prepay
const paymentIntent = await stripe.paymentIntents.create({
amount: chargeAmount,
currency: 'usd',
payment_method: paymentMethod.id,
confirm: true,
metadata: {
bookingId: booking.id,
paymentPolicy: booking.paymentPolicy,
totalPrice: Math.round(amount * 100),
},
});
return paymentIntent;
}
Store your payment policy choice in the booking record. You'll reference it during cancellations, refunds, and provider payouts. Don't hardcode it or derive it per-request—it changes too infrequently to justify the lookup, but changes to policy mid-booking season will break calculations if you're not careful.
Cancellations — Automated refunds based on rules
Cancellations are the most complex part of booking platform payments. Unlike e-commerce (where returns are exceptions), booking platforms treat cancellations as normal business. Your refund policy typically has time-based tiers: full refund if cancelled 7+ days before service, 50% if cancelled within 48 hours, no refund if cancelled within 24 hours.
Build a cancellation rule engine. Store rules as data, not code. A real example: a property rental platform might have one set of rules for summer weekends (non-refundable after 30 days) and another for weekdays (50% refund up to 14 days before). Rules change often; your code shouldn't need deploys.
interface CancellationRule {
id: string;
bookingType: string; // 'accommodation', 'service', 'ride'
daysBeforeService: number;
refundPercentage: number;
priority: number;
}
async function calculateRefundAmount(
booking: Booking,
rules: CancellationRule[]
): Promise {
const now = new Date();
const serviceDate = new Date(booking.serviceDate);
const hoursUntilService = (serviceDate.getTime() - now.getTime()) / (1000 * 60 * 60);
const daysUntilService = Math.ceil(hoursUntilService / 24);
// Match the most specific rule (highest priority that applies)
const applicableRules = rules
.filter((r) => r.bookingType === booking.type && r.daysBeforeService <= daysUntilService)
.sort((a, b) => b.priority - a.priority);
if (applicableRules.length === 0) {
return 0; // No refund
}
const rule = applicableRules[0];
return Math.round(booking.chargedAmount * (rule.refundPercentage / 100));
}
Once you've calculated the refund, you need to issue it. For Stripe, use the refund API tied to the original payment intent or charge. Always store the refund ID in your database—you'll need it for reconciliation and disputes.
async function refundBooking(
booking: Booking,
refundAmount: number
): Promise {
const refund = await stripe.refunds.create({
payment_intent: booking.stripePaymentIntentId,
amount: refundAmount, // in cents
metadata: {
bookingId: booking.id,
reason: 'customer_cancellation',
},
});
await db.bookings.update(booking.id, {
refundId: refund.id,
refundAmount: refundAmount,
refundedAt: new Date(),
status: 'cancelled',
});
// Emit event for downstream systems (email, analytics, provider notification)
await events.emit('booking.refunded', { bookingId: booking.id, refundAmount });
}
One production gotcha: Stripe allows refunds up to the full charged amount, but only within 180 days. If a customer disputes a charge after 6 months, you can't issue a refund through Stripe's API—you'll need to use a manual bank transfer or the customer's account credit. Document this in your cancellation policy and consider holding refund balance in a liability account for edge cases.
No-shows — Charge saved cards with consent
A booking is confirmed and paid. The service time arrives. The customer doesn't show up. You need to charge them to compensate the provider and maintain incentive alignment—but you can't just charge without consent, and different regions have different rules around stored payment methods.
Stored payment method charges (off-session payments) require proper consent and disclosure. In the US, Visa and Mastercard require explicit confirmation that you'll charge for no-shows. In Europe, GDPR and PSD2 add constraints around data retention and recurring transactions. The safest approach: get written consent at booking time, store it in a legally auditable format, and charge only if explicitly documented in your cancellation policy.
Stripe's setup intent flow handles this cleanly for one-time off-session charges:
async function handleNoShow(
booking: Booking,
noShowFeePercentage: number = 100
): Promise {
// Verify booking has explicit no-show charge consent
if (!booking.consentGivenForNoShowCharge) {
console.warn(`Booking ${booking.id} lacks no-show consent. Skipping charge.`);
return;
}
const chargeAmount = Math.round(booking.totalPrice * (noShowFeePercentage / 100) * 100);
const paymentIntent = await stripe.paymentIntents.create({
amount: chargeAmount,
currency: booking.currency,
customer: booking.stripeCustomerId,
payment_method: booking.savedPaymentMethodId,
off_session: true, // Critical: tells Stripe this is an off-session charge
confirm: true,
metadata: {
bookingId: booking.id,
reason: 'no_show',
},
});
await db.bookings.update(booking.id, {
noShowChargeId: paymentIntent.id,
noShowCharged: true,
noShowChargedAt: new Date(),
});
await events.emit('booking.no_show_charged', { bookingId: booking.id, chargeAmount });
}
No-show charges often fail—cards expire, customers dispute them, or the card issuer declines the transaction. Don't assume success. Stripe sends webhooks for payment intent state changes; listen for payment_intent.payment_failed and implement a retry strategy with exponential backoff or escalate to your provider support team.
Provider payouts — Connect transfers after the service
The service is complete. Now you need to pay the provider. If you're a marketplace or two-sided platform, you likely use Stripe Connect to manage provider accounts and transfer funds from your platform account to theirs. The payout timing and fee structure are critical design decisions.
Most platforms settle within 24–72 hours after service completion or customer confirmation. Immediate payouts are rare because they increase operational complexity (disputes, refunds, and chargebacks make sense only after the service is confirmed as satisfactory). Delaying settlement gives you a window to handle cancellations, adjustments, and partial refunds.
Here's a practical payout flow:
- Service is marked complete by provider or auto-completed after scheduled time.
- Start a settlement timer (48 hours, or customer confirmation timeout).
- If a cancellation or refund is issued within the timer, adjust the payout amount.
- After the timer expires, create a transfer to the connected account.
- Deduct platform fees and any chargebacks before the transfer.
async function settleProviderPayout(
booking: Booking,
provider: Provider
): Promise {
// Verify service is complete and settlement window has passed
const now = new Date();
const settlementDeadline = new Date(booking.serviceCompleteDate);
settlementDeadline.setHours(settlementDeadline.getHours() + 48);
if (now < settlementDeadline) {
throw new Error('Settlement window not yet elapsed');
}
// Calculate payout: gross amount minus refunds, chargebacks, platform fees
const grossAmount = booking.chargedAmount;
const refundedAmount = booking.refundAmount || 0;
const chargebackAmount = booking.chargebackAmount || 0;
const platformFeePercentage = provider.platformFee || 0.15; // 15% platform fee example
const netAmount = grossAmount - refundedAmount - chargebackAmount;
const providerPayout = Math.round(netAmount * (1 - platformFeePercentage) * 100);
if (providerPayout <= 0) {
await db.bookings.update(booking.id, {
providerPayoutId: null,
providerPayoutAmount: 0,
providerPayoutStatus: 'no_payout_negative',
});
return;
}
const transfer = await stripe.transfers.create({
amount: providerPayout,
currency: 'usd',
destination: provider.stripeConnectAccountId,
metadata: {
bookingId: booking.id,
providerId: provider.id,
},
});
await db.bookings.update(booking.id, {
providerPayoutId: transfer.id,
providerPayoutAmount: providerPayout,
providerPayoutStatus: 'transferred',
providerPayoutDate: new Date(),
});
await events.emit('booking.provider_paid', {
bookingId: booking.id,
providerId: provider.id,
amount: providerPayout,
});
}
A critical detail: if you're using Stripe Connect, your platform account holds the customer payment, and you transfer to the provider's connected account. This means you're liable for chargebacks and disputes on the full amount, not just your fee. Ensure your reserve policy (if applicable) covers your potential downside.
Monitor payout failures. Providers' connected accounts can be suspended, restricted, or closed. Stripe will return a transfer error; log it and alert your operations team. You may need to issue a manual payout via bank transfer or hold the funds in an escrow account pending account reinstatement.
Common mistakes
- Charging without explicit consent for no-shows: No-show policies must be disclosed upfront and agreed to in writing. Charging blindly will trigger disputes and chargebacks.
- Refunding the wrong amount: Calculate refunds based on your stored cancellation rules, not the customer's request. Rules change over time; hard-coding logic makes it harder to track what was promised.
- Settling provider payouts before service confirmation: Immediate payouts increase refund complexity and reduce your ability to handle chargebacks. Settle only after a safety window has passed.
- Ignoring Connect account restrictions: Stripe Connect accounts can be suspended for compliance reasons. Check account status before initiating transfers; don't assume success.
- Not tracking payment state in your database: Stripe webhooks can be delayed or retried. Store payment IDs, refund IDs, and transfer IDs in your bookings table to ensure idempotency and accurate reconciliation.
Key takeaways
- Choose deposit or full prepay based on your cancellation policy and service model, then design your payment capture and release logic around that choice.
- Build a rules engine for cancellations; store refund tiers as data, not code, so policy changes don't require deploys.
- Off-session charges for no-shows require explicit consent and proper disclosure. Always verify consent before charging and handle failures gracefully.
- Delay provider payouts by 24–72 hours after service completion to create a window for refunds, chargebacks, and adjustments. Use that window to recalculate payout amounts before transfer.
- Store payment IDs, refund IDs, and transfer IDs in your database and reconcile against Stripe's API regularly. Webhooks are reliable but not instantaneous.






Comments
Loading comments…