Case Study · Marine charter

White Sails Singapore

A yacht charter operator with fifteen years of demand and no way to capture it directly. We built the owned booking channel and the automation behind it.

Vertical
Marine charter / luxury hospitality
Engagement
Jun–Aug 2026, iterative
Automated actions
32
Stack
WordPress + custom app on Postgres
White Sails Singapore booking platform — charter listing and booking flow
Live booking flow — whitesailssingapore.com

Scope

Demand-capture site build, custom booking platform, payments, automation layer and admin console. WordPress and Elementor front end, custom application on Postgres, HitPay, Google Calendar, GoHighLevel and Resend.

The situation

White Sails has run charters out of Singapore since 2011 — three catamarans, eighteen guests each. They had a website. What they did not have was a way to sell a charter on it.

Bookings arrived through Klook and comparable marketplaces, which carried three costs stacked on top of each other. The commission came off every transaction. The customer belonged to the platform rather than to White Sails, so there was no list to remarket to and no repeat-charter engine. And the marketplace booking model could not express their actual product, so anything sold outside it was quoted and confirmed entirely by hand.

That manual path ran roughly like this. Read the enquiry. Open the rate card. Check whether the date falls on a weekend or a Singapore public holiday. Add the catering items. Multiply extra guests by the per-vessel rate. Add the card processing fee, or don't, depending on the rail. Email the quote. Wait. Create a payment link in the HitPay dashboard and type the amount in by hand. Watch for the payment to land. Open the vessel calendar and create the event. Open the master calendar and create it again. Write the confirmation. Build an invoice in a separate tool, pull the next number from a register, export a PDF, attach it. Forward the details internally. Retype the customer into the CRM.

Fifteen steps across seven tools, per booking, and every one of them only happens while somebody is awake.

Part one: building the demand surface

A booking engine is worth nothing if traffic never reaches it with intent. The existing site sold the boats. It did not sell the occasions people actually charter for.

  • Eleven occasion landing pages, each built to capture a distinct intent: birthday, corporate and team building, proposal and pre-wedding, family and kids, wedding and solemnisation, fishing, sunset cruise, wedding anniversary, bachelorette, weekend escapes, and dockside events. Client-supplied marketing copy was used verbatim and validated sentence by sentence against the source.
  • A gift voucher storefront covering six tiers across three yachts by weekday and weekend rate, wired to the checkout with click events carrying price values.
  • A fleet and pricing comparison section with JSON-LD Product schema and nested AggregateOffer per vessel, SGD, weekday as low price and weekend as high, pointing at the booking domain — row-per-yacht, because one row per entity is what parsers expect and it reflows on mobile without horizontal scroll.
  • Internal linking rebuilt. The occasion grid on the homepage was static divs; converted to real anchors against verified live slugs, so eleven new pages actually receive link equity instead of sitting orphaned.
  • Three yacht pages reworked with video in the hero slideshow, gated behind data-src so the file only downloads when the carousel reaches it, with auto-advance pausing for the video's duration and resuming on end.

The whole front end was built as namespaced, self-contained widgets with double-init guards, because Elementor's rendering breaks in ways that are hard to diagnose once styles leak between components.

Part two: the booking platform

Five steps, fully branded, no handoff to a marketplace: vessel, date and time, add-ons, guest details, payment. Availability is live, so a sold slot never appears bookable. Catering add-ons carry in-page menu previews. Behind it, thirty-two automated actions.

Pricing that cannot be argued with

Every figure is recomputed on the server at checkout and the browser's numbers are discarded. Base rate by vessel and by weekday or weekend. Public holidays re-derived against a holiday table rather than trusted from a client-side flag. Extra guests at the per-vessel rate. Add-ons itemised. Then a flat five percent processing fee on Card, Google Pay, GrabPay, Paylater by Grab and UPI QR, and zero on PayNow, so the rail the customer picks sets the charge automatically.

The tamper-proofing is not the interesting part. The interesting part is that a human quoting by email has no mechanism to detect an altered amount before the money lands, and this does.

Payment integrity

The payment request is created locked to the selected rails, then the app reads back HitPay's echoed amount and refuses to proceed if it differs from the server figure by more than two cents. Nobody manually re-verifies that a provider created the amount they intended.

Incoming webhooks are HMAC-verified with a constant-time comparison and rejected on mismatch. Underpayments are not confirmed: the booking moves to a separate state, an admin alert fires, and the charter stays unconfirmed. Overpayments log and alert. Terminal states refuse reprocessing, so a duplicate webhook cannot double-finalise and a failed-payment callback cannot downgrade a booking already paid.

