Skip to content

Payments & SaaS

Booking Deposits via Stripe — Hold-Then-Charge for Service Bookings

Deposits stop no-shows cold. A card hold at booking, a charge on completion — no coding mess, pure Stripe.

📅 💳

Clinics, photographers, tradies, and anyone booking appointments live with a nightmare: no-shows. A 20% no-show rate isn't uncommon. That's 20% of your capacity gone, 20% of revenue vanished, and 20% of your day's schedule toast. Deposits kill no-shows. Stripe's Payment Intents with manual capture lets you hold a deposit on a card at booking time, then capture (charge) it when the service completes. Cancel the booking? Void the hold. No-show after the appointment window closes? Charge it. It's the nuclear option for no-show reduction, and Velocity X wires it into every service-based booking system.

Why Deposits Slash No-Show Rates

Psychology wins. A customer who's already given you their card number — and seen a $100 hold appear on their statement — doesn't ghost at 9am. The friction is real. They've made a financial commitment. When they book, they know there's a consequence to cancellation. Studies across clinic booking platforms show hold-based deposits drop no-shows from 15-20% down to 3-5%. That's not incremental — that's transformational for cash flow and schedule optimization.

Compare this to a traditional deposit model: customer sends you money upfront, you hold it in your account, and if they cancel, you have to process a refund. Friction goes the wrong direction. Stripe's hold model flips it: you capture the payment only if the service happens or they miss it. Fewer refunds to process. Fewer customer service conversations. Fewer chargebacks (because customers understand the hold happened at checkout, not weeks later).

How Stripe Payment Intents with Manual Capture Work

A Payment Intent is Stripe's way of saying "I'm going to charge this card." You create the intent with capture_method: "manual", which means the payment is authorized (card is checked, fraud filters run, amount is reserved) but not captured (charged) yet. The customer's bank puts a hold on the amount — they see it in their account as "pending" — but the money doesn't move.

Later, when the appointment time has passed, you capture the intent. The hold becomes a real charge. The money moves into your account. Or: customer cancels before the appointment? You cancel the intent. The hold disappears from their statement within 24–48 hours. Clean, reversible, no refund friction.

// 1. Create a Payment Intent with manual capture (at booking) {"\n"}const intent = await stripe.paymentIntents.create({"{"} {"\n"} amount: 10000, // $100 deposit in cents {"\n"} currency: "aud", {"\n"} customer: customerId, // link to their Stripe customer record {"\n"} capture_method: "manual", // don't charge yet {"\n"} metadata: {"{"} {"\n"} booking_id: "appt_12345", {"\n"} service: "haircut", {"\n"} appointment_time: "2026-06-15T14:00:00Z" {"\n"} {"}"} {"\n"}{"}"}); {"\n"} {"\n"}// 2. Confirm the intent on the client side {"\n"}const confirmed = await stripe.confirmCardPayment( {"\n"} intent.client_secret, {"\n"} {{ payment_method: paymentMethodId }} {"\n"}); // Card is now authorized, hold appears on statement {"\n"} {"\n"}// 3. On appointment completion or expiry, capture the hold {"\n"}const captured = await stripe.paymentIntents.capture( {"\n"} intent.id {"\n"}); // Hold becomes a charge, money moves to account

The three-step flow is dead simple: authorize at booking, capture at completion (or void if cancelled). No webhook gymnastics. No separate charge API calls. No refund logic. Payment Intents with manual capture are the senior-engineer shortcut to deposit management.

Deposit vs. Pre-Pay: Why Deposits Win for Service Businesses

Pre-pay (charging the full service fee upfront) looks like it solves the same problem. Customer books, you charge them immediately, they can't no-show without losing money. But it has two brutal failure modes: chargebacks and bad reviews.

Chargeback happens when a customer claims you charged without authorization. They say "I never agreed to this" or "I cancelled two weeks ago, you shouldn't have charged." Stripe refunds them while you argue. A 2–3% chargeback rate on pre-pay bookings is common. A deposit model (small hold, captured after proof of service) has near-zero chargebacks because the hold happened with explicit consent at booking, and capture happened only after the service. Clear accountability.

Bad reviews happen when a customer books, changes their mind, and now you've got their money. Refund dispute. Three-star Trustpilot review: "They wouldn't refund my deposit." Deposits flip the script: customer books, money is held (not charged), they cancel, hold goes away, no drama. You still get some deposit friction, but the refund process is instant (just void the intent) rather than adversarial.

Webhook Capture: When to Actually Charge

Here's where Velocity X's backend logic lives. You set up a webhook listening for your own internal events — something like appointment.completed or appointment.no_show. When a staff member checks in a customer or closes an appointment as no-show, you fire that event, and a webhook handler captures the intent.

// Listen for appointment completion (your own event) {"\n"}app.post("/webhooks/appointment-completed", async (req, res) => {"{"} {"\n"} const {{ booking_id, payment_intent_id }} = req.body; {"\n"} {"\n"} // Capture the hold (turns it into a real charge) {"\n"} try {"{"} {"\n"} const captured = await stripe.paymentIntents.capture(payment_intent_id); {"\n"} console.log(Captured deposit for booking {{ booking_id }}); {"\n"} {"\n"} // Send receipt email with charge details {"\n"} await sendEmail(booking_id, "appointment-complete", captured); {"\n"} {"\n"} res.json({{ success: true }}); {"\n"} {"}"} catch (err) {"{"} {"\n"} console.error(Failed to capture intent {{ payment_intent_id }}:, err); {"\n"} res.status(500).json({{ error: err.message }}); {"\n"} {"}"} {"\n"}{"}");

