Bookings
Bookings represent scheduled appointments, reservations, or time-based commitments between your business and someone else: a client in your CRM, or a person outside it (someone who booked online, a visitor). A booking is a standalone, first-class record with its own lifecycle, status workflow, and optional links to other business records. It is not a calendar event: the calendar and the agenda (GET /agenda) show bookings as their own kind, next to events, tasks and reminders.
Booking is the only model of an appointment. The former Calendar Booking (/calendar-booking, which required a calendar event) is gone; its records were moved to Booking.
Use Cases by Business Type
Service Businesses (Salons, Spas, Clinics)
Create bookings linked to a service and employee (the provider). Optionally link to a location if you have multiple branches. The client books a specific service with a specific professional at a specific time.
Consulting & Professional Services
Book consultation sessions linked to a client and employee (the consultant). Link to a quote or invoice to tie the booking to billable work.
Product-Based Businesses
Schedule product demos, pickups, or delivery windows by linking bookings to a product and client.
Hospitality & Accommodation
Manage room or facility reservations by linking bookings to a location (the room/facility), client (the guest), and optionally a service (room service, spa package).
Entity Links
Every booking can optionally link to one or more of these entities:
| Field | Entity | Example Use |
|---|---|---|
clientId |
Client | The customer making the booking |
employeeId |
Employee | The host: the staff member providing the service |
serviceId |
Service | The service being booked |
productId |
Product | The product being demonstrated or delivered |
locationId |
Location | The venue or room |
quoteId |
Quote | Associated price quote |
invoiceId |
Invoice | Associated invoice for billing |
All links are optional. A booking can have zero links (a simple time reservation) or multiple links (a full appointment with client, provider, service, and location).
The Person, the Place and the Video Call
When the booking is with someone who is not a client in your CRM, describe them on the booking itself. Where it happens can be text instead of one of your locations, and an online meeting has its link.
| Field | Description |
|---|---|
attendeeName |
Name of the person the booking is with |
attendeeEmail |
Their e-mail address |
attendeePhone |
Their phone number |
attendeeNote |
What they wrote when booking |
place |
Where, as text (an address or a room), when it is not a locationId |
meetingUrl |
Link of the video call (Zoom, Google Meet, Teams) |
The server also records who made the booking and when its status changed. These are read-only:
| Field | Description |
|---|---|
source |
STAFF (made in the app: by the business, Brane or the API) or PUBLIC (made online by the person it is with) |
confirmedAt |
When the booking was confirmed |
cancelledAt |
When the booking was cancelled or declined |
cancellationReason |
Why, as given then |
timeZone, locale |
Time zone and language of the person who booked online |
pageUrl |
The page they booked on |
attribution |
Where they came from: the campaign's UTM tags (source, medium, campaign, term, content), an ad's click id (gclid, fbclid, msclkid), the site that sent them (referrer, origin and path) and the page they entered on (landingPath). List one source's bookings with where={"attribution":{"is":{"source":"facebook"}}} |
utm |
Deprecated: the UTM tags of attribution, still written |
icsSequence |
SEQUENCE of the booking's calendar file: rises with every new time, confirmation, decline and cancellation |
reminderSentAt |
When the reminder a day before went to the person |
Status Workflow
Bookings follow a lifecycle defined by the BookingStatus enum:
Pending → Confirmed → Active → Completed
↓ ↓ ↓
Declined Cancelled NoShow
| Status | Meaning |
|---|---|
Pending |
Booking created, awaiting confirmation |
Confirmed |
Booking accepted and scheduled |
Active |
Booking is currently in progress |
Completed |
Booking finished successfully |
Declined |
The business turned the request down |
Cancelled |
Booking was cancelled before or during the appointment |
NoShow |
The person did not come |
New bookings are always Pending (a status in the body of POST or PUT is ignored). Confirm, decline and cancel a booking with their actions, which also record when:
| Action | From | To | Records |
|---|---|---|---|
PATCH /booking/:id/confirm |
Pending |
Confirmed |
confirmedAt |
PATCH /booking/:id/decline |
Pending |
Declined |
cancelledAt, cancellationReason |
PATCH /booking/:id/cancel |
Pending, Confirmed, Active |
Cancelled |
cancelledAt, cancellationReason |
decline and cancel take an optional body { "cancellationReason": "..." }. Any other change, such as confirming a cancelled booking, returns 400 with error 400126. Active, Completed and NoShow are set with a plain PATCH /booking/:id ({ "status": "Completed" }); a PATCH to Confirmed, Declined or Cancelled follows the rules of the actions above.
API Endpoints
All booking endpoints are under /api/v1/booking and require Bearer token or API key authentication.
Standard CRUD
| Method | Path | Description |
|---|---|---|
GET |
/booking |
List bookings (supports filtering, sorting, pagination) |
GET |
/booking/:id |
Get a single booking by ID |
POST |
/booking |
Create a new booking |
PUT |
/booking/:id |
Update a booking (status is ignored) |
PATCH |
/booking/:id |
Update some fields of a booking, status included, or move it |
DELETE |
/booking/:id |
Delete a booking |
PATCH |
/booking/:id/confirm |
Confirm a pending booking |
PATCH |
/booking/:id/decline |
Decline a pending booking |
PATCH |
/booking/:id/cancel |
Cancel a booking |
GET |
/booking-type/:id/slots?from=&to=&timeZone= |
Free slots of a booking type, by day |
GET |
/booking/:id/slots?from=&to=&timeZone= |
Free slots a booking of a booking type can move to |
Create a Booking
curl -X POST https://api.incrm.app/api/v1/booking \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Haircut Appointment",
"startDate": "2026-04-15T10:00:00.000Z",
"endDate": "2026-04-15T11:00:00.000Z",
"clientId": "6507a1b2c3d4e5f6a7b8c9d0",
"serviceId": "6507a1b2c3d4e5f6a7b8c9d1",
"employeeId": "6507a1b2c3d4e5f6a7b8c9d2"
}'
With someone who is not a client, online:
curl -X POST https://api.incrm.app/api/v1/booking \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Consultation",
"startDate": "2026-04-15T10:00:00.000Z",
"endDate": "2026-04-15T10:30:00.000Z",
"employeeId": "6507a1b2c3d4e5f6a7b8c9d2",
"attendeeName": "Iva Horvat",
"attendeeEmail": "iva@example.com",
"meetingUrl": "https://meet.google.com/abc-defg-hij"
}'
Book a Free Slot of a Booking Type
A booking of a booking type is never double-booked. List the free slots (at most 31 days at a time; times in UTC, grouped by day in timeZone; hostEmployeeIds says who is free), then book a slot's start. The slots follow each host's booking availability (/availability-schedule, "Booking availability" in the app: when they take bookings), date by date: a date override (dateOverrides) first, else nothing on a public holiday of the schedule's holidayCountry unless the date is one of its workedHolidays, else the weekly hours.
curl "https://api.incrm.app/api/v1/booking-type/6507a1b2c3d4e5f6a7b8c9e1/slots?from=2026-11-05T00:00:00%2B01:00&to=2026-11-06T00:00:00%2B01:00&timeZone=Europe/Zagreb" \
-H "Authorization: Bearer YOUR_TOKEN"
curl -X POST https://api.incrm.app/api/v1/booking \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"bookingTypeId": "6507a1b2c3d4e5f6a7b8c9e1",
"startDate": "2026-11-05T09:00:00.000Z",
"clientId": "6507a1b2c3d4e5f6a7b8c9d0",
"attendeeName": "Ana Horvat",
"attendeeEmail": "ana@example.com"
}'
The end is the start plus the type's durationMinutes; the host is employeeId (one of the slot's hostEmployeeIds) or the free host with the fewest bookings that day; the status is Confirmed (Pending when the type requiresConfirmation); the service, place and colour are the type's unless sent; a missing title is "minNoticeMinutes or later than maxDaysAhead) is 409042.
Move a Booking
PATCH /booking/{id} with a new startDate (and endDate) moves a booking. A Pending or Confirmed booking of an active booking type moves only to a free slot of its type, checked as a new booking is:
curl "https://api.incrm.app/api/v1/booking/6507a1b2c3d4e5f6a7b8c9d3/slots?from=2026-11-06T00:00:00%2B01:00&to=2026-11-07T00:00:00%2B01:00&timeZone=Europe/Zagreb" \
-H "Authorization: Bearer YOUR_TOKEN"
curl -X PATCH https://api.incrm.app/api/v1/booking/6507a1b2c3d4e5f6a7b8c9d3 \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "startDate": "2026-11-06T13:00:00.000Z" }'
GET /booking/{id}/slotslists the slots of its booking type with the booking's own time counted as free, so a 60-minute booking can move 30 minutes later. It is409047for a booking without a booking type,409045for a cancelled or declined one and409043when its type is inactive.- The end is the new start plus the type's
durationMinutes, whateverendDatesays, and the status stays. Its host stays when free, else the free host with the fewest bookings that day takes it; sendemployeeId(one of the type'shostEmployeeIds) to choose the host, which is checked too. - A start that is no free slot (of that host) is
409042, and nothing changes. - A booking without a booking type, of an inactive or deleted one, or one that is
Cancelled,Declined,Active,CompletedorNoShowmoves as sent and may overlap others. So does a change with"bookingTypeId": null, which makes it a booking by hand. - A new
endDatealone (a longer booking) is saved as sent.
Confirm or Cancel a Booking
curl -X PATCH https://api.incrm.app/api/v1/booking/6507a1b2c3d4e5f6a7b8c9d3/confirm \
-H "Authorization: Bearer YOUR_TOKEN"
curl -X PATCH https://api.incrm.app/api/v1/booking/6507a1b2c3d4e5f6a7b8c9d3/cancel \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "cancellationReason": "The client is ill" }'
E-mails to the Person and the Host
The person the booking is with gets an e-mail at attendeeEmail when the booking is made, confirmed, declined, cancelled or moved (a new startDate/endDate of a Pending or Confirmed booking), and a reminder a day before a confirmed booking. Each is from the business ("<business name>" <no-reply@incrm.app>), replies go to the booking type's notificationEmail (else, and for a booking without a booking type, the business's e-mail), and it is in the person's language (locale, else the business's) with the time in their zone (timeZone, else the zone of the host's booking availability). Each links to the page where they view, cancel or move the booking.
| When | Calendar file (.ics) | |
|---|---|---|
Made, Confirmed |
Confirmed | METHOD:REQUEST |
Made, Pending |
Request received | none |
| Confirmed | Confirmed | METHOD:REQUEST |
| Declined | Not confirmed, with the reason | METHOD:CANCEL |
| Cancelled | Cancelled (with the reason when the business cancels) | METHOD:CANCEL |
| Moved | New time | METHOD:REQUEST |
| A day before | Reminder (only bookings made more than a day ahead) | none |
The calendar file's UID is the booking's id and its SEQUENCE is the booking's icsSequence, so calendars update or remove the same event. POST /booking, PATCH /booking/{id} and the confirm, decline and cancel actions take notifyAttendee (default true); send false to change a booking without telling them. PUT /booking/{id} tells nobody.
When a booking is made, cancelled or moved online, the business gets an e-mail with the calendar file at the booking type's notificationEmail, and its host (the booking's employee) a BOOKING notification in the app, without an e-mail, when they have a user in the business. Every booking type needs notificationEmail (POST /booking-type without one, or a change that clears it, is 400261; an address that is no e-mail address 400262): one of the business's mail accounts or any address. The e-mail addresses of hosts and of their users are never used for bookings.
Filter by Date Range
Lists take the standard where filter (see Filtering). Bookings that start in April 2026:
curl "https://api.incrm.app/api/v1/booking?where=startDate:gte:date(2026-04-01),startDate:lte:date(2026-04-30T23:59:59.999Z)" \
-H "Authorization: Bearer YOUR_TOKEN"
Find Overlapping Bookings
Before booking a host or a room, look for bookings that overlap the time: the same employeeId or locationId, starting before the new booking ends and ending after it starts, not cancelled or declined. Overlaps are allowed (they are a warning, not an error), so check them yourself. As a JSON where (URL-encode it):
{"AND":[
{"OR":[{"employeeId":"6507a1b2c3d4e5f6a7b8c9d2"},{"locationId":"6507a1b2c3d4e5f6a7b8c9d4"}]},
{"startDate":{"lt":"2026-04-15T11:00:00.000Z"}},
{"endDate":{"gt":"2026-04-15T10:00:00.000Z"}},
{"status":{"notIn":["Cancelled","Declined"]}}
]}
When you edit a booking, add {"id":{"not":"<its id>"}} so it is no conflict with itself.
Filter by Status
curl "https://api.incrm.app/api/v1/booking?where=status:Pending" \
-H "Authorization: Bearer YOUR_TOKEN"
Filter by Linked Entity
Find all bookings for a specific client:
curl "https://api.incrm.app/api/v1/booking?where=clientId:6507a1b2c3d4e5f6a7b8c9d0" \
-H "Authorization: Bearer YOUR_TOKEN"
Booking Online (Public API)
Any web page can show a business's booking types and free slots and book one for a visitor through /api/v1/public/booking: the business's own website, a page builder page or the booking page on app.incrm.app. Brane has no widget or iframe; the page calls the API and draws its own form. No user signs in. The visitor becomes a client of the business, the booking lands in the host's calendar, and the visitor gets a secret token to cancel or move it.
A business takes bookings online when it has the booking module and at least one active public booking type (isPublic and isActive, set at /booking-type). Every endpoint answers 404 for a business without the module.
| Method | Path | Key | Description |
|---|---|---|---|
GET |
/public/booking/types |
required | The active public booking types of the key's business |
GET |
/public/booking/pages/{businessSlug} |
none | The business's name, logo and active public booking types |
GET |
/public/booking/types/{id}/slots?from=&to=&timeZone= |
optional | Free slots of a booking type, by day |
POST |
/public/booking |
optional | Book a free slot for a visitor |
GET |
/public/booking/manage/{token} |
none | The booking, by its manage token |
GET |
/public/booking/manage/{token}/slots?from=&to=&timeZone= |
none | Free slots the booking can move to |
POST |
/public/booking/manage/{token}/cancel |
none | Cancel it: { "reason": "..." } (optional) |
POST |
/public/booking/manage/{token}/reschedule |
none | Move it: { "start": "..." } |
The key is the business's publishable key (incrm_pub_..., the storefront key, sent as X-Api-Key), which is safe to embed in a page. When a page sends it to the slots or the booking endpoint, the booking type must belong to that business (404 otherwise).
Answers never carry internal data: no hosts or their e-mails, no notes, clients or other bookings. A slot has only start and end; the link of a video call is shown only to the visitor who booked (GET /public/booking/manage/{token}).
1. Booking Types
curl https://api.incrm.app/api/v1/public/booking/types \
-H "X-Api-Key: incrm_pub_YOUR_KEY"
{
"success": true,
"data": [
{
"id": "6507a1b2c3d4e5f6a7b8c9e1",
"slug": "consultation",
"name": "Consultation",
"description": "A first call about your project",
"durationMinutes": 30,
"color": "#3b82f6",
"location": { "kind": "VIDEO", "place": null, "meetingUrl": null },
"attendeeQuestion": "What should we build?",
"requiresConfirmation": false,
"minNoticeMinutes": 240,
"maxDaysAhead": 60
}
]
}
A page without the key (e.g. a shared booking link) reads the same types with GET /public/booking/pages/{businessSlug}, which answers { business: { name, slug, logo }, types: [...] }; logo is a URL valid for a day, or null.
2. Free Slots
Ask for a week at a time (at most 31 days). Times are UTC; timeZone (the visitor's, e.g. from Intl.DateTimeFormat().resolvedOptions().timeZone) only groups them into days.
curl "https://api.incrm.app/api/v1/public/booking/types/6507a1b2c3d4e5f6a7b8c9e1/slots?from=2026-11-02T00:00:00%2B01:00&to=2026-11-09T00:00:00%2B01:00&timeZone=Europe/Zagreb"
{
"success": true,
"data": {
"bookingTypeId": "6507a1b2c3d4e5f6a7b8c9e1",
"timeZone": "Europe/Zagreb",
"durationMinutes": 30,
"days": [
{ "date": "2026-11-02", "slots": [{ "start": "2026-11-02T08:00:00.000Z", "end": "2026-11-02T08:30:00.000Z" }] },
{ "date": "2026-11-03", "slots": [] }
]
}
}
3. Book
curl -X POST https://api.incrm.app/api/v1/public/booking \
-H "Content-Type: application/json" \
-d '{
"bookingTypeId": "6507a1b2c3d4e5f6a7b8c9e1",
"start": "2026-11-02T08:00:00.000Z",
"timeZone": "Europe/Zagreb",
"locale": "hr",
"name": "Iva Horvat",
"email": "iva@example.com",
"phone": "+385 91 000 0000",
"company": "Horvat d.o.o.",
"note": "Please call first.",
"answer": "A CRM for our clinic",
"pageUrl": "https://www.example.com/book",
"attribution": { "source": "google", "medium": "cpc", "gclid": "EAIaIQobChMI", "landingPath": "/book" },
"website": ""
}'
{
"success": true,
"data": {
"id": "6507a1b2c3d4e5f6a7b8c9f0",
"start": "2026-11-02T08:00:00.000Z",
"end": "2026-11-02T08:30:00.000Z",
"status": "Confirmed",
"manageToken": "q8J0r2n1QJ7oQd0pWbE9cS0e9H1mY3fKcZr8v5x4a2Q"
}
}
startmust be thestartof a free slot.timeZone,locale,nameandemailare required; unknown fields are rejected (400).attributionis where the visitor came from, as the page read it when they arrived (tags and click ids up to 200 characters,referrerandlandingPathup to 2048). A page that still sendsutm(deprecated) gets the same values inattribution.- The visitor becomes the business's client with that e-mail (an existing client with the same e-mail is reused). The booking has
source: PUBLIC, the visitor asattendee*and their note and answer inattendeeNote, and goes to a host who is free then. - The status is
Confirmed, orPendingwhen the booking typerequiresConfirmation. manageTokenis shown only in this answer; Brane looks it up by its hash and keeps it encrypted for the links of its own e-mails. Put it in the visitor's link to manage the booking; never log it.- The visitor gets the confirmation (or, while
Pending, word that the request arrived) and the host a notification; see E-mails to the Person and the Host. websiteis a honeypot: render it hidden from people (nottype="hidden") and leave it empty. When it is filled, the answer looks the same but nothing is stored.- One e-mail address may hold at most 3 upcoming bookings of a booking type.
4. Cancel or Move
With the token, the visitor reads the booking (GET /public/booking/manage/{token}: its booking type, time, status, where with the link of a video call, the business, canCancel and canReschedule), cancels it until it starts, or moves it while it is more than the booking type's minNoticeMinutes away:
curl -X POST https://api.incrm.app/api/v1/public/booking/manage/q8J0r2n1QJ7oQd0pWbE9cS0e9H1mY3fKcZr8v5x4a2Q/reschedule \
-H "Content-Type: application/json" \
-d '{ "start": "2026-11-03T09:00:00.000Z" }'
Pick the new time from GET /public/booking/manage/{token}/slots, where the booking itself does not count as busy. The new time is checked as a new booking is, so it is never double-booked; the booking keeps its id and token, its host stays when free (else another host takes it), and it is Pending again when the booking type requiresConfirmation. Cancelling a cancelled booking changes nothing.
Errors
| Status | Code | When |
|---|---|---|
400 |
400154 |
timeZone is no IANA time zone |
400 |
400152, 400153 |
The slot range is missing, reversed or longer than 31 days |
400 |
400157 |
Cancel: the booking has started |
400 |
400158 |
Move: the booking starts within the booking type's minNoticeMinutes |
401 |
401023 |
An invalid publishable key was sent (or none to /types) |
404 |
404056 |
The business does not take bookings online (no module, unknown slug, no public booking type) |
404 |
404057 |
The booking type is not active and public, or belongs to another business than the key |
404 |
404058 |
Unknown manage token |
409 |
409042 |
The slot is no longer free: show the slots again |
409 |
409044 |
The e-mail already has 3 upcoming bookings of the booking type |
409 |
409045 |
The booking is cancelled, declined or over |
409 |
409046 |
The booking can no longer be moved online (its booking type is gone, inactive or not public) |
429 |
429000 |
Rate limit: reads 60 a minute; booking, cancelling and moving 5 per 10 minutes each, per IP |
All of /public/booking answers any origin, with or without X-Api-Key, so a page on any domain can call it; send no credentials (cookies), which only Brane's own app gets. The in-browser page preview (Origin: null) can read, but not book, cancel or move.
The Booking Link
Every public booking type has a booking page on the app that works without a website: its publicUrl (GET /booking-type, GET /booking-type/{id}), https://app.incrm.app/book/{businessSlug}/{slug}, which is null for a booking type that is not public. Send it to a client or put it in an e-mail signature; it takes bookings while the booking type is active. The visitor picks a day and a free time in their own time zone (shown, and they can change it), gives their name and e-mail (phone, company, a note and the booking type's attendeeQuestion are optional) and gets the confirmation, with an .ics to add to their calendar. The page follows the browser's language (Croatian or English, with a switch; ?lang=hr or ?lang=en picks one).
https://app.incrm.app/book/{businessSlug}lists the business's active public booking types.https://app.incrm.app/booking/{token}is the page every booking e-mail links to: the booking and its status, with "Reschedule" and "Cancel" whilecanRescheduleandcanCancelallow them. Search engines do not index it.
On the Agenda
GET /agenda lists the bookings the signed-in user hosts (their employee record is the booking's employeeId) as items of kind booking, without cancelled and declined ones. It needs the booking module, but not bookings.read: a host always sees the bookings they host there, while bookings.read is for all of the business's bookings (GET /booking).
Permissions
Booking access is controlled by the bookings resource in the role-based permission system:
| Permission | Description |
|---|---|
bookings.read |
View bookings |
bookings.create |
Create new bookings |
bookings.update |
Update, confirm, decline and cancel bookings |
bookings.delete |
Delete bookings |
Assign these permissions to roles via the Settings → Roles page.
Webhooks
Booking lifecycle events fire standard webhooks:
booking.created— A new booking was createdbooking.updated— A booking was modified (status change, reschedule, etc.)booking.deleted— A booking was removed
Subscribe to these events when setting up a webhook to receive real-time notifications about booking changes.