Bookings and payments
How a booking is made, paid, confirmed and cancelled: card through Stripe, PayPal, pay on arrival, the shipped simulator, the webhooks that confirm a payment, refunds, and the steps the host takes in the dashboard.
For the Full Stack package
Three ways to pay
| Method | Offered when | Where the guest pays |
|---|---|---|
| Card | Through Stripe when STRIPE_SECRET_KEY is set; through the shipped simulator while no processor is configured at all | Stripe's hosted Checkout page, so no card number reaches this application; with the simulator, a card form on the checkout |
| PayPal | Through PayPal when PAYPAL_CLIENT_ID and PAYPAL_CLIENT_SECRET are set; through the simulator while no processor is configured at all | PayPal's approval page |
| Pay on arrival | While the allow_pay_on_arrival setting is on (Settings, Payments) | Nothing online: the host collects it at the door |
| Configured on the API | Offered online | Refused |
|---|---|---|
| Neither Stripe nor PayPal | Card and PayPal, both settled by the simulator | Nothing |
| Stripe only | Card, through Stripe | PayPal |
| PayPal only | PayPal, through PayPal | Card |
| Both | Card through Stripe, PayPal through PayPal | Nothing |
Pay on arrival is offered beside any of these while its setting is on, and refused while it is off. The checkout reads the list from GET /api/payments/methods, so it never offers a method the API cannot take, and a hosted method asks for no card details on the website. A booking sent with a refused method is answered "That payment method is not available. Choose another way to pay." before anything is written, so it holds no nights. The money goes to your own Stripe or PayPal account; the product never holds it.
The shipped simulator
A fresh install has no processor, so the simulator settles every card and PayPal booking at once without contacting anyone. The whole flow is clickable on a laptop with no keys, no merchant account and no network, and the booking, the confirmation, the calendar and the revenue figures are real rows written by the real flow. A card number ending in 0 is declined, so the failure path can be seen.
While it runs, the checkout's card form says "Test mode: no card is charged.", GET /api/payments/methods answers simulated: true, and the API writes one line to its log when it starts:
[Payments] Payment simulator active: card and PayPal bookings are confirmed without payment until a processor is configured (STRIPE_SECRET_KEY, or PAYPAL_CLIENT_ID and PAYPAL_CLIENT_SECRET).It takes no money
A booking the simulator confirms was never paid for, so it runs only while neither Stripe nor PayPal is configured. Configure the processor your guests will pay with before the website is open to them: from then on the simulator is off, and the checkout offers only the methods your processors take, plus pay on arrival while it is on. The Payments tab in the dashboard's settings says when the simulator is in use.
A booking's lifecycle
| Status | Meaning | Next |
|---|---|---|
| Pending | Made on the website, waiting for its payment or for the host | Confirmed or cancelled |
| Confirmed | Paid, or accepted by the host for pay on arrival | Checked in or cancelled |
| Checked in | The guest has arrived. A pay-on-arrival booking is marked paid here | Completed or cancelled (a stay cut short) |
| Completed | The stay is over, and the guest can review it | None |
| Cancelled | By the guest, by the host, or because an unpaid hold ran out | None |
- A card or PayPal booking is confirmed by its payment, never by hand: the host's Confirm is refused with "This booking cannot be confirmed until the payment arrives." until it is paid. A pay-on-arrival booking waits for the host to confirm it.
- The host moves a booking along on its page in the dashboard with Confirm, Check in, Complete and Cancel, and keeps an internal note the guest never sees. A step the booking cannot take now is shown disabled with its reason.
- A booking moves forward only, and completed and cancelled are final.
- Every booking has a reference given in order (
B0001,B0002and on), and keeps the nightly rate, the cleaning fee and the total it was made with. - The dashboard's bell tells every admin who can view bookings when a booking is paid (by the webhook, the PayPal capture or the simulator), when a pay-on-arrival booking is waiting for the host, and when a booking is cancelled, saying whether the guest, the team or a lapsed unpaid hold ended it.
What confirms a payment
The processor's webhook, never the guest returning to the site. A guest sent to Stripe or PayPal comes back to their own booking page, at SITE_URL, which says the payment is being confirmed until the webhook has arrived. A payment that fails leaves the booking pending, so the guest can try again until the 30-minute hold runs out.
- PayPal needs one more step than Stripe: approving leaves the order approved and takes nothing, so the return page captures it, and the capture's own result or the webhook settles the booking.
- A payment that arrives late for a booking already cancelled is recorded as paid and left for the host to refund: the nights may already belong to somebody else.
- Every provider retries its webhooks, and an event that arrives twice confirms the booking once.
Set up Stripe
In the API's .env, with a test key while you try it (a sk_live_ key moves real money, and the API's log says so when it starts):
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...| Event | What it does |
|---|---|
checkout.session.completed | Confirms the booking. The only event that does, because it is the only one that means the session was paid |
payment_intent.payment_failed | Marks the payment failed and leaves the booking pending, so the guest can pay again |
charge.refunded | Marks the payment refunded |
charge.dispute.created | A chargeback, treated as a refund |
On a server, create a webhook endpoint in Stripe at your API's public address followed by /api/webhooks/payments/stripe, for those four events, and put its signing secret in STRIPE_WEBHOOK_SECRET. Locally, forward the events with the Stripe CLI and put the secret it prints in .env:
stripe listen --forward-to localhost:8000/api/webhooks/payments/stripeThen complete a real test checkout in the browser with the card 4242 4242 4242 4242. stripe trigger checkout.session.completed is not useful here: a triggered event carries no booking reference, so it is ignored. Without STRIPE_WEBHOOK_SECRET every Stripe webhook is refused and a card booking never settles.
Set up PayPal
PAYPAL_CLIENT_ID=...
PAYPAL_CLIENT_SECRET=...
PAYPAL_WEBHOOK_ID=...
PAYPAL_ENV=sandbox- PayPal is offered once the client id and secret are both set.
PAYPAL_ENVissandboxunless you setlive, so a misconfigured deployment takes no real money. - Subscribe a webhook to
PAYMENT.CAPTURE.COMPLETED(confirms the booking),PAYMENT.CAPTURE.DENIED(payment failed, booking stays pending),PAYMENT.CAPTURE.REFUNDEDandPAYMENT.CAPTURE.REVERSED(marked refunded) at your API's address followed by/api/webhooks/payments/paypal, and put the subscription's id inPAYPAL_WEBHOOK_ID: it is what verifies PayPal's signature. CHECKOUT.ORDER.APPROVEDis ignored on purpose: confirming there would hold nights for money that has not moved.
The webhook route
POST /api/webhooks/payments/:provider, with stripe or paypal as the provider. It needs no sign-in: each request is checked by the provider's own signature over the raw body and a timestamp, so it can be neither edited nor replayed. A bad signature answers 401 and an unknown provider 404; every verified event answers 200, including the ones about something else, so the provider never retries them for days.
The API must be reachable from the internet at that address. Behind a proxy, forward the request body untouched: a re-serialised body no longer matches its signature.
Cancellations and refunds
- A guest cancels from their booking page, free, while the booking is pending or confirmed and the local check-in time is at least seven days away. Inside that window the page says to contact the host. The policy text guests read is the
cancellation_policysetting; the seven days are a constant in the API. - The host cancels from the dashboard at any time before the booking is completed.
- The product never moves money back. Cancelling a paid booking marks its payment
refund_pending; the host refunds it in Stripe's or PayPal's own dashboard, and the refund webhook is what writesrefunded. A booking paid on arrival is refunded by the host directly. - Cancelling frees the nights at once, and notifies every admin who can view bookings.
Who can do what
| Permission | Allows |
|---|---|
bookings.view | Reading bookings, and being notified of payments, pay-on-arrival requests and cancellations |
bookings.edit | Confirm, Check in, Complete and Cancel, and the internal note |
settings.view, settings.edit | Seeing and changing the Payments tab: pay on arrival and the cancellation policy |
Both seeded roles hold bookings.view and bookings.edit; only the Super Admin holds the settings permissions.
The routes behind them
GET /api/payments/methods answers { methods: [{ method, hosted }], allowPayOnArrival, simulated }. POST /api/bookings is public and answers with the booking's reference, its private access_token, its status and payment status, the total, and a redirect_url to the processor's page for a hosted method; a method the list does not hold answers 422 with payment_method_unavailable. The guest's own routes take that token, or the email the booking was made with: GET /api/bookings/:reference, POST /api/bookings/:reference/settle (the return page's capture), /cancel and /review. The dashboard reads and moves bookings with GET and PATCH /api/bookings/manage/:reference, the latter with { "action": "confirm" }, check_in, complete or cancel, and admin_notes.
How the payment gateways fit together
Included with your purchase. Sign in to read, or open it in your download.
The gateway interface, how a method finds its processor, and how to add your own.