For no-shows, you have options: capture the full deposit (punitive), capture a partial fee (e.g., 50%), or void it and send a warning (first-time grace). Velocity X recommends: one free pass (void), then capture on the second no-show. Customers understand the policy, it's fair, and it kills repeat no-shows without crushing goodwill.

Cancellation Grace Periods and Refund Windows

Policy matters more than technology here. Define a cancellation window: free cancellation if they notify you 24 hours before the appointment, 50% of the deposit forfeited if 12–24 hours, full deposit forfeited if less than 12 hours. Then enforce it in code: when a customer cancels, check elapsed time, and either void the intent (free cancellation) or capture it (forfeit).

// Cancellation handler {"\n"}app.post("/bookings/:booking_id/cancel", async (req, res) => {"{"} {"\n"} const booking = await db.bookings.findOne(booking_id); {"\n"} const now = new Date(); {"\n"} const hoursUntilAppointment = (booking.appointment_time - now) / (1000 * 60 * 60); {"\n"} {"\n"} if (hoursUntilAppointment >= 24) {"{"} {"\n"} // Free cancellation, void the hold {"\n"} await stripe.paymentIntents.cancel(booking.payment_intent_id); {"\n"} status = "refunded"; {"\n"} {"}"} else if (hoursUntilAppointment >= 12) {"{"} {"\n"} // Late cancellation, capture 50% forfeit {"\n"} await stripe.paymentIntents.update(booking.payment_intent_id, {"{"} {"\n"} metadata: {{ forfeited: true, refund_reason: "late_cancel" }} {"\n"} {"}"}); {"\n"} await stripe.paymentIntents.capture(booking.payment_intent_id); {"\n"} status = "partial_forfeit"; {"\n"} {"}"} else {"{"} {"\n"} // No-show window, full forfeit {"\n"} await stripe.paymentIntents.capture(booking.payment_intent_id); {"\n"} status = "full_forfeit"; {"\n"} {"}"} {"\n"} {"\n"} await db.bookings.update(booking_id, {{ status, cancelled_at: now }}); {"\n"} res.json({{ success: true, status }}); {"\n"}{"}"});

Transparency wins. Show the cancellation policy on your booking page. Send a confirmation email with the deposit amount, hold date, and when it will be charged. Customers hate surprises; clarity builds trust and reduces dispute chargebacks.

Frequently Asked Questions

What if the customer's card declines when we try to capture?

Captures can fail if the card is closed, funds are insufficient, or fraud filters block it. Your webhook handler should catch the exception, log it, and send an alert to your team. Email the customer: "Your card on file failed. Please update your payment method." If they don't update within 24–48 hours, mark them as a no-show and consider blocking future bookings. Most capture failures happen at the point of service (customer's there, card fails), so you can ask for a different payment method on the spot.

How long does a hold stay pending on a customer's account?

Bank to bank, typically 5–7 business days. But Stripe lets you capture or void at any time — there's no expiry limit on the intent itself. Some banks will drop a pending hold after 30 days if not captured, so if a customer books a service 6 weeks out, the hold might fall off before the appointment. For bookings far in advance, consider holding the deposit only a few days before the appointment (ask them to re-confirm, re-authorize the hold, then capture after service).

Can I capture more than the original hold amount?

No. A Payment Intent for $100 can only be captured for $100 or less. If you want to charge an overage (e.g., add-on service), create a separate Payment Intent or a new Charge. Don't try to upcharge on an existing intent — it'll fail and frustrate everyone.

What about customers who use payment plans or financing (Klarna, Afterpay)?

Manual capture works with card payments and ACH, but some Buy-Now-Pay-Later providers (Klarna, Afterpay) don't support holds. They require immediate full capture. For those customers, you have two options: (1) ask them to pay with a card instead, or (2) charge them upfront (pre-pay model) and process a refund if they cancel. Not ideal, but BNPL's architecture doesn't play with deposits.

Should I send a receipt email when the hold is placed, or when it's captured?

Both. Send a booking confirmation email with the deposit hold details immediately ("$100 hold placed on your card, will be charged on [date] after your appointment"). Then send a second email after capture with the actual charge details and receipt. Transparency at every step kills chargebacks and dispute support tickets.

What if a customer disputes the charge after the appointment?

Provide proof: staff notes showing they attended, or a system timestamp proving they showed up. Stripe requires documentation for chargeback disputes. If a customer no-showed (you captured the forfeit), their claim that "I never booked" fails because the deposit intent has their explicit consent metadata attached. If they claim "I cancelled and you charged me anyway," your logs show when they cancelled vs. the service time. Deposits with clear policies and audit trails win disputes.

The Bottom Line

Deposits via Stripe Payment Intents with manual capture are the highest-ROI anti-no-show mechanism in service booking. A hold on the card at booking, capture after proof of service, void on cancellation. Zero ambiguity. Zero refund drama. The 70% drop in no-shows translates directly to better schedule utilization, higher revenue, and happier staff (no more wasted slots). The backend is a few dozen lines of code. The policy is transparent and fair. And Stripe handles all the payment machinery. Velocity X bundles deposit management into every tier; see how Stripe Payment Links handle one-off sales for the other half of your revenue.

Let us make some quick suggestions?

Please provide your full name.
Please provide your phone number.
Please provide a valid phone number.
Please provide your email address.
Please provide a valid email address.
Please provide your brand name or website.
Please provide your brand name or website.