docs
/
Walkthroughs

Take bookings with reservations

Define a bookable service, generate real availability, and create a reservation — from the server API and from a page, with a clear line between the endpoints that are public and the one that is not.

Reservations turn a set of opening hours and a service duration into bookable slots, and record who took one. Three calls cover the whole flow:

crm/reservations/definitions   what can be booked
crm/reservations/slots         what is free on a given day
crm/reservations/create        take one

1. Create a reservation definition

A definition is the bookable thing — a consultation, a table, a rental window. It carries the hours, the working days and the services.

The datatype is reservation_definition. Not crm_reservation_definition. Creating it under the wrong name succeeds — you get a record back and everything looks fine — and then every slot request answers Reservation Definition not found. because the slot generator looks in the other collection. If you see that error and your id is definitely right, check the datatype before anything else.

curl -s -X PUT "$APPMINT_HOST/repository/create" \
  -H "orgid: $APPMINT_ORG" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "datatype": "reservation_definition",
    "name": "consultation",
    "data": {
      "name": "consultation",
      "title": "Consultation",
      "description": "A 30-minute call with the team.",
      "type": "service",
      "status": "active",
      "workDays": ["Monday","Tuesday","Wednesday","Thursday","Friday"],
      "officeHours": {
        "timezone": "America/Chicago",
        "startTime": "09:00",
        "endTime": "17:00"
      },
      "services": [
        { "name": "consultation", "title": "Consultation", "duration": 30, "price": 0 }
      ],
      "spots": 1,
      "emails": ["[email protected]"]
    }
  }'

Keep the sk from the response — it is the reservationDefinitionId everywhere else.

FieldNotes
workDaysCapitalised full day names. "monday" or "Mon" will not match and the day generates no slots
officeHours.timezoneAn IANA zone. Slots come back in UTC; this is what they were computed against
services[].nameThe serviceName you pass to the slots call. Keep it identical
services[].durationMinutes. Also the slot length, unless you ask for interval slots
spotsHow many bookings can hold the same slot

2. Ask what is free

curl -s -X POST "$APPMINT_HOST/crm/reservations/slots" \
  -H "orgid: $APPMINT_ORG" \
  -H "Content-Type: application/json" \
  -d '{
    "reservationDefinitionId": "6aa798c44f3cf920a2c64b07",
    "serviceName": "consultation",
    "serviceDate": "2026-09-16"
  }'
{
  "reservationDefinition": { "…": "…" },
  "service": { "name": "consultation", "duration": 30, "price": 0 },
  "location": null,
  "slots": [
    {
      "startTime": "2026-09-16T14:00:00.000Z",
      "endTime": "2026-09-16T14:30:00.000Z",
      "spotsAvailable": 1,
      "businessTimezone": "America/Chicago"
    }
  ]
}

Sixteen slots for a 9-to-5 day at 30 minutes each, minus anything already booked. The generator subtracts existing reservations, so this is real availability rather than a template.

startTime is UTC; businessTimezone is the business's zone. 14:00Z above is 9:00 AM in Chicago. Format for display with the business timezone, not the visitor's — a customer in London booking a Dallas shop needs to see the shop's hours, and showing them 2:00 PM for a 9:00 AM appointment is a booking nobody turns up to.

new Date(slot.startTime).toLocaleTimeString([], {
  hour: 'numeric', minute: '2-digit', timeZone: slot.businessTimezone
});   // "9:00 AM"

Optional parameters:

ParameterEffect
slotIncrementMinutesGenerate slots on a fixed interval instead of by service duration
partySizeSize of the booking, for capacity checks
locationNarrow to a service point

3. Create the reservation

curl -s -X POST "$APPMINT_HOST/crm/reservations/create" \
  -H "orgid: $APPMINT_ORG" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "reservationDefinitionId": "6aa798c44f3cf920a2c64b07",
    "serviceName": "consultation",
    "startTime": "2026-09-16T14:00:00.000Z",
    "endTime":   "2026-09-16T14:30:00.000Z",
    "partySize": 1,
    "customer":  { "name": "Jane Operator", "email": "[email protected]" },
    "note": "Two coffee shops, 14 staff."
  }'

The reservation comes back with a human-readable reference in name:

{
  "sk": "6aa7b0b14f3cf920a2c64d0b",
  "datatype": "reservation",
  "data": {
    "name": "NT423ZCM",
    "serviceName": "consultation",
    "startTime": "2026-09-16T14:00:00.000Z",
    "customer": {
      "id": "6aa7b0b14f3cf920a2c64d09",
      "name": "Jane Operator",
      "email": "[email protected]"
    }
  }
}

