Developers

Public API

DevelopersPublic API

The public API powers the customer-facing booking flow. It requires no auth, is heavily rate limited (30 requests / 60s per IP), and is the only API surface you can hit without a token.

Conventions

  • All endpoints accept and return JSON.
  • Successful responses are wrapped in { data: ... }.
  • Errors follow the Errors conventions.
  • All times are ISO-8601 strings in UTC.
  • All prices are in minor units (cents) unless otherwise noted.

GET /api/public/:slug

GET/api/public/:slug

Returns the public profile of a business. Field names mirror the tenants table (snake_case). Only active tenants are returned; inactive or suspended tenants return 404.

{
  "data": {
    "id": "tn_xxx",
    "slug": "demo-salon",
    "business_name": "Demo Salon",
    "business_type": "salon",
    "description": "Full-service hair and nails.",
    "logo_url": "https://...",
    "cover_image_url": null,
    "timezone": "America/Vancouver",
    "currency": "CAD",
    "country_code": "CA",
    "city": "Vancouver",
    "province_state": "BC",
    "country": "Canada",
    "address_line1": "1234 Main St",
    "phone": "+16045551234",
    "email": "hello@demosalon.example",
    "cancellation_hours": 24,
    "cancellation_policy": "Free cancellation up to 24h before...",
    "allow_guest_booking": 1,
    "require_payment": 0,
    "widget_color": "#6366F1",
    "widget_mode": "popup",
    "template": "atelier",
    "template_accent": "#6366F1"
  }
}

GET /api/public/:slug/services

GET/api/public/:slug/services

Returns the list of services the business offers. Active AND online-bookable only, sorted by order_index then name. snake_case fields.

{
  "data": [
    {
      "id": "sv_abc",
      "name": "Haircut",
      "description": "Classic cut, wash, and style",
      "duration_minutes": 45,
      "price": 6500,
      "deposit_percent": 25,
      "image_url": null,
      "is_online_bookable": 1,
      "order_index": 0
    }
  ]
}

GET /api/public/:slug/staff

GET/api/public/:slug/staff

Returns the list of staff profiles marked as bookable. snake_case fields. Sorted by order_index. The API does NOT return a per-staff service mapping; staff availability is computed from the working_hours table per request.

{
  "data": [
    {
      "id": "sp_alice",
      "display_name": "Alice",
      "title": "Senior Stylist",
      "avatar_url": null,
      "color": "#6366F1"
    }
  ]
}

GET /api/public/:slug/availability

GET/api/public/:slug/availability

Returns bookable time slots for one service, one optional staff, and one date. Slots are 15-minute increments inside each staff member's working hours, less any existing booking.

serviceIdstringrequired

The service ID (e.g. sv_abc). Mandatory.

dateYYYY-MM-DDrequired

The date in the tenant's timezone.

staffIdstring

Optional. A specific staff ID. Omit for "any bookable staff".

timezoneIANA

Defaults to UTC. Currently informational only — slots are returned in the tenant's local YYYY-MM-DDTHH:MM:SS form.

{
  "data": {
    "date": "2026-08-15",
    "timezone": "America/Vancouver",
    "slots": [
      { "staffProfileId": "sp_alice", "staffName": "Alice", "startTime": "2026-08-15T09:00:00", "endTime": "2026-08-15T09:45:00" },
      { "staffProfileId": "sp_alice", "staffName": "Alice", "startTime": "2026-08-15T09:15:00", "endTime": "2026-08-15T10:00:00" }
    ]
  }
}
Slots are 15-min increments
Slot start times advance in 15-minute steps inside each staff member's working hours (see working_hours.day_of_week / open_time / close_time). Slot end time is start + service.duration_minutes. The slot is excluded if it overlaps any non-cancelled booking on that staff member, with the service's buffer_after_minutes plus the tenant's buffer_minutes.

POST /api/public/:slug/bookings

POST/api/public/:slug/bookings

Create a new booking. Returns 403 with code guest_disabled if the tenant does not allow guest checkout. Returns 409 with code slot_taken if the slot is gone.

serviceIdstringrequired

The service ID.

staffProfileIdstringrequired

The staff ID.

startDatetimeISO-8601required

Booking start time. Treated as tenant-local; the first 10 chars (YYYY-MM-DD) are the date the slot was found on.

customer.firstNamestringrequired

First name (1-100 chars).

customer.lastNamestring

Last name (max 100 chars).

customer.emailstringrequired

Email for confirmation.

customer.phonestring

Phone (max 40 chars).

customer.notesstring

Notes for the business (max 2000 chars).

sourceenum

online | widget | walk_in | phone | admin. Default online.

{
  "data": {
    "id": "bk_abc",
    "confirmationCode": "BF-A4F2X9",
    "status": "confirmed",
    "customerUserId": null
  }
}

If a customer JWT is provided in the Authorization header (aud: customer), the booking is also linked to that global account and customerUserId is set.

Slot conflicts return 409
If the slot was just taken by another customer, the response is 409 with code: "slot_taken". Re-fetch availability and ask the user to pick a new time.

GET /api/public/booking/:code

GET/api/public/booking/:code

Look up a booking by its confirmation code (9 chars, prefixed BF-, e.g. BF-A4F2X9). Anyone with the code can read it. Returns id, tenant_id, start_datetime, end_datetime, status, confirmation_code, notes, and the business slug + name.

POST /api/public/booking/:code/reschedule

POST/api/public/booking/:code/reschedule

Move a booking to a new time. The code is the full confirmation_code including the BF- prefix.

newStartDatetimeISO-8601required

New start time.

staffProfileIdstring

New staff. Defaults to the current staff.

POST /api/public/booking/:code/cancel

POST/api/public/booking/:code/cancel

Cancel a booking. Idempotent: if the booking is already cancelled, returns 200 with alreadyCancelled: true.

reasonstring

Free-text reason. Stored on the booking for audit.

GET /api/public/:slug/reviews

GET/api/public/:slug/reviews

List reviews for the business. The reviews table is not yet shipped in v1, so this endpoint currently returns an empty array. The route exists for forward compatibility.

{
  "data": []
}
Need a human?

Email hello@netwit.ca or call +1-604-206-8169. NetWit responds in 1 business day.