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 " – ". A start that is no free slot (just taken, outside the hosts' booking availability, sooner than 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" }'

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 E-mail 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"
  }
}

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).

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:

Subscribe to these events when setting up a webhook to receive real-time notifications about booking changes.