Solution guide · Booking & Scheduling

Building a Booking System With Deposits and Cancellation Rules

The short answer

A booking system with deposits and cancellation rules is buildable in weeks, not months, but it is not the CRUD form vendors quote it as. The money and the calendar have to stay consistent across two systems that fail independently. Realistic bands from Digital Heroes delivery across 2,000+ projects: $15,000 to $60,000 to add this capability properly inside an existing product, $50,000 to $130,000 in 10 to 16 weeks as part of a first release, and $150,000 to $350,000 over 6 to 12 months for a full multi-resource platform with external calendar sync and a real policy engine. The number moves on how many resource types get co-booked and how big your cancellation matrix actually is once someone writes it down.

What this looks like once real people touch it

Saturday, 11:04am. Two customers open the 2:00pm Tuesday slot on their phones and both tap Book inside the same 300 milliseconds. The naive implementation runs a SELECT to check availability, sees the slot free, then INSERTs. Both requests read before either wrote. Now there are two confirmed bookings for one chair, two deposits charged, and nobody finds out until 1:58pm on Tuesday when the second customer is standing at reception holding a confirmation email with your logo on it.

You refund them. They dispute it anyway, because the refund takes five business days and they are annoyed. That costs you a dispute fee (currently $15 in the US on Stripe) whether you win or lose. This is not an exotic race you can shrug off at low volume: one popular slot on one busy morning is enough. And the fix is not in your application code. It is in your schema.

The slot is a database constraint, not an if statement

The only reliable place to prevent a double booking is the database, enforced by the engine, with no application code in the loop. In PostgreSQL that means the btree_gist extension and an exclusion constraint over a time range: EXCLUDE USING gist (resource_id WITH =, during WITH &&), where "during" is a tstzrange. Two concurrent transactions cannot both win. One gets a constraint violation and you turn that into a clean "slot just went" message. If your developer's answer is "we wrap it in a transaction", ask which isolation level. READ COMMITTED, the Postgres default, does not stop this. SERIALIZABLE does, at the price of serialisation failures your code must catch and retry.

Next comes the hold. Payment takes 20 to 90 seconds of human time, and during that window the slot must be reserved but not confirmed. So a booking is not one row state, it is a state machine: held with an expires_at, confirmed, cancelled_by_customer, cancelled_by_staff, no_show, completed. Something has to sweep abandoned holds, because abandoned checkouts are routine and those slots must come back. A cron sweep over an index on expires_at is fine at small scale. Durable job runners such as Temporal, Inngest or Trigger.dev earn their keep once reminders, hold expiry and refund retries all need to survive a deploy.

Then multi-resource. A massage needs a therapist, a room, and sometimes a specific table. A charter needs a boat, a skipper, and a berth. Two concurrent bookings that lock those resources in different orders will deadlock in Postgres, and the naive per-resource check will happily half-book. Lock resources in a deterministic order, sorted by resource id, always. Group classes add capacity and waitlists, and waitlist promotion when a seat opens is the same race wearing a different hat.

Time is not a number

"Just store everything in UTC" is correct for a one-off booking and wrong for a recurring one. A client books Tuesdays at 9:00am in Denver starting in February. You store the UTC instants. On the second Sunday in March, America/Denver shifts, and every future occurrence is now at 8:00am local. The client shows up an hour late for a session they paid a deposit on and blames you.

The correct model is local wall time plus the IANA time zone identifier (America/Denver, not "MST", and never a fixed offset like -07:00), resolved to UTC at read time. The tz database is not stable: governments change DST rules with weeks of notice, and tzdata ships multiple releases a year. Any offset you baked into a row is a bug waiting for a legislature.

Recurrence has a standard, RFC 5545, the same one iCalendar uses. Use its RRULE, EXDATE and RDATE semantics rather than inventing your own repeat table, because the moment a client says "cancel just next week's" or "move this one and all following", you are re-deriving iCalendar badly. Two nasty cases fall out of DST that most builds never handle: on spring forward, 2:30am does not exist, and on fall back, 1:30am happens twice. Decide what your booking engine does with both before a customer discovers it.

And the cancellation clock has a time zone too. "48 hours before" a 9:00am Monday appointment is 9:00am Saturday. If the customer is in London and the salon is in Chicago, whose 48 hours? Write it down. It is a business decision and it belongs in the contract.

