Skip to the article
Aniq-UI

HollinaBookings and payments

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

MethodOffered whenWhere the guest pays
CardThrough Stripe when STRIPE_SECRET_KEY is set; through the shipped simulator while no processor is configured at allStripe's hosted Checkout page, so no card number reaches this application; with the simulator, a card form on the checkout
PayPalThrough PayPal when PAYPAL_CLIENT_ID and PAYPAL_CLIENT_SECRET are set; through the simulator while no processor is configured at allPayPal's approval page
Pay on arrivalWhile the allow_pay_on_arrival setting is on (Settings, Payments)Nothing online: the host collects it at the door
Configured on the APIOffered onlineRefused
Neither Stripe nor PayPalCard and PayPal, both settled by the simulatorNothing
Stripe onlyCard, through StripePayPal
PayPal onlyPayPal, through PayPalCard
BothCard through Stripe, PayPal through PayPalNothing

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:

The API's log
[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

StatusMeaningNext
PendingMade on the website, waiting for its payment or for the hostConfirmed or cancelled
ConfirmedPaid, or accepted by the host for pay on arrivalChecked in or cancelled
Checked inThe guest has arrived. A pay-on-arrival booking is marked paid hereCompleted or cancelled (a stay cut short)
CompletedThe stay is over, and the guest can review itNone
CancelledBy the guest, by the host, or because an unpaid hold ran outNone
  • 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, B0002 and 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):

The API's .env
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
EventWhat it does
checkout.session.completedConfirms the booking. The only event that does, because it is the only one that means the session was paid
payment_intent.payment_failedMarks the payment failed and leaves the booking pending, so the guest can pay again
charge.refundedMarks the payment refunded
charge.dispute.createdA 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:

Terminal
stripe listen --forward-to localhost:8000/api/webhooks/payments/stripe

Then 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

The API's .env
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_ENV is sandbox unless you set live, 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.REFUNDED and PAYMENT.CAPTURE.REVERSED (marked refunded) at your API's address followed by /api/webhooks/payments/paypal, and put the subscription's id in PAYPAL_WEBHOOK_ID: it is what verifies PayPal's signature.
  • CHECKOUT.ORDER.APPROVED is 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_policy setting; 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 writes refunded. 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

PermissionAllows
bookings.viewReading bookings, and being notified of payments, pay-on-arrival requests and cancellations
bookings.editConfirm, Check in, Complete and Cancel, and the internal note
settings.view, settings.editSeeing 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.

Stuck on a step?

Find a fix before you start over.

Troubleshooting

Cookie Preferences

We use cookies to enhance your browsing experience, analyze site traffic, and personalize content. By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising.