Take bookings
Show open slots for a service, book one on an order, and take enquiries for the rest.
A store can sell services: a haircut, a lesson, a call-out. A shopper picks a slot, and the slot is reserved when the order is placed. Where a service takes no online bookings, the shopper sends an enquiry instead.
You need a key with catalog:read and checkout:write, and requests:write for enquiries. Every key has them.
1. Find the services
curl "https://api.shopeazy.co/v1/store/services?limit=20" \
-H "Authorization: Bearer sez_pub_..."You can also list every item and filter by kind: GET /v1/store/products?kind=service. A service has kind: "service" and these details:
{
"kind": "service",
"details": {
"durationMins": 60,
"modes": ["in_store", "virtual"],
"requiresDeposit": false,
"depositValue": null,
"locationNote": null,
"availability": { "enabled": true }
}
}modes is how it can be delivered: in_store, callout or virtual. availability.enabled says whether it takes online bookings.
2. Read the open slots
curl "https://api.shopeazy.co/v1/store/products/haircut/availability" \
-H "Authorization: Bearer sez_pub_..."The slug is the item's slug. The answer lists open slots, already netted off what is reserved:
{
"data": {
"enabled": true,
"timezone": "Africa/Johannesburg",
"durationMins": 60,
"capacityPerSlot": 1,
"days": [
{
"date": "2026-11-02",
"slots": [
{ "at": 1793602800000, "remaining": 1 },
{ "at": 1793606400000, "remaining": 1 }
]
}
]
}
}daysare the days with at least one open slot, in date order.dateisYYYY-MM-DDin the store's timezone.atis the slot's start as epoch milliseconds. Show it to the shopper in South African time.remainingis the seats left in the slot.- The window runs from now plus the service's lead time to its booking horizon: 30 days by default, and never more than 120.
- Narrow it with
fromandto, epoch milliseconds, both inclusive. They never widen the window: an earlierfromor a latertois clamped. A value that is not a number, or afromafterto, is400 invalid_parameter.
When the item is not a service, or the service takes no online bookings, the answer is:
{ "data": { "enabled": false } }Show an enquiry form instead. See step 5.
With the client:
const availability = await shopeazy.products.availability('haircut', { from, to });
if (availability.enabled) {
// availability.days
}3. Book a slot on an order
Add a booking to the service's line. at is required when the service takes online bookings. Use one of the open slots from step 2.
curl https://api.shopeazy.co/v1/store/checkout/quote \
-H "Authorization: Bearer sez_pub_..." \
-H "Content-Type: application/json" \
-d '{
"lines": [
{
"itemId": "item_456",
"quantity": 1,
"booking": { "at": 1793602800000, "mode": "in_store", "note": "Fade, please." }
}
]
}'| Field | Meaning |
|---|---|
booking.at | The slot, epoch milliseconds |
booking.mode | in_store, callout or virtual |
booking.note | A note for the merchant |
Then place the order as in Headless checkout, with the same booking on the line. Slots are reserved atomically when the order is placed. If the slot was taken since you read the availability, the order is refused with 422 validation_error, and the message says to pick another slot. Read the availability again and let the shopper choose.
The order's line shows the booking:
{
"itemId": "item_456",
"title": "Haircut",
"quantity": 1,
"booking": { "at": 1793602800000, "durationMins": 60 }
}Take payment and confirm it as for any order.
4. Show a shopper their bookings
A signed-in shopper can read their upcoming bookings. This needs orders:read and a shopper credential. See Shoppers.
curl https://api.shopeazy.co/v1/store/me/bookings \
-H "Authorization: Bearer sez_pub_..." \
-H "X-Shopper-Session: <session token>"{
"data": {
"items": [
{
"orderId": "order_789",
"orderNumber": "1042",
"paymentStatus": "paid",
"itemId": "item_456",
"title": "Haircut",
"quantity": 1,
"at": 1793602800000,
"durationMins": 60,
"mode": "in_store",
"fulfillmentStatus": "unfulfilled"
}
],
"nextCursor": null
}
}Bookings are soonest first, one row per booked service line, from now on. Lines on cancelled orders are left out. The list is a page: limit defaults to 50 and is at most 100.
const page = await shopeazy.asShopperSession(token).account.bookingsPage();5. Take an enquiry
For a service that takes no online bookings, or a general question, send an enquiry. It needs requests:write.
curl https://api.shopeazy.co/v1/store/booking-requests \
-H "Authorization: Bearer sez_pub_..." \
-H "Content-Type: application/json" \
-d '{
"name": "Sipho Dlamini",
"phone": "+27821234567",
"service": "haircut",
"requestedAt": 1793602800000,
"notes": "Any time on Saturday morning."
}'| Field | Rule |
|---|---|
name | Required, up to 120 characters |
phone, email | At least one is required. Up to 40 and 200 characters |
service | Optional. The service's slug, so the merchant sees what was asked about. Up to 120 characters |
requestedAt | Optional. The time asked for, epoch milliseconds. Absent means any time |
notes | Optional. Up to 2 000 characters |
The answer is 201:
{ "data": { "id": "request_123", "status": "pending" } }await shopeazy.enquiries.submit({ name, phone, service: 'haircut', notes });The enquiry is saved before anything is sent. The merchant is told in the console and by email on a best-effort basis, and a failure there never fails your call. Do not retry a 201: you would file the enquiry twice.
There is deliberately no "send an email" endpoint. A publishable key ships in a browser, so a free-form email call would let anyone send mail from the merchant's own domain. You send structured fields. The recipient, subject and body are made on the server, and no HTML you send is ever put in an email.
The merchant answers enquiries in the console. An app connected through the Merchant API can list them with listBookingRequests and answer with decideBookingRequest.
Errors
| Code | Meaning |
|---|---|
400 invalid_parameter | A bad from or to on availability |
404 not_found | The slug does not exist |
422 validation_error | A missing name or contact, a slot that is full, or a service that needs booking.at. The message says which |