← All articles

Authorize at Booking, Capture at Weigh-In: Fair Marketplace Payments with Stripe

By Shuvo Haldar3 min read
StripePaymentsNode.jsMarketplaceArchitecture
Authorize at Booking, Capture at Weigh-In: Fair Marketplace Payments with Stripe

On a laundry marketplace I built, the price of an order isn't known when the customer checks out. You book a pickup, a driver collects the bag, the shop weighs it, and only then is the real cost known. So how do you take payment fairly — charging the exact final amount, never a cent more — without asking the customer to pay twice?

The answer is a two-step payment: authorize at booking, capture at weigh-in, using Stripe's manual capture. This post walks through the pattern and the one invariant that keeps it honest.

Animated live order-tracking bar moving through Booked, Picked up, Cleaning, Out for delivery, and Delivered
The same app streams live order status to every party in real time — the kind of "it's happening now" UI that makes a product feel alive.

Why the obvious approaches fail

  • Charge an estimate up front, refund the difference. You've now taken money you weren't owed, refunds cost fees and time, and the customer sees a scary full-price charge. Bad trust, bad economics.
  • Charge nothing until weigh-in. Now the card might fail after the work is done. You've cleaned the laundry with no guaranteed way to get paid.

Both break because they treat "reserve the money" and "take the money" as one step. Card networks let you separate them — and Stripe exposes exactly that.

The pattern: manual capture

A Stripe PaymentIntent with capture_method: "manual" places a hold (authorization) on the customer's card for an upper-bound amount at booking. The funds are reserved but not taken. Later, you capture the real weighed amount — which can be less than, but never more than, the authorized amount.

// 1) At booking — authorize an upper bound (e.g. a generous max for the order)
const intent = await stripe.paymentIntents.create({
  amount: maxAuthorizeAmount, // in the smallest currency unit
  currency: "usd",
  capture_method: "manual",   // <- reserve now, take later
  customer: customerId,
  payment_method: paymentMethodId,
  confirm: true,
});
// 2) After the shop weighs the load — capture the exact final amount
const finalAmount = pricingEngine.total(weighedItems); // server computes this

await stripe.paymentIntents.capture(intent.id, {
  amount_to_capture: Math.min(finalAmount, intent.amount), // never exceed the hold
});

That Math.min(...) is the whole game. It encodes the invariant:

You can only ever capture up to what you authorized.

If the final price somehow exceeds the hold (an unusually large order), you don't silently overcharge — you cap the capture and handle the remainder explicitly (a second small charge the customer can see and approve), rather than surprising them.

Keep the client out of the money

Notice that the client app never computes or sends an amount. The browser and the mobile apps just say "this order happened"; the backend is the single pricing authority. It owns the catalog, the per-kg rates, the fees, and the capture. A tampered client can't change what gets charged, because the client never had a say in the number.

This is the same principle behind every trustworthy payment flow: the amount is derived on the server from data the server controls.

What about the marketplace split?

Because the platform captures the payment, it can then split each order — a delivery fee, a platform commission, and the shop's share — and settle the supplier automatically (I used Stripe Connect with a scheduled, idempotent payout sweep 48 hours after delivery). But that's a post of its own.

Takeaways

  • When a price isn't final at checkout, separate the authorization from the capture — don't overcharge-then-refund.
  • Use capture_method: "manual" and capture with amount_to_capture = min(final, authorized).
  • Make the server the only source of the amount. Clients report events, not prices.

This pattern turned a genuinely hard product requirement ("charge the real weighed price, fairly") into a few lines of deliberate code — and a system customers can trust.


I build payment systems, marketplaces, and SaaS platforms end-to-end. If you're working on something similar, let's talk.

Building something similar?

I'm a senior full-stack & DevOps engineer available for full-time roles and projects worldwide.