The deposit is a distributed transaction, and it will fail halfway

The most common bad instinct: "just put a hold on the card." Card authorisations are not booking holds. On Stripe, an uncaptured PaymentIntent with capture_method set to manual is generally good for about 7 days on most cards, and shorter on some networks. A booking six weeks out cannot be secured with an auth.

The real options are: charge the deposit now and refund per policy, or save the payment method with a SetupIntent and charge later off_session. The second path has a trap. Off-session charges get declined with authentication_required when the issuer wants a 3D Secure challenge, and under PSD2 in Europe you need a valid mandate recorded at setup time. Your code must catch that decline, email the customer a link to authenticate, and hold the booking while it waits. That branch alone is usually a week of work nobody quoted.

Then idempotency. Every create call to Stripe gets an Idempotency-Key derived from your booking attempt, not generated fresh per request, or a customer with flaky hotel wifi who taps Pay twice gets charged twice. Webhooks are at-least-once and unordered: payment_intent.succeeded can arrive before your own API call has returned, can arrive twice, and can arrive for a booking the sweeper already cancelled. Stripe retries failed webhooks with backoff for up to three days. Your handler must be idempotent on the event id and must be a state machine rather than a sequence of assumptions. Never treat the browser redirect as proof of payment.

The failure you are actually engineering against: payment succeeds, the booking INSERT loses the exclusion-constraint race, and you are holding a stranger's $200 with no appointment attached. Put booking_id in the PaymentIntent metadata, run a reconciliation job that compares Stripe's ledger to your bookings table daily, and auto-refund orphans. Every mature booking build has this job. Every immature one learns it needs one from a customer.

The cancellation policy is versioned data, and one day you will defend it

Vendors quote cancellation as an if statement. In our delivery data it is the most under-scoped item on a booking project: a policy that sounds like one sentence in a kickoff call has consistently expanded to 20 to 30 distinct branches once written out. Tiered refunds (100% over 7 days, 50% between 2 and 7 days, 0% inside 48 hours). No-show versus late cancel, which are different amounts. Reschedule, which may or may not reset the clock and may or may not be allowed once. Staff-initiated cancellation, which is always a full refund. Weather and force majeure overrides. Deposits that are non-refundable but transferable once. Multiply by actor (customer, staff, admin) and by service type.

Two decisions separate a build that survives from one that does not.

First, snapshot the policy onto the booking. Store the policy version id and the exact rendered text the customer saw, not a foreign key to the live policy row. If the owner tightens the terms in March, every booking made in January must still run under January's terms. If your developer says "it just reads the current policy", that is a lawsuit-shaped bug.

Second, build the dispute evidence on day one. When a no-show disputes their deposit, the card networks route it under a cancelled-services reason code (Visa 13.7 territory), and you get a short window to respond through Stripe's dispute evidence API. What wins is boring and specific: timestamped proof the customer accepted that exact policy text at checkout, with a hash of the text, the IP, the user agent, plus the confirmation email showing the policy inline and the service date. If you did not record it at booking time you cannot manufacture it later, and you lose by default. Retrofitting evidence capture is impossible, which is why it belongs in version one.

One more thing that surprises owners: Stripe does not return the original processing fee when you issue a refund. A 50% refund on a $200 deposit does not leave the business with $100. It leaves roughly $94, after the 2.9% plus 30 cents already paid on the way in. Somebody eats that. Decide who, and put it in the policy text.

The calendar you do not control

If your staff keep their own Google or Outlook calendars, your availability is a lie the moment someone books their dentist outside your app.

Google Calendar gives you incremental sync with a syncToken and a 410 GONE when that token goes stale, which means you must fall back to a full resync and handle it gracefully rather than crash-looping. Push notification channels expire in days, not months, and must be renewed on a schedule. Microsoft Graph calendar subscriptions cap out at 4230 minutes, under three days, and also need proactive renewal. Refresh tokens get revoked when a user changes their password or an admin rotates consent, silently.

The failure mode is the dangerous kind: nothing errors, sync just stops, availability goes stale, and you double-book a real human. The competent build alerts on the age of the last successful sync per connected calendar, not on exceptions. If you cannot answer "how would we know sync broke", you do not have sync, you have a countdown.

