Skip to content

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:

  1. Form submit: authorize the maximum possible amount via Stripe (capture_method=manual). Card is held but no charge yet.
  2. 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 submit
  • capturePaymentIntent(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_submissions

Capture paths

PathTriggerModule
/admin/booking/{id}/addon-captureBill clicks email linksrc/routes/admin.ts
/admin/booking/{id}/addon-declineBill clicks email linksrc/routes/admin.ts
/forms/late-checkout POSTGuest picks a timesrc/routes/forms-late-checkout.ts
Auto-decline (back-to-back)Workflow detects conflictsrc/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 helpers
  • src/routes/forms-pre-checkin.ts — authorize-on-submit
  • src/routes/admin.ts — capture/decline endpoints
  • src/routes/forms-late-checkout.ts — partial-capture at tier