ShopEazydevelopers
Recipes

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 }
        ]
      }
    ]
  }
}
  • days are the days with at least one open slot, in date order. date is YYYY-MM-DD in the store's timezone.
  • at is the slot's start as epoch milliseconds. Show it to the shopper in South African time. remaining is 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 from and to, epoch milliseconds, both inclusive. They never widen the window: an earlier from or a later to is clamped. A value that is not a number, or a from after to, is 400 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." }
      }
    ]
  }'
FieldMeaning
booking.atThe slot, epoch milliseconds
booking.modein_store, callout or virtual
booking.noteA 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."
  }'
FieldRule
nameRequired, up to 120 characters
phone, emailAt least one is required. Up to 40 and 200 characters
serviceOptional. The service's slug, so the merchant sees what was asked about. Up to 120 characters
requestedAtOptional. The time asked for, epoch milliseconds. Absent means any time
notesOptional. 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

CodeMeaning
400 invalid_parameterA bad from or to on availability
404 not_foundThe slug does not exist
422 validation_errorA missing name or contact, a slot that is full, or a service that needs booking.at. The message says which

On this page