What it costs and how long it takes

These are Digital Heroes bands from delivery across 2,000+ projects, not industry averages.

Adding deposits and cancellation rules to an existing product that already has auth, a jobs runner and a payments integration: $15,000 to $60,000. Building it as part of a first release, with the booking flow, staff console and policy engine: $50,000 to $130,000 over 10 to 16 weeks. A full platform with multi-resource scheduling, external calendar sync, waitlists, marketplace-style supply and a real policy engine: $150,000 to $350,000 over 6 to 12 months.

What specifically moves this capability's number:

Co-booked resource types. One resource is a week. Three resources that must all be free simultaneously, with buffers and travel time, is a different engine and roughly doubles the scheduling work.

The size of the policy matrix. Each row of tier by actor by service type is a rule and a test. Ten rules is fine. Forty rules is a month.

External calendar sync. Budget $8,000 to $15,000, and accept that it is the largest single source of post-launch tickets forever.

More than one country. SCA mandates, 3D Secure fallbacks, and local methods such as iDEAL or SEPA Direct Debit carry different refund windows and different dispute rules than cards. Each one is real work.

Staff override tooling. The receptionist needs to waive a fee, force a partial refund, move a booking outside policy, and rebook a no-show. This is routinely 30% of the build and is almost never in the brief.

Migrating live bookings that already have money attached, from a spreadsheet or from Acuity, with no downtime and correct policy mapping. Two to three weeks on its own.

When you should not build this

Most people asking this question should buy, and I will name names.

If your bookable resources are humans with calendars, your policy fits on one page, and booking is a feature of your business rather than the product itself, use Cal.com, Acuity Scheduling or Square Appointments. Cal.com is open source and self-hostable, has a real API and platform tier, and already handles the DST and RRULE problems you are about to rediscover. Square Appointments has a free tier and takes deposits with card on file at standard card rates. Acuity sits in the low tens of dollars per month at list for a small operation. Any of them beats a $40,000 custom build on both cost and correctness for a salon, a clinic, a studio, or a consultancy.

The honest test: can you describe every cancellation outcome in a single page, and is every resource a person with a Google Calendar? If yes, buy. You will be live next week.

Build custom when booking is the product, not the plumbing. Two-sided supply where the providers are your inventory. Assets with attributes that must be matched, like boats by capacity or rooms by equipment. Policies that are contractual rather than conventional, meaning legal has an opinion. Or booking data that has to join to the rest of your domain, where availability depends on stock, staff certifications, or a job in progress. Off-the-shelf tools cannot reach into your data model, and the integration tax will exceed the build.

Either way: do not build a payments layer. Use Stripe.

How to brief and vet a developer for this

Bring the cancellation matrix to the first call, written out, every row. If you have not written it, you are not ready to brief, and any quote you receive is fiction.

Then ask these, in this order. They expose someone who has read a tutorial rather than shipped this.

"Two people click the same slot in the same 100 milliseconds. Show me in the schema where that is prevented." You want a database constraint named out loud. Application-level checking or a vague "we use transactions" means they have not hit it.

"A client books recurring Tuesdays at 9am in Denver starting in February. What happens on the second Sunday in March?" If DST does not come up unprompted, stop.

"We take a $200 deposit for a booking six weeks out. Auth hold or charge?" If they say hold the card, they have not read Stripe's documentation on authorisation expiry.

"Your webhook handler gets payment_intent.succeeded twice, and the second time the booking is already cancelled. What happens?" You want idempotency on the event id and an explicit state machine.

"The owner edits the cancellation policy today. What happens to a booking taken last week?" If the answer is "it updates", walk.

"A no-show disputes their deposit. What exactly do we send Stripe?" If they cannot list the evidence fields, nobody is capturing them.

"What sweeps abandoned holds, and what happens if that job dies for six hours?"

Finally, ask them to draw the booking state machine on a whiteboard, unprompted. Count the states. If refunds are a boolean rather than their own states with amounts, they have never processed a partial refund against a tiered policy, which is the entire job you are hiring for.

Research & sources

The evidence behind this guide

