Developers

Bookings

A booking is one appointment: a customer, a treatment, a start and end time. Every booking has two identifiers:

Statuses

Status Meaning
requested The customer asked for the slot, but something still has to be arranged (for example a second therapist). The business confirms or declines within its response time.
awaiting_payment The customer chose to pay online on the booking page and has not paid yet. The slot is held for 30 minutes. Unpaid, the booking becomes cancelled.
confirmed The appointment stands.
in_session The treatment has started.
completed The treatment is done. A completed booking always has an invoice.
cancelled Cancelled by the customer or the business.
no_show The customer did not turn up.
draft Internal state while a booking is being made. You will rarely see it.

List bookings

GET /api/v1/bookings

Scope: bookings:read. Newest start time first.

Query parameter Description
status Only bookings with this status, for example confirmed.
from Only bookings that start at or after this time (ISO 8601).
to Only bookings that start at or before this time (ISO 8601).
limit, offset See pagination.
curl "https://app.aphrabooking.com/api/v1/bookings?status=confirmed&from=2026-11-01T00:00:00Z&to=2026-11-30T23:59:59Z" \
  -H "Authorization: Bearer $APHRA_KEY"
{
  "bookings": [
    {
      "id": "6f1c2a5e-0b7d-4c8e-9a51-2f0e7d3b9c11",
      "reference": "K7Q2MX",
      "status": "confirmed",
      "startsAt": "2026-11-20T14:00:00+00:00",
      "endsAt": "2026-11-20T15:00:00+00:00",
      "total": 65,
      "currency": "USD",
      "notes": "Sensitive skin",
      "createdAt": "2026-11-14T09:12:44.120+00:00"
    }
  ],
  "limit": 50,
  "offset": 0
}

total is the price at the time of booking, including chosen extras. The final amount is on the invoice.

The list does not include the customer or the treatment yet.

Create a booking

POST /api/v1/bookings

Scope: bookings:write.

Use this when a booking is made somewhere else, for example in your own app or a form on your site that you built yourself. Aphra checks availability exactly as the booking page does. If the slot is free, the booking is made, the customer gets the usual confirmation by mail (and WhatsApp, if the business uses it), and your booking.created webhooks fire.

There is no payment step: a booking made through the API is confirmed (or requested) straight away, and the customer pays at the business.

The three steps, all with ids you get from the API itself:

  1. GET /api/public/services gives each treatment’s id, its variants and its addons.
  2. GET /api/availability with a service and variant id gives the free start times for a day.
  3. POST /api/v1/bookings with one of those times books it.

Body

Field Type Required Description
serviceId UUID yes The treatment. Get it from /api/public/services.
variantId UUID yes The variant of the treatment, for example the 60 or 90 minute version. One of the service’s variants.
slotStart string yes Start time in UTC, ending in Z. Use a value from /api/availability.
firstName string yes
lastName string yes
email string yes Used to find an existing customer or create a new one.
phone string no International format, for example +5997001234.
notes string no Shown to the business with the booking.
serviceAddress string no Required when the variant has address_required: true, for treatments given at the customer’s place.
addonIds UUID[] no Extras for the treatment, from the service’s addons.
locale string no Language for the customer’s messages: nl, en, de, es, fr or pt. Defaults to the business’s language.
whatsappOptIn boolean no Send true only if the customer ticked a box agreeing to WhatsApp messages; Meta requires you to be able to show that. Leave it out if you did not ask.

Promo codes, gift cards and prepaid passes cannot be used through the API. They only work on the booking page.

Example

curl https://app.aphrabooking.com/api/v1/bookings \
  -H "Authorization: Bearer $APHRA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "serviceId": "0d4f3b1e-6a2c-4f1a-9e7d-2b8c5a1f3e90",
    "variantId": "9a7e2c41-3b5d-4e6f-8a1b-c2d3e4f5a6b7",
    "slotStart": "2026-11-20T14:00:00Z",
    "firstName": "Maria",
    "lastName": "Janga",
    "email": "maria@example.com",
    "phone": "+5997001234",
    "locale": "en"
  }'

Response

201 Created:

{
  "booking": {
    "id": "6f1c2a5e-0b7d-4c8e-9a51-2f0e7d3b9c11",
    "reference": "K7Q2MX",
    "startsAt": "2026-11-20T14:00:00.000Z",
    "status": "confirmed",
    "pending": [],
    "total": 65,
    "currency": "USD"
  }
}

If status is requested, the slot is held but not yet promised. pending then says what the business still has to arrange, for example [{ "type": "staff", "count": 1 }]. The customer is told the business will respond, and you receive booking.created (with status requested) now and booking.cancelled later if the business declines.

Errors

A 422 means the booking was not made. The error field holds one of these codes:

Code What happened
unknown_service The treatment does not exist or is switched off.
unknown_variant The variant does not belong to this treatment.
slot_in_past slotStart is in the past.
slot_unavailable Nobody is free at that time. Fetch fresh times and let the customer choose again.
slot_just_taken Someone else booked the slot a moment ago. Same remedy.
customer_double_booked This customer already has an appointment at that time.
customer_blocked The business does not accept online bookings from this customer.
address_required The treatment is given at the customer’s place and serviceAddress is missing.
booking_failed, booking_line_failed Saving failed on our side. Safe to retry.

A 400 means a field is missing or has the wrong format. The error field then names the problem.

Retries

There is no idempotency key yet. If your request times out, list bookings for that time before you send it again, or you may create the booking twice.