A customer record is created from customer if that email is not already known, and reused if it is.

Pass back the slot's startTime and endTime exactly as the slots call returned them. Rebuilding them from a local Date reintroduces the timezone the slot generator already resolved, and the reservation lands an hour out twice a year.

Which endpoints are public

This is the part that decides your architecture, and it does not match what the route definitions suggest. Verified by calling each one with no credentials at all:

EndpointAnonymous
GET crm/reservations/definitions200public
POST crm/reservations/slots201public
GET crm/reservations/by-email/:email200public
DELETE crm/reservations/cancel/:email/:number—public
POST crm/reservations/create401requires auth
{
  "code": "missing_authorization_header",
  "statusCode": 401,
  "error": "Authorization header is required. Send requests with `Authorization: Bearer <token>`.",
  "path": "/crm/reservations/create",
  "method": "POST"
}

So a visitor can browse availability from a static page with no backend, but cannot complete a booking that way. You have three options:

On an AppMint-hosted page — use window.appmint. The runtime is authenticated on that origin and carries the application's credentials, so create works from the browser. This is the easiest correct answer and the rest of this page uses it.

On a site you host — proxy it. Your server holds the credential and makes the create call. See The server proxy.

Signed-in customers — relay their token. Your server sends its own Authorization plus the customer's x-client-authorization, and the booking is attributed to them.

Do not solve this by putting an operator token in the page so fetch works. That token can read and write everything in the org, and a page is public by definition. Proxy it.

4. A booking widget with window.appmint

On an AppMint-hosted page, the same three calls are already wrapped:

var a = window.appmint.reservation;

// availability
a.availableSlots({
  reservationDefinitionId: DEF,
  serviceName: 'consultation',
  serviceDate: dayInput.value,     // "2026-09-16"
})
 .then(function (r) {
    var slots = (r && r.slots) || [];
    if (!slots.length) {
      note.textContent = 'Nothing free that day.';
      return;
    }
    slots.forEach(function (s) {
      var b = document.createElement('button');
      b.type = 'button';
      b.textContent = new Date(s.startTime).toLocaleTimeString([], {
        hour: 'numeric', minute: '2-digit', timeZone: s.businessTimezone
      });
      b.addEventListener('click', function () { chosen = s; /* … */ });
      list.appendChild(b);
    });
 })
 .catch(function () {
    note.textContent = 'Could not load times just now — email [email protected].';
 });
// book it
a.create({
  reservationDefinitionId: DEF,
  serviceName: 'consultation',
  startTime: chosen.startTime,     // straight from the slot
  endTime:   chosen.endTime,
  partySize: 1,
  customer: { name: values.name, email: values.email },
  note: values.note || '',
})
 .then(function (res) {
    var ref = res && (res.name || (res.data && res.data.reservationNumber));
    show('ok', 'Booked. A confirmation is on its way' + (ref ? ' — reference ' + ref : '') + '.');
 })
 .catch(function () {
    show('err', 'That slot would not take — it may have just gone. Pick another.');
 });

The rest of the reservation namespace: list, get, modify, cancel, definitions.

Three things worth building in from the start:

Bound the date input. There is no point offering last Tuesday:

day.min = new Date().toISOString().slice(0, 10);
day.max = new Date(Date.now() + 60 * 86400000).toISOString().slice(0, 10);
day.value = new Date(Date.now() + 86400000).toISOString().slice(0, 10);

Treat a failed create as a lost slot, not an error. Someone else may have taken it between the slots call and the click. Re-fetch availability instead of showing a stack trace.

Guard double submission the same way as a form — dataset.sending on the form element, not just a disabled button.

Verify against the backend

The success message in the UI is not proof. Read the collection:

s, d = req("GET", "/repository/find/reservation?perPage=25", token=t)
for r in d["data"]:
    dd = r["data"]
    print(dd["name"], dd["startTime"], dd["customer"]["email"])
# NT423ZCM 2026-09-16T14:00:00.000Z [email protected]

One booking should produce exactly one row. Two rows from one click means the handler is bound twice — see Run JavaScript on a hosted page, where that has a specific cause.

Confirmation emails are billed like any other outbound mail. An org with no credit books the reservation and sends nothing. Check the org's credit before telling a customer to watch their inbox.

Checklist

  • The definition is reservation_definition, workDays are capitalised
  • slots returns real times and you render them in businessTimezone
  • create is called from the runtime or your server, never with a token in the page
  • startTime/endTime are passed through untouched
  • One booking, one row in reservation

Next: Run JavaScript on a hosted page — without which none of the widget code above will actually run.