Stripe authorize-only flow
We use Stripe Elements + the v2 REST API to handle add-on payments. Pattern: authorize on form submit, capture when availability is confirmed.
Why authorize-only
For early check-in / late check-out add-ons, we can’t confirm availability at form-submit time. The guest might book early check-in but back-to-back bookings could make it impossible. To avoid charging guests for things we can’t deliver:
- Form submit: authorize the maximum possible amount via Stripe (capture_method=manual). Card is held but no charge yet.
- Availability check (M4c at A-2 days, M4d at D-3 days): Bill reviews + decides. If approved → capture. If not → cancel auth (no charge).
Authorizations expire after ~7 days if not captured, so the workflow must decide before then.
Implementation
src/stripe/client.ts — minimal fetch-based client (no Stripe SDK).
Functions:
createPaymentIntent({amountCents, paymentMethodId, captureMethod: "manual", confirm: true, metadata, ...})— authorize on submitcapturePaymentIntent(intentId, amountToCaptureCents?)— capture (full or partial up to authorized amount)cancelPaymentIntent(intentId)— release authorization
amount_to_capture < authorized is allowed; > is not. For late
checkout we authorize at the MAX tier price ($200 for 2pm); whichever
tier the guest picks is captured at that tier’s price.
Form-side integration
src/routes/forms-pre-checkin.ts:
const intent = await createPaymentIntent(env, { amountCents: addonTotal * 100, paymentMethodId, description: `Booking ${bookingId} add-ons: ${addonLabel}`, receiptEmail: guestEmail, metadata: { booking_id, property_id, addon_early_checkin, addon_late_checkout }, captureMethod: "manual",});if (intent.status !== "requires_capture" && intent.status !== "succeeded") { return new Response("Card declined", { status: 402 });}// Save intent.id to pre_checkin_submissionsCapture paths
| Path | Trigger | Module |
|---|---|---|
/admin/booking/{id}/addon-capture | Bill clicks email link | src/routes/admin.ts |
/admin/booking/{id}/addon-decline | Bill clicks email link | src/routes/admin.ts |
/forms/late-checkout POST | Guest picks a time | src/routes/forms-late-checkout.ts |
| Auto-decline (back-to-back) | Workflow detects conflict | src/workflows/booking-flag.ts |
All paths update pre_checkin_submissions.payment_status and
payment_captured_at (or payment_status='canceled' on decline).
Test mode
Bill uses Stripe test keys (sk_test_..., pk_test_...) in .dev.vars.
Live keys would be set via wrangler secret put. Test card
pm_card_visa always works in test mode.
Source
src/stripe/client.ts— REST helperssrc/routes/forms-pre-checkin.ts— authorize-on-submitsrc/routes/admin.ts— capture/decline endpointssrc/routes/forms-late-checkout.ts— partial-capture at tier