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 one1. 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.
| Field | Notes |
|---|---|
workDays | Capitalised full day names. "monday" or "Mon" will not match and the day generates no slots |
officeHours.timezone | An IANA zone. Slots come back in UTC; this is what they were computed against |
services[].name | The serviceName you pass to the slots call. Keep it identical |
services[].duration | Minutes. Also the slot length, unless you ask for interval slots |
spots | How 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:
| Parameter | Effect |
|---|---|
slotIncrementMinutes | Generate slots on a fixed interval instead of by service duration |
partySize | Size of the booking, for capacity checks |
location | Narrow 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:
| Endpoint | Anonymous | |
|---|---|---|
GET crm/reservations/definitions | 200 | public |
POST crm/reservations/slots | 201 | public |
GET crm/reservations/by-email/:email | 200 | public |
DELETE crm/reservations/cancel/:email/:number | — | public |
POST crm/reservations/create | 401 | requires 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,workDaysare capitalised -
slotsreturns real times and you render them inbusinessTimezone -
createis called from the runtime or your server, never with a token in the page -
startTime/endTimeare 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.