Independent findings on why this investment pays off. Every link goes to the primary source.

  1. In an RCT, the no-show rate was 23.5% for patients receiving a text-message reminder versus 38.1% for the control group - a 14.6 percentage-point reduction (p = 0.04). Source: Clinical Pediatrics / PubMed Central (Lin et al.) (2016) →
  2. In an RCT, text-message reminders (11.7% missed) were non-inferior to telephone reminders (10.2% missed; difference not significant, within the 2% non-inferiority margin) but far cheaper - total cost EUR 230 for SMS versus EUR 8,910 for telephone over 6 months - making SMS more cost-effective. Source: BMC Health Services Research / PubMed Central (Junod Perron et al.) (2013) →
  3. Per Sensor Tower's State of Mobile 2026, worldwide consumers spent about $85 billion on apps in 2025 (up 21% YoY), and for the first time non-game apps surpassed games in consumer spending; generative-AI in-app purchase revenue more than tripled to top $5 billion. Source: Sensor Tower (via TechCrunch) (2026) →
  4. Median SaaS spend reached $9,455 per employee, and organizations leave an average of 36% of their SaaS licenses unused. Source: Zylo (2026) →
Rohan Malhotra · Enterprise Software Consultant

Rohan advises mid-market and enterprise teams on ERP, CRM and custom software, and has led delivery on dozens of business-software builds.

Writes for Digital Heroes, shipping business software for 2,000+ brands across 55+ countries since 2017.

FAQ

Frequently asked questions