Double-booking caught at the instant of payment

Two customers can complete payment for the same vessel and slot seconds apart. A person watching a calendar cannot detect that in the gap. At finalisation the system queries for a conflicting paid booking, and on a hit flags the record, queues a customer email explaining the slot just filled, and sends a flagged alert to the office, instead of silently selling the same boat twice and finding out at the marina.

The fanout, and what happens when it breaks

Confirmed payment writes to the database first, then enqueues three durable side effects: calendar insert, customer confirmation, admin notification. The queue drains inline but abandons after five seconds, so a slow Google or a slow mail provider cannot stall the webhook and trigger the payment provider's own retry storm. Anything unfinished stays queued.

Calendar event ids are derived from a hash of the booking dedupe key, so a retry updates the existing event rather than creating a second. Events write to both the vessel calendar and a master operational calendar with full particulars.

Invoice numbers are allocated from a database sequence under a row lock, stamped once and never twice, with a branded PDF generated and attached to the confirmation. If PDF generation fails the email still sends and the failure is logged rather than swallowed.

Failed queue items retry at one, five, thirty, one hundred twenty, three hundred sixty and seven hundred twenty minutes. On the sixth failure the row is marked dead and one batched summary lists every failed task with its last error, rather than flooding the inbox. The CRM webhook posts the full record, including a signed reschedule link, on a five-second timeout, and never throws — a CRM outage cannot break checkout.

Four cron loops nobody has to remember

A queue processor drains due side effects every minute. A reconciliation job runs every five minutes against the payment provider directly, walking pending and underpaid bookings, re-deriving expected price, voiding the request when amounts do not match, and finalising the ones the provider confirms. That job is the net that catches a webhook which never arrived.

An expiry sweep every fifteen minutes releases abandoned attempts, returning held vouchers and promo slots to circulation rather than leaking inventory to people who never paid.

And a watchdog every ten minutes reads the heartbeat each cron writes, alerting when a job has been silent for three times its expected interval or failed three times running, then rearming on the next success. Most builds skip this. A stopped job produces no error, and absence of activity looks exactly like a quiet day.

Two revenue lines that did not exist before

A gift voucher shop issuing cryptographically random serials from an alphabet with no ambiguous characters, uniqueness re-checked against the table, six-month expiry stamped automatically. Redemption is a conditional single-winner update, so two people racing the same code produce one success and one clean rejection, which a paper ledger cannot do.

A promo engine supporting percentage, fixed-amount and free-add-on discounts, constrained by promotion window, cruise-date range, vessel, minimum subtotal, day of week, total redemption cap and one-use-per-email. Redemptions record only once money arrives, so abandoned checkouts do not consume the cap.

Admin

Bookings table with detail views and reschedule links, voucher and promo management, reports and analytics, protected by admin-only auth and row-level security enforced at the data layer rather than only in the UI. Rate limiting rejects a sixth pending booking from the same email inside ten minutes.

What transfers

The vertical is marine charter. The mechanism is not. Any business selling a time-slotted, capacity-constrained service at variable prices runs the same architecture underneath: availability, tiered pricing, payment, calendar, CRM, and a chain of downstream actions that must not fail quietly. Restoration crews against job sites. Aesthetics clinics against rooms and practitioners. Home services dispatching technicians against territory and skill. The domain language changes. The failure modes are identical.

And the marketplace problem generalises further than it looks. Any operator whose bookings arrive through a third party is renting demand and does not own the customer record. The fix is the same everywhere: build the direct channel, make it capable of expressing the real product, and instrument it so the transaction lands in a system you control.

Three decisions carry into any of them.

  • Write to the database before fanning out. The transaction is recorded before any downstream call is attempted, which is what makes every downstream step retryable. Most integrations are chained webhook to webhook, so one failed hop loses the record entirely and nobody learns about it until a customer complains.
  • Compute price on the server. The browser calculates for display. The server calculates for charge. Any system where the client computes the billed amount is one devtools session from a problem.
  • Monitor the monitors. Retries and reconciliation only help if they are running. The watchdog exists because a silent cron and a quiet Tuesday are indistinguishable from the outside, and the gap between them is measured in bookings.

Built by VELO. Production CRM and revenue systems.

Next step

Book a 30-minute audit. We'll walk your current pipeline live, identify the three highest-leverage gaps, and send a written roadmap within 48 hours.

Book a Free Pipeline Audit