Booking systems for developers: what you still build yourself

Stanislav TyshchenkoEngineering9 min readSep 8, 2026
A build plan Gantt chart: the calendar UI bar is short, while availability, reschedule links, deposits and reminders all run past the two-week quoted line

Every booking build has four jobs in it: computing availability, keeping a reschedule link alive, taking money, and running reminders. Vendors sell you the first one. The other three are where the estimate goes wrong. Pick your tooling by which of the four you want to stop owning.

An agency emailed me last year with a scope they'd already sent the client: two weeks to add booking to a physiotherapy site they'd built in Next.js. Three practitioners, two rooms, deposits at booking. They shipped in seven weeks.

Nothing exotic went wrong. The calendar rendered on day three. What ate the other five weeks was everything around it — a practitioner who works Tuesdays at one address and Thursdays at another, a client who moved her appointment from a link in an email sent in March, a Stripe webhook arriving twice, and the reminder job that quietly stopped at 2am on a Sunday and nobody noticed until Wednesday.

That story is not unusual, and it's why I think "which booking API should I use" is the wrong first question. The better one is: which of these four jobs do I want to still own in a year?

Availability is a search, not a calendar read

Here's the part that surprises people who've only built meeting schedulers. For a Calendly-shaped problem, availability is a read: fetch a calendar, subtract busy blocks, return free slots. For a service business it's a constraint search.

Take the physio clinic. A 50-minute deep tissue session on Thursday at 15:00 is available only if all of these hold at once:

  • a practitioner qualified for that service is working Thursday
  • her shift covers 15:00 through 15:50, plus whatever buffer she needs after
  • the clinic itself is open at that address on Thursday
  • a treatment room is free for the whole block
  • she isn't already booked, and nothing has been held in a cart for the last eight minutes

Five constraints, and they compose. Add a second location and the practitioner's availability becomes location-dependent. Add a group class and capacity stops being one and starts being twelve, with a waitlist behind it. Add multi-day rentals — a bike hire, a photo studio — and a "slot" isn't a slot any more, it's an interval you have to check for overlap against a set.

The naive implementation generates candidate slots and filters them. That's fine for one practitioner. At four practitioners across two locations with fifteen services, you're generating a few thousand candidates per calendar month and doing it on every page load, and the booking page starts taking 900ms. So you cache. Then someone books, and the cache is wrong, and you've now got a double-booking bug that only reproduces under concurrency.

None of that is hard computer science. It's just a solid week of work that isn't on anyone's estimate, and it's the piece that a real booking API takes off you completely — you ask it for availability, it answers.

A booking is not an event. It's a small state machine that strangers can drive, from an email, months later, on a phone, without logging in.

The states are boring: pending → confirmed → completed, with cancelled and no-show hanging off the side, and rescheduled being a transition that has to atomically free one slot and take another. What isn't boring is the access story. The customer has no account. So you're issuing a signed token in a URL, and that token needs to still work in October, still resolve after the practitioner has changed her hours, and refuse politely once the appointment has passed rather than 500.

Then someone reschedules a paid booking into a slot with a different price, and you have to decide what happens to the money. Then the practitioner leaves the business and you have twenty future bookings pointing at a staff record you can't delete.

I've watched three different teams write this layer. All three wrote it more or less the same way, all three found the "reschedule across a price change" case in production rather than in review, and none of them enjoyed it.

Money changes which failures you can tolerate

Before payments, a bug means someone shows up at the wrong time and is annoyed. After payments, a bug means you took money for a slot that doesn't exist, and now you're doing refunds by hand.

The specific things that bite:

Holds. Between "customer clicked pay" and "Stripe confirmed", the slot has to be reserved but not booked, and released if they abandon. That's a TTL and a sweeper. Miss it and your calendar fills with ghosts.

Idempotency. Payment webhooks arrive more than once. If your handler creates a booking, you now have two.

Partial payment. Deposits are the normal case in service businesses, not an edge case — take 30% now, the rest at the appointment. That's two payment intents against one booking, and a reconciliation view when they disagree.

Refund policy as code. "Full refund up to 48 hours, half after that, nothing inside 12" is three branches, a timezone, and an argument with the client about whether the clock starts at booking time or appointment time.

The one that costs real money