How much does it cost to add deposits and cancellation rules to an existing booking flow?
Typically $15,000 to $60,000 if the product already has authentication, a background job runner and a payments integration. The spread is driven almost entirely by how many rules are in your cancellation matrix and whether staff need override tooling to waive fees and force refunds. A simple flat non-refundable deposit with one 48 hour rule sits at the bottom of that band. Tiered refunds across multiple service types and actors sit at the top.
How long does it take to build?
Ten to sixteen weeks for a first release that includes the booking flow, deposits, a policy engine and a staff console. A focused add-on to an existing product is usually four to eight weeks. Add two to three weeks if you need two-way Google or Microsoft calendar sync, and another two to three weeks if you are migrating live bookings that already have deposits attached.
Can you add this to our existing app without rebuilding it?
Yes, in most cases, and it is the cheaper path. The two things that decide the price are whether your database can enforce slot integrity with a real constraint and whether you already have a durable job runner for hold expiry and reminders. If your bookings table has no unique or exclusion constraint on resource and time, expect a data cleanup pass first, because you almost certainly have silent double bookings already.
What breaks at scale?
The double-book race, first and worst, because it is invisible at low traffic and constant on popular slots. After that: abandoned holds that never get swept and slowly starve availability, webhook handlers that assume ordering and start creating duplicate charges under retry storms, and external calendar sync that stops silently when a token expires. Every one of those is prevented at design time and expensive to retrofit.
Should we use Calendly or Cal.com instead of building?
If your resources are people with calendars, your policy fits on one page, and booking is not your product, then yes, use Cal.com, Acuity or Square Appointments and go live next week. Build custom only when booking is the product, when you match assets by attributes rather than by person, when the policy is contractual, or when availability depends on your own data like stock or staff certifications. Off-the-shelf tools cannot reach into your data model, and that integration tax is what kills the buy option.
We have a $25,000 budget. What can we realistically get?
A focused, correct build on top of an existing product: database-enforced slot integrity, Stripe deposits with idempotency and proper webhook handling, a snapshotted policy with two or three refund tiers, hold expiry, and dispute evidence capture. What will not fit: two-way external calendar sync, multi-resource co-booking, waitlists, and a rich staff override console. Spend the budget on the money-and-slot correctness, because that is the part you cannot bolt on later.
Why would anyone spend $150,000 to $350,000 on booking?
Because at that end booking is the business, not a form. That band buys multi-resource scheduling with buffers and travel time, two-way calendar sync with health monitoring, waitlists and capacity, a policy engine the client can edit safely with versioning, multi-currency payments with SCA handling, and a staff console that handles the 20% of bookings that go sideways. If your booking flow is how you take revenue from thousands of customers a month, the correctness is the product.
Can we just hold the card instead of charging a deposit?
Not for anything more than a week out. On Stripe an uncaptured authorisation is generally available for about 7 days on most cards and shorter on some networks, so a booking six weeks ahead cannot be secured with a hold. The real options are charging the deposit now and refunding per policy, or saving the card with a SetupIntent and charging later off-session, which requires handling authentication_required declines and a proper mandate in Europe.
What happens to recurring bookings when daylight saving changes?
They shift by an hour and your clients miss appointments, unless you store local wall time plus the IANA zone identifier rather than a UTC instant or a fixed offset. Recurrence should follow RFC 5545 semantics, the same standard iCalendar uses, so that exceptions and this-and-following edits behave predictably. Also decide upfront what happens to a 2:30am booking on spring forward, when that time does not exist, and on fall back, when it happens twice.
What should the first version of a booking app include?
Ship four things: a public booking page, staff calendars with availability rules, card payments or deposits, and automated email and SMS reminders. Leave memberships, packages, gift cards, and reporting dashboards for phase two; they roughly double the build cost and get redesigned after real usage anyway. In Digital Heroes MVP scopes, that four-feature core covers about 80 percent of daily front-desk work from day one.
How do I vet a software agency for a booking system project?
Ask to see a live booking system they built and break it yourself: try booking overlapping slots, cancelling inside the penalty window, and switching time zones mid-booking. An agency that has shipped scheduling before will talk unprompted about double-booking prevention, calendar sync conflicts, and no-show handling; one that has not will only talk about screens. Also ask who writes the booking-rules specification, because at Digital Heroes that document is the single best predictor of a project landing on budget.
How much should a small business budget for its first custom app or website?
For a focused first build, most small businesses land between $8,000 and $60,000: roughly $8,000 to $45,000 for a custom website and $25,000 to $60,000 for an internal tool or simple web app, based on Digital Heroes delivery across 2,000+ projects. Customer-facing products with payments, logins, or a mobile app start around $40,000. Quotes far below these bands usually mean a template with your logo on it, not software shaped around your workflow.
How quickly does a custom booking system pay for itself?
Payback comes from three lines: cancelled subscriptions, which run $100 to $600 a month for tools like Mindbody, recovered no-show revenue from deposits and reminders, and admin hours saved on manual scheduling. For businesses handling 300+ bookings a month, Digital Heroes typically sees a $20,000 to $30,000 build recover its cost within 18 to 30 months. Under about 100 bookings a month the math rarely works, and an off-the-shelf tool remains the right call.
Should I hire a freelancer or an agency to build my booking app?
A strong freelancer works for a simple booking page with payments, roughly the $5,000 to $12,000 range in our experience. Choose an agency once the project needs a designer, backend and frontend developers, and QA working at the same time, which describes nearly every system with staff schedules, payments, and reminders. The practical freelancer risk is bus factor: if one person leaves mid-project, an agency replaces them and you cannot.
How long does it take to build custom booking software?
Plan on 6 to 10 weeks for a working MVP and 3 to 5 months for a full platform with memberships, reporting, and integrations. Across Digital Heroes booking projects, the calendar engine takes about a third of the timeline because recurring availability, time zones, and double-booking prevention need heavy testing. Migrating data from your old tool usually adds 1 to 2 weeks at the end.
What happens to my software if the agency shuts down or we stop working together?
Nothing dramatic, if the engagement was set up correctly: the code sits in your repository, hosting runs on your cloud account, and a handover document explains how to deploy and operate the system. Any competent replacement team can then take over in days rather than months. If the agency controls the repo, the servers, or the domain, fix that now, because renegotiating access during a dispute is the most expensive place to discover the problem.
Who owns the code if an agency builds my booking software?
You should own it outright, and the contract must say so: full IP assignment on final payment, source code in a repository you control, and no clause tying the software to the agency's servers. Watch for vendors that keep ownership and charge a monthly license, which quietly turns your custom build back into a subscription. Digital Heroes assigns all code and hands over the repository, hosting accounts, and documentation at handoff, and that should be your baseline expectation from any agency.
Keep reading
let's build

Build something worth launching.

A plan, a team, a timeline, within 24 hours. No decks, no discovery calls. Tell us what you're building and we'll come back with a real scope and a real number.

message us directly · we reply within one business day

mission briefing

Monthly dispatch

Playbooks, real build costs, and what we're shipping. One email a month. No fluff.

visit us

New York HQ

1140 Broadway, Suite 704 · New York, NY 10001

Get directions
Online now

Hey there 👋 How can we help you today?