WorkestraDocs
PlatformCalendar & Scheduling

Paid Bookings

Take payment via Stripe before a booking confirms — pricing, refund policy text, and a clean pending-payment state machine.

Some meetings should only happen if the attendee actually pays first — paid coaching, paid consultations, paid masterclasses. Workestra's paid bookings flow runs Stripe Checkout between the slot pick and the confirmation, so the slot is reserved for the attendee while they pay and released if they don't.

This is opt-in per booking link — most links never need it. When it's off, the booking flow is unchanged.

Paid booking flow

Screenshot needed — public booking page handing off to Stripe Checkout, then returning to confirmation

Setup checklist

  1. Connect Stripe to the workspace (under Stripe Integration). The workspace's standard STRIPE_SECRET_KEY is reused.
  2. On the booking link's Advanced tab, toggle Require payment to book.
  3. Set the price (in minor units, e.g. 5000 for €50.00) and the currency (ISO 4217 — EUR, USD, GBP, etc.).
  4. Optionally fill in the description and refund-policy text shown on the public page.
  5. Save. Workestra creates a Stripe Product + Price for the link and stores the IDs.

The syncBookingLinkStripeArtifactsAction server action runs on save. It's idempotent — re-saving with the same price doesn't create duplicates.

On a brand-new link, the Stripe sync is currently triggered on the first edit, not the first save. If you create a link with Require payment already on, save it once, then save again to create the Stripe artifacts. We're tightening this in a follow-up release; in the meantime the form hides checkout flows until artifacts exist.

What the attendee sees

  1. Booking link page (/book/<slug>) — the price is shown above the slot picker ("€50.00 · 30 min · 1:1") and the refund policy text appears under the form.
  2. Slot picked + form filled — they hit Confirm.
  3. Stripe Checkout opens in the same tab (window.location.href = checkout_url). The booking row exists in the database with status = pending_payment.
  4. Stripe handles payment — credit card, Apple Pay, Google Pay, SEPA, iDEAL, and any other method you've enabled in your Stripe dashboard.
  5. On success, Stripe redirects back to /book/<slug>/confirmed?session_id=.... The Stripe webhook (/api/webhooks/stripe-bookings) flips the booking to confirmed, fires the post-booking email, and enqueues reminders. The confirmation page loads from the local row.
  6. On cancel / abandon, Stripe redirects back to /book/<slug>/cancelled-payment. The booking row stays at pending_payment until the Stripe checkout.session.expired event arrives (default 24 hours), then transitions to cancelled.

The slot is held during this window. If a different attendee tries to book the same slot while it's pending payment, the slot still appears available — Workestra does not lock the slot for unpaid bookings, by design. Locking unpaid bookings would let bad actors block your calendar by initiating checkouts they never finish. Instead, the second attendee can complete payment and gets the slot; the first is told their session expired and offered a re-pick.

State machine

Paid bookings add pending_payment to the booking status enum:

pending_payment → confirmed     (Stripe webhook: checkout.session.completed)
pending_payment → cancelled     (Stripe webhook: checkout.session.expired)
pending_payment → cancelled     (manual cancel from /calendar)

Because the workflow engine listens for the booking_created lifecycle trigger, it explicitly waits for the post-payment confirmation before firing — so your "Booked" workflow doesn't send a prep doc to someone who never actually paid.

Refunds

Refunds are not automated yet. If a paid booking is cancelled (by the attendee, by you, or for a no-show), the booking moves to cancelled and the Stripe charge stays. Issue the refund manually from your Stripe dashboard.

The roadmap is to wire automatic refunds based on a per-link policy ("refund full if cancelled ≥24h ahead, no refund inside 24h") — not shipped yet. The refund-policy text on the public page is documentation for the attendee; it's not enforced programmatically.

Currencies

Whatever currency Stripe accepts in your country, Workestra accepts. The currency is stored as a 3-letter ISO 4217 code on the link. Mixing currencies across links in the same workspace is fine — each link's price is independent.

For multi-currency rendering on a global site, add the locale to the embed URL or the public page URL — the price formats according to Intl.NumberFormat with the link's currency.

What's NOT supported

  • Subscriptions / recurring billing for bookings. Paid bookings are one-shot. For recurring billing on a meeting series, use Finance Subscriptions and link it from the booking workflow.
  • Coupons / promo codes at booking time. Apply them in Stripe directly if needed.
  • Tax calculation per attendee. Stripe Tax can be enabled on the workspace; tax appears on the Stripe Checkout page.
  • Refunds from the booking row. Manual via Stripe dashboard, as noted.
  • Split payments. One charge, one customer, one host.

Why this isn't a marketplace cut

Workestra takes no platform fee on paid bookings. You keep 100% of the payment minus Stripe's processing fee (typically 1.4% + €0.25 in the EU; 2.9% + $0.30 in the US). The booking goes through your own Stripe account — Workestra never touches the money.

That's the structural difference vs. Calendly's paid bookings (also free of platform fee) or Stripe-Atlas-style "scheduling-as-a-service" platforms. The booking is in our database; the money flows directly between your customer and you.