Partner API

Machine-to-machine access for hotel and business systems — guest passes, bookings, catalog, staff.

Base URL: https://<your-salon-domain>/api/v1

Authentication: every request carries your API key (issued by the salon in Admin → API keys):

Authorization: Bearer mp_live_...

Conventions: you address every resource by your own extern_id, which you supply on creation. All timestamps are ISO 8601 with offset (UTC recommended). Prices are integer cents. DELETE on catalog resources deactivates them ("passive") — history is preserved and they can be reactivated with PATCH {"active": true}.

Errors

StatusBodyMeaning
401{"error":"invalid_api_key"}Missing, malformed, or revoked key
403{"error":"insufficient_scope","required":"..."}Key lacks the required permission
400{"error":"invalid_request","detail":...}Validation failed (detail explains)
404{"error":"not_found"}No resource with that extern_id
409{"error":"extern_id_conflict"}You already used this extern_id
409{"error":"slot_unavailable"}Booking time no longer bookable
400{"error":"overlapping_shifts","weekday":n}Working-hours intervals overlap

Guest passes scopes: guest-passes:read / guest-passes:write

A guest pass is a personal booking link. The guest's details are pre-filled on the booking page, the reservation number is locked in, and the link stops working at valid_until (checkout).

POST/guest-passes
GET/guest-passes/{extern_id}
PATCH/guest-passes/{extern_id} — e.g. extend valid_until on late checkout
POST/guest-passes/{extern_id}/revoke — e.g. early checkout

Example: create a pass at check-in

POST /api/v1/guest-passes
Authorization: Bearer mp_live_...
Content-Type: application/json

{
  "extern_id": "pms-checkin-49821",
  "guest_name": "Nesrin Yilmaz",
  "guest_email": "nesrin@example.com",
  "guest_phone": "+32 470 00 00 00",
  "reservation_ref": "HTL-2026-777",
  "valid_from": "2026-09-05T00:00:00Z",
  "valid_until": "2026-09-08T23:59:59Z"
}

Response 201 — email or print booking_url for the guest:

{
  "guest_pass": {
    "extern_id": "pms-checkin-49821",
    "guest_name": "Nesrin Yilmaz",
    "guest_email": "nesrin@example.com",
    "reservation_ref": "HTL-2026-777",
    "valid_from": "2026-09-05T00:00:00+00:00",
    "valid_until": "2026-09-08T23:59:59+00:00",
    "revoked_at": null,
    "booking_url": "https://weedoo.massageplanner.app/g/60778b98..."
  }
}

Bookings scopes: bookings:read / bookings:write

POST/bookings — 1 member = solo, 2 members = couple
GET/bookings?from=&to=
POST/bookings/{ref}/cancel
POST/bookings/{ref}/approve — confirm a booking in status pending

{ref} is your extern_id, or the numeric booking number. A booking returns status pending when the salon must move portable equipment into the room first; it holds its slot either way.

Example: create a booking

POST /api/v1/bookings

{
  "extern_id": "pms-book-1201",
  "starts_at": "2026-09-06T13:00:00Z",
  "reservation_ref": "HTL-2026-777",
  "members": [
    {
      "service_extern_id": "classic-60",
      "customer": { "name": "Nesrin Yilmaz", "email": "nesrin@example.com" }
    }
  ]
}

Response 201:

{
  "booking": {
    "booking_no": 42,
    "status": "confirmed",
    "starts_at": "2026-09-06T13:00:00+00:00",
    "ends_at": "2026-09-06T14:00:00+00:00",
    "manage_token": "1d02...",
    "group_id": "afc2..."
  }
}

staff_extern_id (per member) and room_extern_id are optional — omitted, the engine picks any qualified therapist and suitable room. If the time cannot be satisfied you get 409 slot_unavailable.

Catalog: services, items, rooms scopes: catalog:read / catalog:write

GET/services · POST/services · PATCH/services/{extern_id} · DELETE/services/{extern_id}
PUT/services/{extern_id}/required-items — replaces the full list
GET/items · POST/items · PATCH/items/{extern_id} · DELETE/items/{extern_id}
GET/rooms · POST/rooms · PATCH/rooms/{extern_id} · DELETE/rooms/{extern_id}
PUT/rooms/{extern_id}/fixed-items · PUT/rooms/{extern_id}/restrictions

Example: create a service and require an item

POST /api/v1/services
{ "extern_id": "hotstone-90", "name": "Hot Stone Massage",
  "description": "Warmed basalt stones melt deep muscle tension",
  "duration_min": 90, "price_cents": 9500 }

PUT /api/v1/services/hotstone-90/required-items
[ { "item_extern_id": "stone-heater", "quantity": 1 } ]

Example: item and room

POST /api/v1/items
{ "extern_id": "stone-heater", "name": "Hot Stone Heater", "portable": false }

POST /api/v1/rooms
{ "extern_id": "room-2", "name": "Room 2", "capacity": 1 }

PUT /api/v1/rooms/room-2/fixed-items
[ { "item_extern_id": "stone-heater", "quantity": 1 } ]

PUT /api/v1/rooms/room-2/restrictions
[ "aroma-diffuser" ]

Staff, hours, time off scopes: staff:read / staff:write

GET/staff · POST/staff · PATCH/staff/{extern_id} · DELETE/staff/{extern_id}
PUT/staff/{extern_id}/skills — array of service extern_ids, replaces the set
PUT/staff/{extern_id}/working-hours — replaces the weekly schedule
GET/time-off · POST/time-off · DELETE/time-off/{extern_id}

Example: schedule with a split shift

PUT /api/v1/staff/therapist-ayla/working-hours
[
  { "weekday": 1, "start_time": "08:00", "end_time": "12:00" },
  { "weekday": 1, "start_time": "14:00", "end_time": "18:00" },
  { "weekday": 2, "start_time": "09:00", "end_time": "17:00" }
]

Weekday 0 = Sunday. Shifts on one day must not overlap (touching boundaries like 12:00/12:00 are fine).

Example: time off

POST /api/v1/time-off
{ "extern_id": "leave-2026-09-10", "staff_extern_id": "therapist-ayla",
  "starts_at": "2026-09-10T00:00:00Z", "ends_at": "2026-09-11T00:00:00Z",
  "reason": "Day off" }

Omit both staff_extern_id and room_extern_id to close the whole salon; pass room_extern_id for room maintenance.

Powered by massageplanner.app