Deposits plus reschedules is the combination that generates support tickets. A customer pays a 30% deposit, reschedules twice, then cancels inside the refund window. Decide up front what the deposit is attached to — the appointment or the customer — and write it down before you write the code.

Reminders make you the operator of a queue

The moment you send "your appointment is tomorrow at 3", you own infrastructure that has to run whether or not anyone is looking at it.

You need a scheduled job. That job needs to be idempotent, because it will run twice one day. It needs to survive the appointment being cancelled after the reminder was queued but before it was sent. It needs the customer's timezone, not the server's, and not the business's. If you're sending SMS it needs a per-country sender configuration and a bounce path. And it needs an alarm on itself, because a reminder system that silently stops is worse than no reminder system — the business has already stopped phoning people.

This is the job most likely to be scoped as "and we'll add reminders" in the last line of a proposal and then take four days.

What each approach hands you for free

Four common routes, scored against the four jobs above. Everything in this table is about what you don't write.

JobRoll your ownCalendar API (Google/Graph)Scheduling API (Cal.com, Cronofy, Nylas)Service commerce API
Multi-staff, multi-location availabilityYou write itYou write it — the API only reports busy/freeMostly handled; multi-location varies by vendorHandled
Booking lifecycle + customer reschedule linksYou write itYou write itUsually handledHandled
Payments, deposits, refundsYou write itNot in scopeRarely in scope — you wire Stripe yourselfHandled
Reminders as operated infrastructureYou run the queueNot in scopeUsually includedIncluded
Where your time actually goesAll of itSync edge casesPayments and the commerce layerYour UI

Two honest notes on that table. A calendar API is not a booking system and was never trying to be — if you picked Google Calendar as your backend, the availability work is still entirely yours. And the scheduling-API column is a generalisation across products that differ a lot; the vendor-by-vendor comparison has the detail.

Where I'd start, and when to ignore all of this

If you're building for one person who takes 1:1 calls, stop reading and use Cal.com or Calendly. Genuinely. The four jobs above collapse to one and a half at that scale, and paying for a commerce layer you don't use is silly.

If you're building for a business with staff, rooms, services of different lengths, and money changing hands, my recommendation is to own the interface and rent the engine. Build the booking UI yourself — that's the part the client will judge you on, that's where your design work shows, and it's the part that's actually cheap. Put someone else's availability engine, state machine, payment handling and reminder queue behind it. That's what running headless means in practice, and it's the shape our API is built for: a REST interface plus a typed TypeScript SDK, with the endpoint reference covering the actual calls.

What would change my mind: an unusual booking model. If your domain has a rule no commerce API models — bookings that depend on tide times, equipment that has to be serviced every N rentals, a licence that caps a customer at four sessions a year — you may spend more time fighting the abstraction than you'd have spent writing it. Prototype the availability query against a real API before you commit. If you can't express it in one call, build it yourself.

For agencies specifically there's a second axis that has nothing to do with code: who holds the account when the project ends. A per-booking price means the client pays for what they use and you're not reselling a subscription you have to support. We priced Opencals at $0.99 per completed appointment for that reason — a client with a slow February pays nothing, and nobody has to have a conversation about it. If you build for service businesses regularly, the agency side is the same idea at volume, and the MIT-licensed starter templates exist so you're not writing the booking UI from an empty file each time.

What Opencals won't do for you

Three things, said plainly.

We are not open source. The templates and the SDK are MIT and you can do what you like with them; the platform behind them is a hosted API. If your requirement is that everything runs on your own metal, we're the wrong answer and the open-source options are the right place to look.

We're not a meeting scheduler and we're not trying to be. Calendly is better at Calendly's job.

And the embarrassing one: our own homepage said "API launching Q1 2025" for considerably longer than Q1 2025 lasted. We launched on the Shopify App Store first and assumed developers would embed the widget. The developers who actually showed up didn't want a widget, they wanted the API, and it took us most of a year longer than that banner implied. If you're evaluating us on the strength of a roadmap promise, don't — evaluate us on what's in the docs today.

Frequently Asked Questions

Early Access — 3 Months Free

Ready to transform your service business?

Join 150+ businesses already using Opencals. Get 3 months completely free with all features unlocked.

No credit card required
Setup in 10 minutes
Cancel anytime