A service menu is a price list that also sets your day, because every row is a block of time.
A service menu app stores each bookable service as a row with a length in minutes, and that length is what turns a price list into a diary. Thirty minute appointments fill an eight hour day sixteen times. Fifty minutes with a ten minute turnaround fills it eight times. Nothing else on the row changes the shape of the day like that, so the columns you choose for the menu decide what the booking screen is able to offer and what the rota has to absorb. Most people build the booking screen first and try to retrofit a length onto it afterwards. This page takes the other order. It is for whoever is building a booking app for business and has reached the part where the services need times on them.
What follows is what belongs on a bookable row, how a calendar actually represents an appointment of any length, where buffer and turnaround have to live given that no calendar field exists for them, what to do with a service that has a gap in the middle, and which rule decides how the money moves. Field names and quotations come from Google's and Apple's own documentation, read on 14 October 2026.
Start hereThe short version
Minutes on the service, a start and an end on the booking.
Two numbers, kept in two places, doing two different jobs. The menu row holds the planned length, because that is the number you edit on the day a treatment grows by ten minutes. The booking holds a real start and a real end, written at the moment someone books, because an invoice and a complaint are both about what happened rather than about what the menu says today. Keep only the length and you have nothing to compare one booking against another. Keep only the start and end and you have nothing to generate tomorrow from.
The rest of a service menu is the part nobody warns you about. Buffer time has no field in the calendar API, so you either pad the block or carry a turnaround column of your own. Durations vary by staff member, so the length wants to sit on the join between staff and service and not only on the service. A treatment with a processing gap is not one block at all. And the money for something a customer attends in person is explicitly not allowed to go through in-app purchase.
What actually belongs on a service menu row?
Nine columns cover almost every trade. An id you generate rather than the name, because names get rewritten. A display name and a short description for the booking screen. A planned length in minutes. A turnaround in minutes, meaning the time after the appointment that belongs to you and is not for sale. A price. A flag for whether the service is bookable on its own or only as an add-on. A category, so a long menu can be grouped on screen. An active flag. And a sort order, because owners care about the order of their menu far more than any developer expects them to.
Two of those are worth arguing about before you write the schema. The active flag is not a nicety: the moment a booking references a service you can never delete that row without orphaning history, so retiring a service has to mean hiding it from the menu while leaving it readable. Teams that skip this discover it when an owner tidies up the menu in January and last year's takings stop adding up. The add-on flag earns its place differently. It is what stops someone booking a fifteen minute beard trim as a standalone slot that wrecks the shape of the afternoon. An add-on carries its own length and is added to a parent booking, which is the reason the booking stores its own total rather than reading the menu back at display time.
Then the column most menus are missing. Length is not a property of the service alone, it is a property of the service and the person doing it. A senior stylist does the cut in forty minutes that a new joiner needs an hour for, and an app that insists both take fifty is an app the staff work around. The fix is small. Keep the default length on the service, then put a nullable length on the row that joins a staff member to a service, and have availability read the override when one exists. Price usually varies along the same line, so that join row ends up holding both and you avoid duplicating the whole service per person.
A menu built this way is most of an appointment calendar app already. The slot generator needs four inputs and no more: the day, the staff member, the hours that person is working, and a length. It walks the working window in steps, drops any step that collides with an existing booking or a break, and returns what is left. Every hard part of that is in the length, which is why the menu is the right place to start and the booking screen is the wrong one.
Pick yours
What one menu row has to carry
The length on a row is rarely the whole story. Something sets it, something follows it, and the money has its own rule. Pick the closest thing to what you sell.
Planned length: 20 minutes for the cut, 15 for the beard as an add-on
What sets it: The hands-on time of one person, which barely varies
Turnaround after: 5 minutes to sweep and reset the chair
How to store it: One block per booking, turnaround in its own column
How it is paid: Attended in the shop, so card or Apple Pay, never in-app purchase
Common shapes, not a ruling. Time your own work for a fortnight before you commit a number to the menu.
How does a calendar store an appointment that is forty minutes long?
As a start and an end, and nothing else. The Google Calendar API events reference, read on 14 October 2026, gives an event a start object and an end object. Each holds either a date, for an all day event, or a dateTime for a timed one, plus an optional timeZone. There is no duration field anywhere in the resource. The reference calls the start inclusive and the end exclusive, and that one word is what makes back-to-back bookings behave: an appointment that ends at ten and one that starts at ten do not overlap, so you do not need a second of padding to keep them apart.
Three more field level facts save a rewrite later. A dateTime needs a time zone offset unless a zone is given separately in timeZone, so a bare local timestamp is not a valid value. For a recurring event the timeZone is required, and it is the zone the recurrence is expanded in, which is how a six o'clock weekly slot stays at six through a clock change instead of drifting to five. And the recurrence field takes RRULE, RDATE, EXRULE and EXDATE lines, but the reference states that DTSTART and DTEND lines are not allowed in it. A repeating service slot therefore takes its length from the master event's start and end, not from the rule, which is another reason the length has to be stored somewhere you control.
Two fields decide whether the event you write is any use to the person whose calendar it lands in. The transparency field defaults to opaque, and its other value, transparent, means the event does not block time on the calendar, which is the opposite of what a booking needs. And you are allowed to set the event id yourself: lowercase letters a to v and the digits 0 to 9, between 5 and 1024 characters, unique within the calendar. Deriving it from your own booking id makes the write idempotent, so a retry updates the event instead of creating a second copy of the same appointment. If you are about to add calendar sync, that single decision prevents most of the duplicate events people end up apologising for.
Your service id has nowhere obvious to go on a calendar event, and the answer is extendedProperties. The reference describes extendedProperties.private as properties private to the copy of the event that appears on this calendar, and extendedProperties.shared as properties shared between copies of the event on other attendees' calendars. Put the booking id and the service id in private. It gives you a way to find your own events again without matching on the title, which is what people do instead and then regret the first time an owner edits a title by hand. The status field is worth reading at the same time, because cancelled covers both a deleted event and a cancelled occurrence of a repeating one, and a booking app has to tell those apart.
Google Calendar API, Events resource reference, read 14 October 2026
Where does buffer time actually go?
Nowhere in the API, which is the answer that surprises people. The events reference has no buffer field, no gap field and no travel time field. The nearest things in the resource are the autoDeclineMode settings on focus time and out of office events, and those decide whether a conflicting invitation gets declined, not how far apart two appointments sit. Google Calendar as a product plainly believes in the idea: an appointment schedule has a setting called Buffer time, under Booked appointment settings, alongside Appointment duration and Maximum bookings per day, checked on 14 October 2026. The feature exists and the API field does not, so in your own app the gap is yours to build.
There are two honest ways to build it. Pad the event, so a forty minute treatment with a ten minute turnaround is written as a fifty minute block and everything downstream just works. Or keep the turnaround in its own column and subtract it inside the availability query. Padding is a few lines of code and it moves a small lie into the owner's calendar: the ten o'clock massage looks like it runs until eleven when the customer actually leaves at ten to. That lie is harmless until someone reads the calendar to answer the phone, or until you try to report on paid minutes. The separate column keeps both truths and costs you a query that has to know about it. Pick one deliberately and write down which, because a codebase with half of each produces gaps nobody can account for.
Before you choose the column, decide who owns the gap, because it is not always the service. A treatment room needs airing and a fresh couch cover whatever was done in it, so the gap belongs to the room and applies to every service booked into it. A mobile service needs travel, which belongs to the pair of consecutive bookings rather than to either one of them, and changes with every day's route. A barber's five minutes of sweeping belongs to the service. Put the number on whichever of those three actually causes it, and the rule stays true when the menu changes. Put it on the service when the room caused it, and you will be editing twenty rows the day you move premises.
Then the case that defeats a single duration altogether. A colour appointment is apply, then develop, then finish, and during the develop stage the stylist is free. Sold as one unbroken two and a half hour block it costs you an hour of sellable time on every booking, which in a two chair salon is most of the week. The model that works is a stage list: each stage has a length and a flag for whether staff are held during it, the booking writes one calendar event per held stage, and the slot generator is allowed to place another short service inside the gap. Trades with a processing step all have this shape, and it is the main reason an off the shelf fifteen minute grid never fits them.
How do you take money for something on the menu?
On a card, and that is an instruction rather than a concession. App Review Guideline 3.1.3(e), Goods and Services Outside of the App, read on 14 October 2026, says that if your app enables people to purchase physical goods or services that will be consumed outside of the app, you must use purchase methods other than in-app purchase to collect those payments, such as Apple Pay or traditional credit card entry. People get this backwards and describe a haircut as exempt from in-app purchase. It is not exempt, it is forbidden from it. Build the deposit or the balance on a card processor and the rule is satisfied. Wire it to Apple's billing because that felt safer and you have built the thing that gets rejected.
Group sessions are where a service menu stops being one case. Guideline 3.1.3(d), Person-to-Person Services, says that if your app enables the purchase of real-time person-to-person services between two individuals, naming tutoring, medical consultations, real estate tours and fitness training, you may use purchase methods other than in-app purchase, and then it says one-to-few and one-to-many real-time services must use in-app purchase. So the single appointment on your menu and the group class two rows below it do not automatically share an answer. Read both guidelines against your own services before you build one checkout for the whole menu, and if you are genuinely unsure which one covers a class, ask App Review rather than guessing, because the two sentences point in opposite directions for a class people attend in a room.
The line that decides it is usually where the thing is consumed. Guideline 3.1.1 says that if you want to unlock features or functionality within your app, giving subscriptions, in-game currencies, game levels, access to premium content and unlocking a full version as the examples, you must use in-app purchase, and that apps may not use their own mechanisms to unlock content or functionality. A class streamed inside your app is content unlocked within the app. The same class in a studio is a service consumed outside it. One menu can hold both, and when it does the payment path forks even though the two rows look identical on screen.
One practical consequence for the menu itself. Because the money for in-person work does not run through store billing, the price on a menu row is just a number in your database, with no product id, no review queue and no store approval between you and changing it. That is a real advantage of a service business app and it cuts the other way too: nothing validates your prices, nothing stops a tax rate going stale, and refunds are entirely your problem. Keep the price on the booking as well as on the menu, for the same reason you keep the length there.
Five ways to store a length, and what each one can do
| How the length is stored | Can offer start times | Past bookings stay true | Real gap visible | Handles a processing gap |
|---|---|---|---|---|
| Minutes on the menu row only | Yes | No | No | No |
| Start and end on the booking only | No | Yes | Only if you padded the block | No |
| Minutes on the row, start and end on the booking | Yes | Yes | No | No |
| Add a turnaround column to the row | Yes | Yes | Yes | No |
| Add a stage list to the service | Yes | Yes | Yes | Yes |
Most apps should start on the fourth row and add the fifth only when a trade needs it. The first two rows are the ones to avoid, and they are also the two that look simplest on day one.
Where this lands if a builder generated your app
Newly is an AI app builder. You describe an app in plain English and it writes a real React Native and Expo project you own, with two-way GitHub sync, so the schema is a file you can read rather than a setting in somebody's dashboard. It uploads iOS builds to TestFlight, publishes to Google Play internal testing and builds a standalone Android APK. It is $25 a month and there is no free tier.
The useful thing to know is which parts to dictate. Ask for the menu as rows with a name, a length in minutes, a turnaround in minutes, a price, an active flag and an add-on flag, and ask for a join table between staff and services with a nullable length and a nullable price on it. Ask for the booking to store its own start, end and total rather than reading the menu back. Those three sentences get you a schema you can live with for years, and they are much cheaper to say now than to migrate later. A generated app starts local first, with data on the phone and no accounts, and the agent adds a backend with sign-in, an API and a Postgres database the moment the app needs staff logins or bookings taken on two devices.
Two honest limits. No builder submits the iOS app for you, because App Review Guideline 4.2.6 says apps created from a commercialized template or app generation service will be rejected unless they are submitted directly by the provider of the app's content, so the Apple Developer account and the submit button are yours. And nothing about generating code tells you how long your own work takes. Time twenty real appointments with a stopwatch before you fill in the menu. If you are weighing up whether to ship at all, the numbers in what it costs to publish an app are the ones to start from.
Questions people ask about service menus and durations
It is the stored list of things a customer can book, and each row carries more than a name and a price. A usable row has a length in minutes, the staff who are allowed to perform it, the gap you need after it, and whether it can be booked on its own or only added to something else. The length is the part that does real work, because it decides how many bookings a day can hold and which start times you are allowed to offer. A price list on a wall needs none of that. A booking app needs all of it.
Write the menu down before you build the booking screen
Every service, its length, its turnaround, who can do it and what it costs. One table, written out longhand on paper first. The booking screen is an afternoon of work once that table is honest, and a month of rework when it is not.
Start building