{"openapi":"3.1.1","jsonSchemaDialect":"https://spec.openapis.org/oas/3.1/dialect/base","info":{"title":"pamprr Public API","summary":"The pamprr partner API: bookings, availability, clients, services, staff and locations for one business, by key.","version":"1.2.0","description":"The pamprr public API for partner integrations. Authenticate with a bearer API key minted in the operator's Settings, which carries an access setting (scopes); each operation names the one scope it requires. Every response carries X-Request-Id (quote it to support). Rate limited per key over a sliding sixty second window: the allowance in effect is authoritative in X-RateLimit-Limit on every authenticated response (100 today), every authenticated request counts, and a 429 carries Retry-After. Lists paginate by opaque cursor (nextCursor). Writes accept an Idempotency-Key so a retry replays the first outcome. Additive only: fields, headers, parameters, enum values and endpoints are added, never removed or renamed, without at least six months' notice, a period that is only ever extended, never shortened. Availability is computed live at the moment of the request from the same rules the business's own online booking calendar uses; it is advisory, and the create booking call is the truth. Every time is ISO 8601 UTC; every business runs on Europe/London today, and the business and location records name the zone, which is where a per business zone would appear. A key created with notifications on sends the customer the confirmation email and SMS on create and the app notification on cancel and reschedule, subject to the business's own notification settings and to the customer having an email, a phone or the pamprr app; a key with notifications off sends nothing. Webhooks deliver signed events (Standard Webhooks headers) to endpoints a business registers; deliveries are retried for about two days and are at least once."},"servers":[{"url":"https://pro.pamprr.me","description":"Production"}],"externalDocs":{"description":"The developer documentation","url":"https://pro.pamprr.me/developers"},"security":[{"bearerAuth":[]}],"tags":[{"name":"Business","description":"The record the key belongs to."},{"name":"Locations","description":"The business's branches."},{"name":"Staff","description":"The roster's partner projection."},{"name":"Services","description":"The menu, and who performs each service."},{"name":"Availability","description":"The bookable slots, computed live from the business's own calendar rules."},{"name":"Bookings","description":"The diary: list, create, read, cancel and reschedule."},{"name":"Clients","description":"The client book: list, create and read."},{"name":"Webhooks","description":"Signed event deliveries to endpoints the business registers: the endpoints, their secrets, the test and the delivery log."}],"paths":{"/api/v1/business":{"get":{"tags":["Business"],"summary":"Get the calling key's business record","operationId":"getBusiness","x-pamprr-required-scope":"business:read","security":[{"bearerAuth":["business:read"]}],"description":"The business the key belongs to. Its names, slug and email, the time zone every time in this API is read against (Europe/London for every business today) and the currency every price is in (GBP, in pence).","responses":{"200":{"description":"Business record","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"type":"object","properties":{"business":{"$ref":"#/components/schemas/Business"}},"required":["business"]},"examples":{"business":{"summary":"The business the key belongs to","value":{"business":{"id":"00000000-0000-4000-8000-0000000b0001","name":"Example Studio Ltd","slug":"example-studio","displayName":"Example Studio","email":"hello@example.test","timezone":"Europe/London","currency":"GBP"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/v1/bookings":{"get":{"tags":["Bookings"],"summary":"List bookings","operationId":"listBookings","x-pamprr-required-scope":"bookings:read","security":[{"bearerAuth":["bookings:read"]}],"description":"The bookings in a start time window, oldest first. With no status filter every status but cancelled is returned; ask for status=cancelled or status=all to see cancellations. To keep a copy in step, poll with updatedSince set to your last poll time, status=all and a wide window: every booking created, changed or cancelled since then comes back. Each row is the Booking projection; the operator's private notes are never included.","parameters":[{"name":"startDate","in":"query","required":false,"schema":{"type":"string","format":"date"},"description":"Inclusive lower bound on start_time. Defaults to 30 days ago.","example":"2026-10-05"},{"name":"endDate","in":"query","required":false,"schema":{"type":"string","format":"date"},"description":"Inclusive upper bound on start_time. Defaults to 90 days from now.","example":"2026-10-11"},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["pending","confirmed","in_progress","completed","cancelled","no_show","all"]},"description":"Only this status; all for every status. Absent: every status but cancelled. Any other word is refused with 400 VALIDATION_FAILED.","example":"all"},{"name":"updatedSince","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"Only bookings whose last change (updatedAt, else createdAt) is at or after this instant, inside the window. The order stays start time. A value that is not an ISO 8601 instant is refused with 400 VALIDATION_FAILED.","example":"2026-10-06T08:00:00Z"},{"name":"staffProfileId","in":"query","required":false,"schema":{"type":"string","format":"uuid"},"description":"Only this staff member's bookings. A value that is not a UUID is refused with 400 VALIDATION_FAILED.","example":"00000000-0000-4000-8000-0000000000a1"},{"name":"locationId","in":"query","required":false,"schema":{"type":"string","format":"uuid"},"description":"Only this branch's bookings. A value that is not a UUID is refused with 400 VALIDATION_FAILED.","example":"00000000-0000-4000-8000-000000000c01"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"Rows per page, 1 to 100 (a value above 100 is treated as 100).","example":50},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"Opaque keyset cursor from the previous page's nextCursor. Store and pass it back unchanged with the same filters; absent on the first page. A cursor never expires; one from another list, or from different filters, is refused with 400 INVALID_CURSOR.","example":"eyJ2IjoyLCJsIjoiYm9va2luZ3MiLCJrIjpbIjIwMjYtMTAtMDZUMDk6MTU6MDAuMDAwWiIsIjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDBiMiJdLCJmIjoiNWQ0MTQwMmEifQ"}],"responses":{"200":{"description":"List of bookings scoped to the calling business","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"type":"object","properties":{"bookings":{"type":"array","items":{"$ref":"#/components/schemas/Booking"}},"nextCursor":{"type":["string","null"],"description":"Pass back as cursor to fetch the next page; null when this page was the last."}},"required":["bookings","nextCursor"]},"examples":{"window":{"summary":"Two bookings in the window, a second page to follow","value":{"bookings":[{"id":"00000000-0000-4000-8000-0000000000b1","customerName":"Alex Example","customerEmail":"alex@example.test","customerPhone":"+44 7700 900123","serviceName":"Signature Facial","serviceDuration":60,"staffName":"Maya Okafor","status":"confirmed","source":"online","paymentStatus":"deposit","startTime":"2026-10-06T09:15:00.000Z","endTime":"2026-10-06T10:15:00.000Z","priceInPence":6500,"notes":null,"createdAt":"2026-09-20T14:02:11.000Z","serviceId":"00000000-0000-4000-8000-0000000000e2","staffProfileId":"00000000-0000-4000-8000-0000000000a1","locationId":"00000000-0000-4000-8000-000000000c01","updatedAt":null,"cancelledAt":null},{"id":"00000000-0000-4000-8000-0000000000b2","customerName":"Sam Example","customerEmail":"sam@example.test","customerPhone":null,"serviceName":"Consultation","serviceDuration":30,"staffName":"Theo Brandt","status":"pending","source":"phone","paymentStatus":"unpaid","startTime":"2026-10-06T13:00:00.000Z","endTime":"2026-10-06T13:30:00.000Z","priceInPence":0,"notes":"First visit","createdAt":"2026-09-28T09:41:00.000Z","serviceId":"00000000-0000-4000-8000-0000000000e1","staffProfileId":"00000000-0000-4000-8000-0000000000a2","locationId":"00000000-0000-4000-8000-000000000c01","updatedAt":"2026-09-29T16:10:00.000Z","cancelledAt":null}],"nextCursor":"eyJ2IjoyLCJsIjoiYm9va2luZ3MiLCJrIjpbIjIwMjYtMTAtMDZUMDk6MTU6MDAuMDAwWiIsIjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDBiMiJdLCJmIjoiNWQ0MTQwMmEifQ"}}}}}},"400":{"description":"The window or a filter is malformed: a date bound that does not parse (code INVALID_DATE_RANGE), a status outside the seven words, a non UUID id or an unparseable instant (code VALIDATION_FAILED), or a cursor that does not decode, names another list or was issued under other filters (code INVALID_CURSOR)","x-pamprr-variants":[{"$ref":"#/components/responses/BadRequest"},{"$ref":"#/components/responses/InvalidCursor"}],"headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"tags":["Bookings"],"summary":"Create a booking","operationId":"createBooking","x-pamprr-required-scope":"bookings:write","security":[{"bearerAuth":["bookings:write"]}],"description":"Creates a booking on behalf of the calling business, confirmed and unpaid, recorded with the source api (the dashboard shows it as Partner). When the service's effective booking protection includes a deposit (deposit-only or both) and the business has Stripe live, the request is refused with 402 DEPOSIT_REQUIRED; when it is card capture, with 402 CARD_CAPTURE_REQUIRED; those bookings must be made through the consumer booking flow, which takes the deposit or the card. The slot must be free for the staff member: 409 BOOKING_OVERLAP when another booking has it, 409 SLOT_HELD while a customer is mid checkout for it (the hold frees itself within ten minutes if they abandon checkout). When the API key notifies customers, the customer receives the confirmation email with a calendar attachment if the booking carries an email, and the confirmation SMS if it carries a phone, subject to the business's own SMS settings. Send an Idempotency-Key so a retried request replays the first outcome instead of creating a second booking.","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","minLength":1,"maxLength":255},"description":"Optional but recommended: any string of 1 to 255 printable ASCII characters, a UUID v4 by preference, unique per request. For 24 hours the same key with the same request replays the first response (2xx, 4xx or 5xx alike) with Idempotent-Replayed: true; the same key with a different request is refused with 422 IDEMPOTENCY_KEY_REUSED; a retry while the first request is still running is refused with 409 IDEMPOTENCY_REQUEST_IN_PROGRESS; a malformed key is refused with 400 IDEMPOTENCY_KEY_INVALID. Keys are scoped to your business, so a rotated API key still replays. Requests are processed once, with one qualification: a retry more than sixty seconds after a request that never completed may execute afresh, so in the rare event of a timeout retry with the same key promptly.","example":"0b3f6c2e-8f0a-4d5b-9c1e-7a2b3c4d5e6f"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateBookingRequest"},"examples":{"concierge":{"summary":"A concierge books a facial with a named therapist at the Islington branch","value":{"customerName":"Alex Example","customerEmail":"alex@example.test","customerPhone":"+44 7700 900123","serviceId":"00000000-0000-4000-8000-0000000000e2","staffProfileId":"00000000-0000-4000-8000-0000000000a1","startTime":"2026-10-06T13:30:00.000Z","locationId":"00000000-0000-4000-8000-000000000c02","notes":"Booked by the hotel concierge"}}}}}},"responses":{"201":{"description":"Booking created (or, with Idempotent-Replayed: true, the stored outcome of the first request that carried this Idempotency-Key)","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"Idempotent-Replayed":{"$ref":"#/components/headers/Idempotent-Replayed"},"X-Original-Request-Id":{"$ref":"#/components/headers/X-Original-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"type":"object","properties":{"booking":{"$ref":"#/components/schemas/Booking"}},"required":["booking"]},"examples":{"created":{"summary":"The booking as created: confirmed, unpaid, source api","value":{"booking":{"id":"00000000-0000-4000-8000-0000000000b3","customerName":"Alex Example","customerEmail":"alex@example.test","customerPhone":"+44 7700 900123","serviceName":"Signature Facial","serviceDuration":60,"staffName":"Maya Okafor","status":"confirmed","source":"api","paymentStatus":"unpaid","startTime":"2026-10-06T13:30:00.000Z","endTime":"2026-10-06T14:30:00.000Z","priceInPence":6500,"notes":"Booked by the hotel concierge","createdAt":"2026-10-06T08:31:12.000Z","serviceId":"00000000-0000-4000-8000-0000000000e2","staffProfileId":"00000000-0000-4000-8000-0000000000a1","locationId":"00000000-0000-4000-8000-000000000c02","updatedAt":null,"cancelledAt":null}}},"replayed":{"summary":"The same body on a retry with the same Idempotency-Key; the response carries Idempotent-Replayed: true and X-Original-Request-Id req_9a8b7c6d5e4f3a2b1c0d9e8f","value":{"booking":{"id":"00000000-0000-4000-8000-0000000000b3","customerName":"Alex Example","customerEmail":"alex@example.test","customerPhone":"+44 7700 900123","serviceName":"Signature Facial","serviceDuration":60,"staffName":"Maya Okafor","status":"confirmed","source":"api","paymentStatus":"unpaid","startTime":"2026-10-06T13:30:00.000Z","endTime":"2026-10-06T14:30:00.000Z","priceInPence":6500,"notes":"Booked by the hotel concierge","createdAt":"2026-10-06T08:31:12.000Z","serviceId":"00000000-0000-4000-8000-0000000000e2","staffProfileId":"00000000-0000-4000-8000-0000000000a1","locationId":"00000000-0000-4000-8000-000000000c02","updatedAt":null,"cancelledAt":null}}}}}}},"400":{"description":"The request is malformed: the body is not JSON (code INVALID_JSON), a required field is missing (code VALIDATION_FAILED), startTime does not parse (code INVALID_START_TIME) or is in the past (code PAST_START_TIME), the branch named is not usable or the staff member does not work there (code INVALID_LOCATION), or the Idempotency-Key is malformed (code IDEMPOTENCY_KEY_INVALID)","x-pamprr-variants":[{"$ref":"#/components/responses/BadRequest"},{"$ref":"#/components/responses/InvalidLocation"}],"headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"description":"The booking must be made through the consumer booking flow: the service's effective booking protection includes a deposit and Stripe is live (code DEPOSIT_REQUIRED), or it is card capture and Stripe is live (code CARD_CAPTURE_REQUIRED)","x-pamprr-variants":[{"$ref":"#/components/responses/DepositRequired"},{"$ref":"#/components/responses/CardCaptureRequired"}],"headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The slot is not free for the staff member: another booking has it (code BOOKING_OVERLAP) or a customer mid checkout holds it (code SLOT_HELD); or a retry while the first request carrying this Idempotency-Key is still being processed (code IDEMPOTENCY_REQUEST_IN_PROGRESS)","x-pamprr-variants":[{"$ref":"#/components/responses/Conflict"},{"$ref":"#/components/responses/SlotHeld"}],"headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"$ref":"#/components/responses/IdempotencyKeyReused"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/v1/bookings/{id}":{"get":{"tags":["Bookings"],"summary":"Get a booking","operationId":"getBooking","x-pamprr-required-scope":"bookings:read","security":[{"bearerAuth":["bookings:read"]}],"description":"One booking by id, the Booking projection: the same fields as a list row. A cancelled booking is returned (you asked for it by id). A booking that is not yours, or an id that is not a UUID, is 404 NOT_FOUND.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"The booking id.","example":"00000000-0000-4000-8000-0000000000b3"}],"responses":{"200":{"description":"The booking","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"type":"object","properties":{"booking":{"$ref":"#/components/schemas/Booking"}},"required":["booking"]},"examples":{"booking":{"summary":"One booking by id, the same projection as a list row","value":{"booking":{"id":"00000000-0000-4000-8000-0000000000b3","customerName":"Alex Example","customerEmail":"alex@example.test","customerPhone":"+44 7700 900123","serviceName":"Signature Facial","serviceDuration":60,"staffName":"Maya Okafor","status":"confirmed","source":"api","paymentStatus":"unpaid","startTime":"2026-10-06T13:30:00.000Z","endTime":"2026-10-06T14:30:00.000Z","priceInPence":6500,"notes":"Booked by the hotel concierge","createdAt":"2026-10-06T08:31:12.000Z","serviceId":"00000000-0000-4000-8000-0000000000e2","staffProfileId":"00000000-0000-4000-8000-0000000000a1","locationId":"00000000-0000-4000-8000-000000000c02","updatedAt":null,"cancelledAt":null}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/v1/bookings/{id}/cancel":{"post":{"tags":["Bookings"],"summary":"Cancel a booking","operationId":"cancelBooking","x-pamprr-required-scope":"bookings:write","security":[{"bearerAuth":["bookings:write"]}],"description":"Cancels a booking the way the business cancels one in its dashboard. The booking is marked cancelled with an optional reason, any unpaid checkout link for it is closed, anyone on the waiting list for the slot is notified, and the customer is told through the pamprr app if they booked with it and the API key notifies customers. Cancelling through the API does not refund a deposit or charge a late cancellation fee; refunds are made by the business in its dashboard. A booking that is already cancelled is returned as it stands with alreadyCancelled set, and nothing fires again. Send an Idempotency-Key so a retried request replays the first outcome.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"The booking id.","example":"00000000-0000-4000-8000-0000000000b3"},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","minLength":1,"maxLength":255},"description":"Optional but recommended: the same rules as on create booking. For 24 hours the same key with the same request replays the first response; the same key against another booking is refused with 422 IDEMPOTENCY_KEY_REUSED.","example":"4e7d1c2b-8a9f-4b3e-a1c5-6d7e8f9a0b1c"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CancelBookingRequest"},"examples":{"withReason":{"summary":"A cancellation with a reason; an empty body is also allowed","value":{"reason":"Guest checked out early"}}}}}},"responses":{"200":{"description":"The booking as it now stands; alreadyCancelled is true when it was cancelled before this call and nothing fired","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"Idempotent-Replayed":{"$ref":"#/components/headers/Idempotent-Replayed"},"X-Original-Request-Id":{"$ref":"#/components/headers/X-Original-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"type":"object","properties":{"booking":{"$ref":"#/components/schemas/Booking"},"alreadyCancelled":{"type":"boolean","description":"True when the booking was cancelled before this call; the booking is returned as it stands and nothing fires again."}},"required":["booking","alreadyCancelled"]},"examples":{"cancelled":{"summary":"The booking cancelled by this call","value":{"booking":{"id":"00000000-0000-4000-8000-0000000000b3","customerName":"Alex Example","customerEmail":"alex@example.test","customerPhone":"+44 7700 900123","serviceName":"Signature Facial","serviceDuration":60,"staffName":"Maya Okafor","status":"cancelled","source":"api","paymentStatus":"unpaid","startTime":"2026-10-07T09:15:00.000Z","endTime":"2026-10-07T10:15:00.000Z","priceInPence":6500,"notes":"Booked by the hotel concierge","createdAt":"2026-10-06T08:31:12.000Z","serviceId":"00000000-0000-4000-8000-0000000000e2","staffProfileId":"00000000-0000-4000-8000-0000000000a1","locationId":"00000000-0000-4000-8000-000000000c02","updatedAt":"2026-10-06T08:45:30.000Z","cancelledAt":"2026-10-06T08:45:30.000Z"},"alreadyCancelled":false}},"alreadyCancelled":{"summary":"The booking was cancelled before this call; nothing fired","value":{"booking":{"id":"00000000-0000-4000-8000-0000000000b3","customerName":"Alex Example","customerEmail":"alex@example.test","customerPhone":"+44 7700 900123","serviceName":"Signature Facial","serviceDuration":60,"staffName":"Maya Okafor","status":"cancelled","source":"api","paymentStatus":"unpaid","startTime":"2026-10-07T09:15:00.000Z","endTime":"2026-10-07T10:15:00.000Z","priceInPence":6500,"notes":"Booked by the hotel concierge","createdAt":"2026-10-06T08:31:12.000Z","serviceId":"00000000-0000-4000-8000-0000000000e2","staffProfileId":"00000000-0000-4000-8000-0000000000a1","locationId":"00000000-0000-4000-8000-000000000c02","updatedAt":"2026-10-06T08:45:30.000Z","cancelledAt":"2026-10-06T08:45:30.000Z"},"alreadyCancelled":true}}}}}},"400":{"description":"The request is malformed: the body is not JSON (code INVALID_JSON), the reason is not a string of up to 500 characters (code VALIDATION_FAILED), or the Idempotency-Key is malformed (code IDEMPOTENCY_KEY_INVALID)","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The booking cannot be cancelled: it is completed (code BOOKING_COMPLETED) or was marked as a no show (code BOOKING_NO_SHOW); or a retry while the first request carrying this Idempotency-Key is still being processed (code IDEMPOTENCY_REQUEST_IN_PROGRESS)","x-pamprr-variants":[{"$ref":"#/components/responses/BookingCompleted"},{"$ref":"#/components/responses/BookingNoShow"},{"$ref":"#/components/responses/Conflict"}],"headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"$ref":"#/components/responses/IdempotencyKeyReused"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/v1/bookings/{id}/reschedule":{"post":{"tags":["Bookings"],"summary":"Reschedule a booking","operationId":"rescheduleBooking","x-pamprr-required-scope":"bookings:write","security":[{"bearerAuth":["bookings:write"]}],"description":"Moves a booking to a new start time, and optionally to another staff member, the way the business does in its dashboard. The end time follows from the booking's own duration; the customer's reminders are reset for the new time; the customer is told through the pamprr app if they booked with it and the API key notifies customers. The new time must be free for the staff member: 409 BOOKING_OVERLAP when another booking has it, 409 SLOT_HELD while a customer is mid checkout for it. The API does not check the business's opening hours or the staff member's schedule for a reschedule, so check availability first. A new staff member must be one of your staff (404 otherwise) and must work at the booking's branch (400 INVALID_LOCATION otherwise). Send an Idempotency-Key so a retried request replays the first outcome.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"The booking id.","example":"00000000-0000-4000-8000-0000000000b3"},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","minLength":1,"maxLength":255},"description":"Optional but recommended: the same rules as on create booking. For 24 hours the same key with the same request replays the first response; the same key against another booking or another time is refused with 422 IDEMPOTENCY_KEY_REUSED.","example":"9c2a5e71-4d0b-4f8e-9a6c-2b1d3e4f5a6b"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RescheduleBookingRequest"},"examples":{"nextMorning":{"summary":"Move the booking to the next morning, keeping the same therapist","value":{"startTime":"2026-10-07T09:15:00.000Z","staffProfileId":"00000000-0000-4000-8000-0000000000a1"}}}}}},"responses":{"200":{"description":"The booking at its new time","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"Idempotent-Replayed":{"$ref":"#/components/headers/Idempotent-Replayed"},"X-Original-Request-Id":{"$ref":"#/components/headers/X-Original-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"type":"object","properties":{"booking":{"$ref":"#/components/schemas/Booking"}},"required":["booking"]},"examples":{"moved":{"summary":"The booking at its new time; updatedAt set","value":{"booking":{"id":"00000000-0000-4000-8000-0000000000b3","customerName":"Alex Example","customerEmail":"alex@example.test","customerPhone":"+44 7700 900123","serviceName":"Signature Facial","serviceDuration":60,"staffName":"Maya Okafor","status":"confirmed","source":"api","paymentStatus":"unpaid","startTime":"2026-10-07T09:15:00.000Z","endTime":"2026-10-07T10:15:00.000Z","priceInPence":6500,"notes":"Booked by the hotel concierge","createdAt":"2026-10-06T08:31:12.000Z","serviceId":"00000000-0000-4000-8000-0000000000e2","staffProfileId":"00000000-0000-4000-8000-0000000000a1","locationId":"00000000-0000-4000-8000-000000000c02","updatedAt":"2026-10-06T08:40:02.000Z","cancelledAt":null}}}}}}},"400":{"description":"The request is malformed: the body is not JSON (code INVALID_JSON), startTime is missing or staffProfileId is not a UUID (code VALIDATION_FAILED), startTime does not parse (code INVALID_START_TIME) or is in the past (code PAST_START_TIME), the new staff member does not work at the booking's branch (code INVALID_LOCATION), or the Idempotency-Key is malformed (code IDEMPOTENCY_KEY_INVALID)","x-pamprr-variants":[{"$ref":"#/components/responses/BadRequest"},{"$ref":"#/components/responses/InvalidLocation"}],"headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The booking cannot be moved: it is cancelled (code BOOKING_CANCELLED), completed (code BOOKING_COMPLETED) or was marked as a no show (code BOOKING_NO_SHOW); the new time is taken by another booking (code BOOKING_OVERLAP) or held by a customer mid checkout (code SLOT_HELD); or a retry while the first request carrying this Idempotency-Key is still being processed (code IDEMPOTENCY_REQUEST_IN_PROGRESS)","x-pamprr-variants":[{"$ref":"#/components/responses/BookingCancelled"},{"$ref":"#/components/responses/BookingCompleted"},{"$ref":"#/components/responses/BookingNoShow"},{"$ref":"#/components/responses/SlotHeld"},{"$ref":"#/components/responses/Conflict"}],"headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"$ref":"#/components/responses/IdempotencyKeyReused"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/v1/clients":{"get":{"tags":["Clients"],"summary":"List clients","operationId":"listClients","x-pamprr-required-scope":"clients:read","security":[{"bearerAuth":["clients:read"]}],"description":"The business's active clients by name. With search, only those whose name, email or phone contains the text (case insensitive, the text taken literally). Each row is the Client projection; the operator's private notes are never included.","parameters":[{"name":"search","in":"query","required":false,"schema":{"type":"string","minLength":2,"maxLength":100},"description":"Only clients whose name, email or phone contains this text, case insensitive; the text is taken literally. Two to one hundred characters, otherwise 400 VALIDATION_FAILED.","example":"example"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"Rows per page, 1 to 100 (a value above 100 is treated as 100).","example":50},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"Opaque keyset cursor from the previous page's nextCursor. Store and pass it back unchanged with the same filters; absent on the first page. A cursor never expires; one from another list, or from different filters, is refused with 400 INVALID_CURSOR.","example":"eyJ2IjoyLCJsIjoiY2xpZW50cyIsImsiOlsiU2FtIEV4YW1wbGUiLCIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwYzIiXSwiZiI6ImUzYjBjNDQyIn0"}],"responses":{"200":{"description":"List of clients scoped to the calling business","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"type":"object","properties":{"clients":{"type":"array","items":{"$ref":"#/components/schemas/Client"}},"nextCursor":{"type":["string","null"],"description":"Pass back as cursor to fetch the next page; null when this page was the last."}},"required":["clients","nextCursor"]},"examples":{"byName":{"summary":"The active clients matching the search, in name order","value":{"clients":[{"id":"00000000-0000-4000-8000-0000000000c1","name":"Alex Example","email":"alex@example.test","phone":"+44 7700 900123","createdAt":"2026-04-11T10:00:00.000Z","updatedAt":null},{"id":"00000000-0000-4000-8000-0000000000c2","name":"Sam Example","email":"sam@example.test","phone":null,"createdAt":"2026-09-28T09:40:00.000Z","updatedAt":"2026-09-29T16:10:00.000Z"}],"nextCursor":null}}}}}},"400":{"description":"The search text is under two or over one hundred characters (code VALIDATION_FAILED), or the cursor does not decode, names another list or was issued under other filters (code INVALID_CURSOR)","x-pamprr-variants":[{"$ref":"#/components/responses/BadRequest"},{"$ref":"#/components/responses/InvalidCursor"}],"headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"tags":["Clients"],"summary":"Create a client","operationId":"createClient","x-pamprr-required-scope":"clients:write","security":[{"bearerAuth":["clients:write"]}],"description":"Creates a client record for the business, or returns the existing one. When an active client already has the given email (compared case insensitively) it is returned with existing set instead of a duplicate being created, and when only an archived client has it that record is restored and returned the same way, so the client id stays stable. The email is stored lower cased. Private notes cannot be set through the API. Send an Idempotency-Key so a retried request replays the first outcome.","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","minLength":1,"maxLength":255},"description":"Optional but recommended: the same rules as on create booking. For 24 hours the same key with the same request replays the first response (the 201 or the 200); the same key with a different request is refused with 422 IDEMPOTENCY_KEY_REUSED.","example":"7b8c9d0e-1f2a-4b3c-8d4e-5f6a7b8c9d0e"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateClientRequest"},"examples":{"walkIn":{"summary":"A till adds a walk in; the email is stored lower cased","value":{"name":"Robin Example","email":"Robin@Example.test","phone":"+44 7700 900456"}}}}}},"responses":{"200":{"description":"A client with that email already exists (an archived one is restored); returned with existing set","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"Idempotent-Replayed":{"$ref":"#/components/headers/Idempotent-Replayed"},"X-Original-Request-Id":{"$ref":"#/components/headers/X-Original-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"type":"object","properties":{"client":{"$ref":"#/components/schemas/Client"},"existing":{"type":"boolean","enum":[true],"description":"Always true on this response: the client existed before this call."}},"required":["client","existing"]},"examples":{"existing":{"summary":"An active client already had that email (or an archived one was restored)","value":{"client":{"id":"00000000-0000-4000-8000-0000000000c1","name":"Alex Example","email":"alex@example.test","phone":"+44 7700 900123","createdAt":"2026-04-11T10:00:00.000Z","updatedAt":null},"existing":true}}}}}},"201":{"description":"Client created (or, with Idempotent-Replayed: true, the stored outcome of the first request that carried this Idempotency-Key)","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"Idempotent-Replayed":{"$ref":"#/components/headers/Idempotent-Replayed"},"X-Original-Request-Id":{"$ref":"#/components/headers/X-Original-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"type":"object","properties":{"client":{"$ref":"#/components/schemas/Client"}},"required":["client"]},"examples":{"created":{"summary":"A new client record","value":{"client":{"id":"00000000-0000-4000-8000-0000000000c3","name":"Robin Example","email":"robin@example.test","phone":"+44 7700 900456","createdAt":"2026-10-06T08:50:00.000Z","updatedAt":null}}}}}}},"400":{"description":"The request is malformed: the body is not JSON (code INVALID_JSON), the name is missing or a field fails its rule (code VALIDATION_FAILED, with field naming it), or the Idempotency-Key is malformed (code IDEMPOTENCY_KEY_INVALID)","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"A retry while the first request carrying this Idempotency-Key is still being processed (code IDEMPOTENCY_REQUEST_IN_PROGRESS; retry shortly)","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"$ref":"#/components/responses/IdempotencyKeyReused"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/v1/clients/{id}":{"get":{"tags":["Clients"],"summary":"Get a client","operationId":"getClient","x-pamprr-required-scope":"clients:read","security":[{"bearerAuth":["clients:read"]}],"description":"One client by id, the Client projection: the same fields as a list row. An archived client, a client that is not yours, or an id that is not a UUID is 404 NOT_FOUND.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"The client id.","example":"00000000-0000-4000-8000-0000000000c1"}],"responses":{"200":{"description":"The client","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"type":"object","properties":{"client":{"$ref":"#/components/schemas/Client"}},"required":["client"]},"examples":{"client":{"summary":"One client by id","value":{"client":{"id":"00000000-0000-4000-8000-0000000000c1","name":"Alex Example","email":"alex@example.test","phone":"+44 7700 900123","createdAt":"2026-04-11T10:00:00.000Z","updatedAt":null}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/v1/services":{"get":{"tags":["Services"],"summary":"List services","description":"The business's menu in its own order. Every service with its duration, price in pence, flags and the branches offering it; inactive services are returned with isActive false. Each row is the Service projection.","operationId":"listServices","x-pamprr-required-scope":"services:read","security":[{"bearerAuth":["services:read"]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"Rows per page, 1 to 100 (a value above 100 is treated as 100).","example":50},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"Opaque keyset cursor from the previous page's nextCursor. Store and pass it back unchanged with the same filters; absent on the first page. A cursor never expires; one from another list, or from different filters, is refused with 400 INVALID_CURSOR.","example":"eyJ2IjoyLCJsIjoic2VydmljZXMiLCJrIjpbMiwiU2lnbmF0dXJlIEZhY2lhbCIsIjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDBlMiJdLCJmIjoiZTNiMGM0NDIifQ"}],"responses":{"200":{"description":"List of services scoped to the calling business","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"type":"object","properties":{"services":{"type":"array","items":{"$ref":"#/components/schemas/Service"}},"nextCursor":{"type":["string","null"],"description":"Pass back as cursor to fetch the next page; null when this page was the last."}},"required":["services","nextCursor"]},"examples":{"menu":{"summary":"The first page of the menu, a second page to follow","value":{"services":[{"id":"00000000-0000-4000-8000-0000000000e1","name":"Consultation","description":"A thirty minute skin consultation.","durationMinutes":30,"priceInPence":0,"isActive":true,"isOnlineBookable":true,"isAddOn":false,"locationIds":["00000000-0000-4000-8000-000000000c01","00000000-0000-4000-8000-000000000c02"]},{"id":"00000000-0000-4000-8000-0000000000e2","name":"Signature Facial","description":null,"durationMinutes":60,"priceInPence":6500,"isActive":true,"isOnlineBookable":true,"isAddOn":false,"locationIds":["00000000-0000-4000-8000-000000000c01","00000000-0000-4000-8000-000000000c02"]}],"nextCursor":"eyJ2IjoyLCJsIjoic2VydmljZXMiLCJrIjpbMiwiU2lnbmF0dXJlIEZhY2lhbCIsIjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDBlMiJdLCJmIjoiZTNiMGM0NDIifQ"}}}}}},"400":{"$ref":"#/components/responses/InvalidCursor"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/v1/services/{id}/eligible-staff":{"get":{"tags":["Services"],"summary":"Eligible staff for a service","operationId":"listEligibleStaff","x-pamprr-required-scope":"services:read","security":[{"bearerAuth":["services:read"]}],"description":"Who can perform a service, and at which branches. The members assigned to the service who are bookable, each with locationIds narrowed to the visible branches where they can perform it (their branch memberships intersected with the branches offering the service), from the same rules the availability search applies. Every member listed can take a booking for this service at each listed branch, subject to hours and existing bookings; the availability search is the next call. A listed member need not have any free slot. The whole set, not paginated.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"The service id from the services list. A service that is not yours, is deleted, is inactive or is offered at no branch customers can see is 404 NOT_FOUND.","example":"00000000-0000-4000-8000-0000000000e2"},{"name":"locationId","in":"query","required":false,"schema":{"type":"string","format":"uuid"},"description":"Only the members who can perform the service at this branch. A branch that is not one of your visible branches offering this service is refused with 400 INVALID_LOCATION; a value that is not a UUID with 400 VALIDATION_FAILED.","example":"00000000-0000-4000-8000-000000000c01"}],"responses":{"200":{"description":"The service, the branch asked for (or null) and the eligible members","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EligibleStaff"},"examples":{"eligible":{"summary":"Every bookable member assigned to the service, with the branches each can perform it at","value":{"serviceId":"00000000-0000-4000-8000-0000000000e2","serviceName":"Signature Facial","locationId":null,"staff":[{"id":"00000000-0000-4000-8000-0000000000a1","name":"Maya Okafor","initials":"MO","colour":"#6B8E4E","jobTitle":"Senior therapist","profileImageUrl":"https://example.test/images/maya.jpg","isBookable":true,"acceptsOnlineBookings":true,"deliveryMode":"both","locationIds":["00000000-0000-4000-8000-000000000c01","00000000-0000-4000-8000-000000000c02"]},{"id":"00000000-0000-4000-8000-0000000000a2","name":"Theo Brandt","initials":"TB","colour":"#C58B3F","jobTitle":"Facialist","profileImageUrl":null,"isBookable":true,"acceptsOnlineBookings":false,"deliveryMode":"fixed","locationIds":["00000000-0000-4000-8000-000000000c01"]}]}}}}}},"400":{"description":"locationId is not a UUID (code VALIDATION_FAILED), or names a branch that is not one of your visible branches offering this service (code INVALID_LOCATION)","x-pamprr-variants":[{"$ref":"#/components/responses/BadRequest"},{"$ref":"#/components/responses/InvalidLocation"}],"headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/v1/locations":{"get":{"tags":["Locations"],"summary":"List locations","operationId":"listLocations","x-pamprr-required-scope":"locations:read","security":[{"bearerAuth":["locations:read"]}],"description":"The business's branches, live ones only, the primary first. A branch the business has hidden from customers is listed with isVisible false: it cannot take a booking through the API or the online calendar, but the business still sees it. Every location carries timezone; every business runs on Europe/London today.","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"Rows per page, 1 to 100 (a value above 100 is treated as 100).","example":50},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"Opaque keyset cursor from the previous page's nextCursor. Store and pass it back unchanged with the same filters; absent on the first page. A cursor never expires; one from another list, or from different filters, is refused with 400 INVALID_CURSOR.","example":"eyJ2IjoyLCJsIjoibG9jYXRpb25zIiwiayI6WyIyMDI2LTAzLTAyVDA5OjAwOjAwLjAwMFoiLCIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDBjMDIiXSwiZiI6ImUzYjBjNDQyIn0"}],"responses":{"200":{"description":"List of locations scoped to the calling business","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"type":"object","properties":{"locations":{"type":"array","items":{"$ref":"#/components/schemas/Location"}},"nextCursor":{"type":["string","null"],"description":"Pass back as cursor to fetch the next page; null when this page was the last."}},"required":["locations","nextCursor"]},"examples":{"branches":{"summary":"Three branches, the primary first, one hidden from customers","value":{"locations":[{"id":"00000000-0000-4000-8000-000000000c01","name":"Example Studio, Soho","address":"12 Example Street","city":"London","postcode":"EX1 1EX","phone":"+44 20 7946 0100","email":"soho@example.test","website":"https://example.test","locationModel":"both","isPrimary":true,"isVisible":true,"timezone":"Europe/London","createdAt":"2026-03-02T09:00:00.000Z"},{"id":"00000000-0000-4000-8000-000000000c02","name":"Example Studio, Islington","address":"4 Sample Road","city":"London","postcode":"EX2 2EX","phone":"+44 20 7946 0101","email":null,"website":null,"locationModel":"fixed","isPrimary":false,"isVisible":true,"timezone":"Europe/London","createdAt":"2026-05-14T11:20:00.000Z"},{"id":"00000000-0000-4000-8000-000000000c03","name":"Example Studio, Pop up","address":null,"city":null,"postcode":null,"phone":null,"email":null,"website":null,"locationModel":null,"isPrimary":false,"isVisible":false,"timezone":"Europe/London","createdAt":"2026-08-01T08:00:00.000Z"}],"nextCursor":null}}}}}},"400":{"$ref":"#/components/responses/InvalidCursor"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/v1/staff":{"get":{"tags":["Staff"],"summary":"List staff","operationId":"listStaff","x-pamprr-required-scope":"staff:read","security":[{"bearerAuth":["staff:read"]}],"description":"The roster's partner projection. The staff id a booking needs, the name, initials, colour, job title, photo, the bookable flags, the delivery mode, the home branch, the branches each member works at and the services each performs. Never contact or demographic fields. Filter by locationId for the members of one branch.","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"Rows per page, 1 to 100 (a value above 100 is treated as 100).","example":50},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"Opaque keyset cursor from the previous page's nextCursor. Store and pass it back unchanged with the same filters; absent on the first page. A cursor never expires; one from another list, or from different filters, is refused with 400 INVALID_CURSOR.","example":"eyJ2IjoyLCJsIjoic3RhZmYiLCJrIjpbIlRoZW8iLCJCcmFuZHQiLCIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwYTIiXSwiZiI6ImUzYjBjNDQyIn0"},{"name":"locationId","in":"query","required":false,"schema":{"type":"string","format":"uuid"},"description":"Only the members of this branch (a member works at every branch listed in locationIds). A branch that is not one of your business's live branches is refused with 400 INVALID_LOCATION; a value that is not a UUID with 400 VALIDATION_FAILED.","example":"00000000-0000-4000-8000-000000000c01"}],"responses":{"200":{"description":"List of staff scoped to the calling business","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"type":"object","properties":{"staff":{"type":"array","items":{"$ref":"#/components/schemas/Staff"}},"nextCursor":{"type":["string","null"],"description":"Pass back as cursor to fetch the next page; null when this page was the last."}},"required":["staff","nextCursor"]},"examples":{"roster":{"summary":"The members of one branch","value":{"staff":[{"id":"00000000-0000-4000-8000-0000000000a1","name":"Maya Okafor","initials":"MO","colour":"#6B8E4E","jobTitle":"Senior therapist","profileImageUrl":"https://example.test/images/maya.jpg","isBookable":true,"acceptsOnlineBookings":true,"deliveryMode":"both","homeLocationId":"00000000-0000-4000-8000-000000000c01","locationIds":["00000000-0000-4000-8000-000000000c01","00000000-0000-4000-8000-000000000c02"],"serviceIds":["00000000-0000-4000-8000-0000000000e1","00000000-0000-4000-8000-0000000000e2"],"createdAt":"2026-03-02T09:05:00.000Z"},{"id":"00000000-0000-4000-8000-0000000000a2","name":"Theo Brandt","initials":"TB","colour":"#C58B3F","jobTitle":"Facialist","profileImageUrl":null,"isBookable":true,"acceptsOnlineBookings":false,"deliveryMode":"fixed","homeLocationId":"00000000-0000-4000-8000-000000000c01","locationIds":["00000000-0000-4000-8000-000000000c01"],"serviceIds":["00000000-0000-4000-8000-0000000000e2"],"createdAt":"2026-06-10T10:00:00.000Z"}],"nextCursor":null}}}}}},"400":{"description":"locationId is not a UUID (code VALIDATION_FAILED), names a branch that is not one of your business's live branches (code INVALID_LOCATION), or the cursor does not decode, names another list or was issued under other filters (code INVALID_CURSOR)","x-pamprr-variants":[{"$ref":"#/components/responses/BadRequest"},{"$ref":"#/components/responses/InvalidLocation"},{"$ref":"#/components/responses/InvalidCursor"}],"headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/v1/availability":{"get":{"tags":["Availability"],"summary":"Availability search","operationId":"searchAvailability","x-pamprr-required-scope":"availability:read","security":[{"bearerAuth":["availability:read"]}],"description":"The bookable slots for a service, computed live from the same rules the business's own online booking calendar uses. Those rules, applied at the moment of the request: opening hours and closures, each staff member's schedule, existing bookings, customers mid checkout (their held slot reads as taken and frees itself within ten minutes if they abandon checkout), time blocks and travel time. Three modes: date for one day of slots; from with days (1 to 31) for a per day status; from with days (1 to 7) and detail=slots for the slots per day. Slots start on fifteen minute boundaries from the branch's opening time; today's slots start at least thirty minutes ahead. The answer is advisory: a booking or a checkout after it can take a slot, and the create booking call is the truth (409 BOOKING_OVERLAP when the slot is gone, 409 SLOT_HELD while a customer holds it). Every time is UTC; every business runs on Europe/London today and the response names the zone. Never cached.","parameters":[{"name":"serviceId","in":"query","required":true,"schema":{"type":"string","format":"uuid"},"description":"The service. Must be active and offered at a branch customers can see; otherwise 404 NOT_FOUND.","example":"00000000-0000-4000-8000-0000000000e2"},{"name":"date","in":"query","required":false,"schema":{"type":"string","format":"date"},"description":"One day of slots (YYYY-MM-DD). Send date or from, never both.","example":"2026-10-06"},{"name":"from","in":"query","required":false,"schema":{"type":"string","format":"date"},"description":"The first day of a window (YYYY-MM-DD).","example":"2026-10-05"},{"name":"days","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":31},"description":"With from: the window's length. 1 to 31 for the summary (default 14); 1 to 7 with detail=slots (default 7).","example":7},{"name":"detail","in":"query","required":false,"schema":{"type":"string","enum":["summary","slots"]},"description":"With from: summary (a status per day, the default) or slots (the slots per day, days capped at 7).","example":"slots"},{"name":"staffProfileId","in":"query","required":false,"schema":{"type":"string","format":"uuid"},"description":"Only this staff member's slots. Must be one of your staff; otherwise 404 NOT_FOUND.","example":"00000000-0000-4000-8000-0000000000a2"},{"name":"locationId","in":"query","required":false,"schema":{"type":"string","format":"uuid"},"description":"Only this branch's slots. Must be a branch customers can see that offers the service; otherwise 400 INVALID_LOCATION.","example":"00000000-0000-4000-8000-000000000c01"},{"name":"deliveryMode","in":"query","required":false,"schema":{"type":"string","enum":["fixed","mobile"]},"description":"Restrict to staff who work at the branch (fixed) or travel to the customer (mobile); absent means either. Mobile applies the business's travel buffer between visits.","example":"fixed"}],"responses":{"200":{"description":"The slots for a day, the status per day, or the slots per day, by mode","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/AvailabilityDay"},{"$ref":"#/components/schemas/AvailabilitySummary"},{"$ref":"#/components/schemas/AvailabilityWindow"}]},"examples":{"day":{"summary":"date: one day of slots","value":{"date":"2026-10-06","timezone":"Europe/London","serviceId":"00000000-0000-4000-8000-0000000000e2","slotMinutes":15,"durationMinutes":60,"slots":[{"startTime":"2026-10-06T09:15:00.000Z","endTime":"2026-10-06T10:15:00.000Z","localTime":"10:15","staff":[{"id":"00000000-0000-4000-8000-0000000000a1","name":"Maya Okafor","locationId":"00000000-0000-4000-8000-000000000c01"}]},{"startTime":"2026-10-06T13:30:00.000Z","endTime":"2026-10-06T14:30:00.000Z","localTime":"14:30","staff":[{"id":"00000000-0000-4000-8000-0000000000a1","name":"Maya Okafor","locationId":"00000000-0000-4000-8000-000000000c02"},{"id":"00000000-0000-4000-8000-0000000000a2","name":"Theo Brandt","locationId":"00000000-0000-4000-8000-000000000c01"}]}],"message":null}},"summary":{"summary":"from and days: a status per day","value":{"from":"2026-10-05","timezone":"Europe/London","serviceId":"00000000-0000-4000-8000-0000000000e2","days":[{"date":"2026-10-05","status":"fully_booked"},{"date":"2026-10-06","status":"available"},{"date":"2026-10-07","status":"available"},{"date":"2026-10-08","status":"available"},{"date":"2026-10-09","status":"available"},{"date":"2026-10-10","status":"available"},{"date":"2026-10-11","status":"closed"}],"message":null}},"window":{"summary":"from, days and detail=slots for one therapist: the slots per day","value":{"from":"2026-10-05","timezone":"Europe/London","serviceId":"00000000-0000-4000-8000-0000000000e2","slotMinutes":15,"durationMinutes":60,"days":[{"date":"2026-10-05","status":"fully_booked","slots":[]},{"date":"2026-10-06","status":"available","slots":[{"startTime":"2026-10-06T13:30:00.000Z","endTime":"2026-10-06T14:30:00.000Z","localTime":"14:30","staff":[{"id":"00000000-0000-4000-8000-0000000000a2","name":"Theo Brandt","locationId":"00000000-0000-4000-8000-000000000c01"}]}]},{"date":"2026-10-07","status":"available","slots":[{"startTime":"2026-10-07T09:00:00.000Z","endTime":"2026-10-07T10:00:00.000Z","localTime":"10:00","staff":[{"id":"00000000-0000-4000-8000-0000000000a2","name":"Theo Brandt","locationId":"00000000-0000-4000-8000-000000000c01"}]}]},{"date":"2026-10-08","status":"fully_booked","slots":[]},{"date":"2026-10-09","status":"available","slots":[{"startTime":"2026-10-09T10:15:00.000Z","endTime":"2026-10-09T11:15:00.000Z","localTime":"11:15","staff":[{"id":"00000000-0000-4000-8000-0000000000a2","name":"Theo Brandt","locationId":"00000000-0000-4000-8000-000000000c01"}]}]},{"date":"2026-10-10","status":"fully_booked","slots":[]},{"date":"2026-10-11","status":"closed","slots":[]}]}}}}}},"400":{"description":"A parameter fails its rule: serviceId missing, date and from together or neither, a malformed or impossible date, days out of range, detail=slots with days over 7, or a non UUID id (code VALIDATION_FAILED); or locationId names a branch that is not a visible branch offering the service (code INVALID_LOCATION)","x-pamprr-variants":[{"$ref":"#/components/responses/BadRequest"},{"$ref":"#/components/responses/InvalidLocation"}],"headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/v1/webhooks":{"get":{"tags":["Webhooks"],"summary":"List webhook endpoints","operationId":"listWebhookEndpoints","x-pamprr-required-scope":"webhooks:manage","security":[{"bearerAuth":["webhooks:manage"]}],"description":"The business's live webhook endpoints, in creation order, the whole set (at most ten; no cursor). Endpoints belong to the business: every key carrying webhooks:manage sees every endpoint, whichever key or dashboard user created it. A deleted endpoint is not listed.","responses":{"200":{"description":"The endpoints","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"type":"object","properties":{"endpoints":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEndpoint"}}},"required":["endpoints"]},"examples":{"endpoints":{"summary":"Two endpoints, one enabled and one disabled after three days of failures","value":{"endpoints":[{"id":"00000000-0000-4000-8000-00000000e001","url":"https://partner.example/pamprr/hooks","description":"Production till","events":["booking.created","booking.cancelled","booking.rescheduled"],"status":"enabled","disabledReason":null,"payloadVersion":"1","createdAt":"2026-09-25T09:00:00.000Z","updatedAt":"2026-09-25T09:00:00.000Z","lastDeliveryAt":"2026-09-25T10:20:00.000Z","lastSuccessAt":"2026-09-25T10:20:00.000Z","lastFailureAt":null,"secretRotatedAt":null},{"id":"00000000-0000-4000-8000-00000000e002","url":"https://reporting.example/integrations/pamprr/webhooks/events","description":"Reporting mirror","events":["*"],"status":"disabled","disabledReason":"failing","payloadVersion":"1","createdAt":"2026-09-25T09:00:00.000Z","updatedAt":"2026-09-25T09:00:00.000Z","lastDeliveryAt":"2026-09-25T10:18:00.000Z","lastSuccessAt":"2026-09-22T08:00:00.000Z","lastFailureAt":"2026-09-25T10:18:00.000Z","secretRotatedAt":null}]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"tags":["Webhooks"],"summary":"Create a webhook endpoint","operationId":"createWebhookEndpoint","x-pamprr-required-scope":"webhooks:manage","security":[{"bearerAuth":["webhooks:manage"]}],"description":"Registers an https endpoint for signed deliveries and returns its signing secret ONCE. The URL must use https, carry no credentials or fragment, and resolve to a public address (pamprr's own hosts and private networks are refused); the events filter is [\"*\"] for the whole catalogue or a non empty list of event names; the payload version is pinned at creation (\"1\" today). A business may hold ten live endpoints. Store the secret in your server's environment: it signs every delivery and is never shown again (rotate it if it is lost). Send an Idempotency-Key so a retried request replays the first 201 with the same secret rather than creating a second endpoint.","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","minLength":1,"maxLength":255},"description":"Optional but recommended: the same rules as on create booking. For 24 hours the same key with the same request replays the first response, secret included, so a retried create never mints two endpoints; the same key with a different request is refused with 422 IDEMPOTENCY_KEY_REUSED.","example":"2c6f1a3e-5b7d-4e9f-8a1b-3c5d7e9f1a2b"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWebhookEndpointRequest"},"examples":{"filtered":{"summary":"Three booking events to a production endpoint","value":{"url":"https://partner.example/pamprr/hooks","events":["booking.created","booking.cancelled","booking.rescheduled"],"description":"Production till"}},"everything":{"summary":"The whole catalogue","value":{"url":"https://reporting.example/integrations/pamprr/webhooks/events","events":["*"]}}}}}},"responses":{"201":{"description":"Endpoint created; the secret appears in this response only","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"Idempotent-Replayed":{"$ref":"#/components/headers/Idempotent-Replayed"},"X-Original-Request-Id":{"$ref":"#/components/headers/X-Original-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"type":"object","properties":{"endpoint":{"$ref":"#/components/schemas/WebhookEndpoint"},"secret":{"type":"string","pattern":"^whsec_[A-Za-z0-9+/]{32}$","description":"The signing secret: whsec_ plus the base64 of 24 key bytes. Shown once."}},"required":["endpoint","secret"]},"examples":{"created":{"summary":"A new endpoint and its one time secret","value":{"endpoint":{"id":"00000000-0000-4000-8000-00000000e001","url":"https://partner.example/pamprr/hooks","description":"Production till","events":["booking.created","booking.cancelled","booking.rescheduled"],"status":"enabled","disabledReason":null,"payloadVersion":"1","createdAt":"2026-09-25T09:00:00.000Z","updatedAt":"2026-09-25T09:00:00.000Z","lastDeliveryAt":null,"lastSuccessAt":null,"lastFailureAt":null,"secretRotatedAt":null},"secret":"whsec_MfKQ9r8GtPB5J2zPbmgs1kuT1Xs6KXFZ"}}}}}},"400":{"description":"The request is malformed: the body is not JSON (code INVALID_JSON), the url does not use https or resolve to a public address, the events list is empty or names an unknown event, the description is over 200 characters or the payloadVersion is unknown (code VALIDATION_FAILED, with field naming it), or the Idempotency-Key is malformed (code IDEMPOTENCY_KEY_INVALID)","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"The business already has ten webhook endpoints (code WEBHOOK_LIMIT_REACHED); or a retry while the first request carrying this Idempotency-Key is still being processed (code IDEMPOTENCY_REQUEST_IN_PROGRESS)","x-pamprr-variants":[{"$ref":"#/components/responses/WebhookLimitReached"},{"$ref":"#/components/responses/Conflict"}],"headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"$ref":"#/components/responses/IdempotencyKeyReused"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/v1/webhooks/{id}":{"get":{"tags":["Webhooks"],"summary":"Get a webhook endpoint","operationId":"getWebhookEndpoint","x-pamprr-required-scope":"webhooks:manage","security":[{"bearerAuth":["webhooks:manage"]}],"description":"One endpoint by id, the same projection as a list row: never the secret. An endpoint that is not yours, a deleted endpoint, or an id that is not a UUID is 404 NOT_FOUND.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"The endpoint id.","example":"00000000-0000-4000-8000-00000000e001"}],"responses":{"200":{"description":"The endpoint","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"type":"object","properties":{"endpoint":{"$ref":"#/components/schemas/WebhookEndpoint"}},"required":["endpoint"]},"examples":{"endpoint":{"summary":"One endpoint by id","value":{"endpoint":{"id":"00000000-0000-4000-8000-00000000e001","url":"https://partner.example/pamprr/hooks","description":"Production till","events":["booking.created","booking.cancelled","booking.rescheduled"],"status":"enabled","disabledReason":null,"payloadVersion":"1","createdAt":"2026-09-25T09:00:00.000Z","updatedAt":"2026-09-25T09:00:00.000Z","lastDeliveryAt":"2026-09-25T10:20:00.000Z","lastSuccessAt":"2026-09-25T10:20:00.000Z","lastFailureAt":null,"secretRotatedAt":null}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}},"patch":{"tags":["Webhooks"],"summary":"Update a webhook endpoint","operationId":"updateWebhookEndpoint","x-pamprr-required-scope":"webhooks:manage","security":[{"bearerAuth":["webhooks:manage"]}],"description":"Changes any of the url (re checked against the same rules as on create), the events filter, the description, the payloadVersion, or enabled. Setting enabled false disables the endpoint with reason manual and cancels its pending deliveries: nothing is delivered while an endpoint is disabled, and an event that occurs during the disabled period is not delivered on re enabling (poll the lists with updatedSince to catch up, and replay from the delivery log). Setting enabled true clears the reason and the failure clock. Send an Idempotency-Key so a retried request replays the first outcome.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"The endpoint id.","example":"00000000-0000-4000-8000-00000000e001"},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","minLength":1,"maxLength":255},"description":"Optional: the same rules as on create booking; a retried update replays the first outcome.","example":"8d1e2f3a-4b5c-4d6e-9f7a-8b9c0d1e2f3a"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWebhookEndpointRequest"},"examples":{"widen":{"summary":"Subscribe to everything","value":{"events":["*"]}},"disable":{"summary":"Stop deliveries for now","value":{"enabled":false}}}}}},"responses":{"200":{"description":"The endpoint as it now stands","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"Idempotent-Replayed":{"$ref":"#/components/headers/Idempotent-Replayed"},"X-Original-Request-Id":{"$ref":"#/components/headers/X-Original-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"type":"object","properties":{"endpoint":{"$ref":"#/components/schemas/WebhookEndpoint"}},"required":["endpoint"]},"examples":{"updated":{"summary":"The filter widened","value":{"endpoint":{"id":"00000000-0000-4000-8000-00000000e001","url":"https://partner.example/pamprr/hooks","description":"Production till","events":["*"],"status":"enabled","disabledReason":null,"payloadVersion":"1","createdAt":"2026-09-25T09:00:00.000Z","updatedAt":"2026-09-25T11:00:00.000Z","lastDeliveryAt":"2026-09-25T10:20:00.000Z","lastSuccessAt":"2026-09-25T10:20:00.000Z","lastFailureAt":null,"secretRotatedAt":null}}},"disabled":{"summary":"Disabled by hand; deliveries stop until it is enabled again","value":{"endpoint":{"id":"00000000-0000-4000-8000-00000000e001","url":"https://partner.example/pamprr/hooks","description":"Production till","events":["booking.created","booking.cancelled","booking.rescheduled"],"status":"disabled","disabledReason":"manual","payloadVersion":"1","createdAt":"2026-09-25T09:00:00.000Z","updatedAt":"2026-09-25T11:05:00.000Z","lastDeliveryAt":"2026-09-25T10:20:00.000Z","lastSuccessAt":"2026-09-25T10:20:00.000Z","lastFailureAt":null,"secretRotatedAt":null}}}}}}},"400":{"description":"The request is malformed: the body is not JSON or sends none of the five fields (code INVALID_JSON or VALIDATION_FAILED), a field fails its rule (code VALIDATION_FAILED, with field naming it), or the Idempotency-Key is malformed (code IDEMPOTENCY_KEY_INVALID)","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"A retry while the first request carrying this Idempotency-Key is still being processed (code IDEMPOTENCY_REQUEST_IN_PROGRESS)","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"$ref":"#/components/responses/IdempotencyKeyReused"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"tags":["Webhooks"],"summary":"Delete a webhook endpoint","operationId":"deleteWebhookEndpoint","x-pamprr-required-scope":"webhooks:manage","security":[{"bearerAuth":["webhooks:manage"]}],"description":"Stops every delivery to the endpoint: pending deliveries are cancelled, the delivery log is kept for thirty days, and the slot under the cap of ten is freed. A second delete of the same id is 404 NOT_FOUND. Send an Idempotency-Key so a retried request replays the first outcome.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"The endpoint id.","example":"00000000-0000-4000-8000-00000000e001"},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","minLength":1,"maxLength":255},"description":"Optional: a retried delete replays the first outcome.","example":"1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d"}],"responses":{"200":{"description":"Deleted, with the number of pending deliveries cancelled","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"Idempotent-Replayed":{"$ref":"#/components/headers/Idempotent-Replayed"},"X-Original-Request-Id":{"$ref":"#/components/headers/X-Original-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean","enum":[true]},"cancelledDeliveries":{"type":"integer","minimum":0,"description":"Pending deliveries cancelled by this delete."}},"required":["deleted","cancelledDeliveries"]},"examples":{"deleted":{"summary":"Deleted with two deliveries cancelled","value":{"deleted":true,"cancelledDeliveries":2}}}}}},"400":{"description":"The Idempotency-Key is malformed (code IDEMPOTENCY_KEY_INVALID)","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"A retry while the first request carrying this Idempotency-Key is still being processed (code IDEMPOTENCY_REQUEST_IN_PROGRESS)","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"$ref":"#/components/responses/IdempotencyKeyReused"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/v1/webhooks/{id}/rotate-secret":{"post":{"tags":["Webhooks"],"summary":"Rotate a webhook secret","operationId":"rotateWebhookSecret","x-pamprr-required-scope":"webhooks:manage","security":[{"bearerAuth":["webhooks:manage"]}],"description":"Mints a new signing secret and returns it ONCE. The old secret keeps signing for twenty four hours: every delivery in that window carries two signatures (the new first, then the old, space separated in webhook-signature), so deploy the new secret at leisure and verify with either. A second rotation inside the window discards the older previous secret; only two are ever live. A rotation cannot be undone. Send an Idempotency-Key so a retried request replays the first outcome with the same secret.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"The endpoint id.","example":"00000000-0000-4000-8000-00000000e001"},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","minLength":1,"maxLength":255},"description":"Optional but recommended: a retried rotation replays the first outcome, secret included, rather than rotating twice.","example":"3e4f5a6b-7c8d-4e9f-8a0b-1c2d3e4f5a6b"}],"responses":{"200":{"description":"The new secret, shown once, and when the old one stops signing","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"Idempotent-Replayed":{"$ref":"#/components/headers/Idempotent-Replayed"},"X-Original-Request-Id":{"$ref":"#/components/headers/X-Original-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"type":"object","properties":{"endpoint":{"$ref":"#/components/schemas/WebhookEndpoint"},"secret":{"type":"string","pattern":"^whsec_[A-Za-z0-9+/]{32}$","description":"The new signing secret. Shown once."},"previousSecretExpiresAt":{"type":"string","format":"date-time","description":"When the previous secret stops signing: twenty four hours from the rotation."}},"required":["endpoint","secret","previousSecretExpiresAt"]},"examples":{"rotated":{"summary":"The replacement secret and the end of the dual signing window","value":{"endpoint":{"id":"00000000-0000-4000-8000-00000000e001","url":"https://partner.example/pamprr/hooks","description":"Production till","events":["booking.created","booking.cancelled","booking.rescheduled"],"status":"enabled","disabledReason":null,"payloadVersion":"1","createdAt":"2026-09-25T09:00:00.000Z","updatedAt":"2026-09-25T11:10:00.000Z","lastDeliveryAt":"2026-09-25T10:20:00.000Z","lastSuccessAt":"2026-09-25T10:20:00.000Z","lastFailureAt":null,"secretRotatedAt":"2026-09-25T11:10:00.000Z"},"secret":"whsec_pQ7bY1c2d3e4f5g6h7i8j9k0l1m2n3o4","previousSecretExpiresAt":"2026-09-26T11:10:00.000Z"}}}}}},"400":{"description":"The Idempotency-Key is malformed (code IDEMPOTENCY_KEY_INVALID)","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"A retry while the first request carrying this Idempotency-Key is still being processed (code IDEMPOTENCY_REQUEST_IN_PROGRESS)","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"$ref":"#/components/responses/IdempotencyKeyReused"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/v1/webhooks/{id}/test":{"post":{"tags":["Webhooks"],"summary":"Send a test delivery","operationId":"testWebhookEndpoint","x-pamprr-required-scope":"webhooks:manage","security":[{"bearerAuth":["webhooks:manage"]}],"description":"Sends a ping event to the endpoint at once, signed like any delivery, and answers 200 whether the endpoint accepted it or not: the outcome is data, not an error. The ping is delivered regardless of the events filter and is never retried; its delivery appears in the log. A disabled endpoint is refused with 409 WEBHOOK_ENDPOINT_DISABLED. Send an Idempotency-Key so a retried request replays the first outcome rather than pinging twice.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"The endpoint id.","example":"00000000-0000-4000-8000-00000000e001"},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","minLength":1,"maxLength":255},"description":"Optional: a retried test replays the first outcome rather than pinging twice.","example":"5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f"}],"responses":{"200":{"description":"The ping's delivery and its one attempt, whatever the outcome","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"Idempotent-Replayed":{"$ref":"#/components/headers/Idempotent-Replayed"},"X-Original-Request-Id":{"$ref":"#/components/headers/X-Original-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"type":"object","properties":{"delivery":{"$ref":"#/components/schemas/WebhookDelivery"},"attempt":{"$ref":"#/components/schemas/WebhookDeliveryAttempt"}},"required":["delivery","attempt"]},"examples":{"delivered":{"summary":"The endpoint answered 200 in 312 ms","value":{"delivery":{"id":"00000000-0000-4000-8000-00000000d003","eventId":"evt_00c0ffee00c0ffee00c0ffee","eventType":"ping","occurredAt":"2026-09-25T10:30:00.000Z","status":"delivered","attempts":0,"nextAttemptAt":null,"lastStatusCode":200,"lastError":null,"deliveredAt":"2026-09-25T10:30:00.000Z","failedAt":null,"createdAt":"2026-09-25T10:30:00.000Z","attemptLog":[{"attempt":1,"kind":"test","requestedAt":"2026-09-25T10:30:00.000Z","durationMs":312,"statusCode":200,"error":null,"responseExcerpt":"{\"received\":true}"}]},"attempt":{"attempt":1,"kind":"test","requestedAt":"2026-09-25T10:30:00.000Z","durationMs":312,"statusCode":200,"error":null,"responseExcerpt":"{\"received\":true}"}}},"failed":{"summary":"The endpoint answered 503; the ping is not retried","value":{"delivery":{"id":"00000000-0000-4000-8000-00000000d004","eventId":"evt_00c0ffee00c0ffee00c0ffee","eventType":"ping","occurredAt":"2026-09-25T10:30:00.000Z","status":"failed","attempts":0,"nextAttemptAt":null,"lastStatusCode":503,"lastError":"http_503","deliveredAt":null,"failedAt":"2026-09-25T10:30:01.000Z","createdAt":"2026-09-25T10:30:00.000Z","attemptLog":[{"attempt":1,"kind":"test","requestedAt":"2026-09-25T10:30:00.000Z","durationMs":1204,"statusCode":503,"error":"http_503","responseExcerpt":"{\"error\":\"Temporarily unavailable\"}"}]},"attempt":{"attempt":1,"kind":"test","requestedAt":"2026-09-25T10:30:00.000Z","durationMs":1204,"statusCode":503,"error":"http_503","responseExcerpt":"{\"error\":\"Temporarily unavailable\"}"}}}}}}},"400":{"description":"The Idempotency-Key is malformed (code IDEMPOTENCY_KEY_INVALID)","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The endpoint is disabled; enable it before sending a test (code WEBHOOK_ENDPOINT_DISABLED); or a retry while the first request carrying this Idempotency-Key is still being processed (code IDEMPOTENCY_REQUEST_IN_PROGRESS)","x-pamprr-variants":[{"$ref":"#/components/responses/WebhookEndpointDisabled"},{"$ref":"#/components/responses/Conflict"}],"headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"$ref":"#/components/responses/IdempotencyKeyReused"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/v1/webhooks/{id}/deliveries":{"get":{"tags":["Webhooks"],"summary":"List deliveries","operationId":"listWebhookDeliveries","x-pamprr-required-scope":"webhooks:manage","security":[{"bearerAuth":["webhooks:manage"]}],"description":"The endpoint's delivery log, newest first: each row carries its event's type and time, the delivery's state, the ladder position and every attempt with its status code, error class and a response excerpt of up to 1 KiB. The event's payload is not repeated (you received it, or replay the delivery). Filter by status, by event type, or by since (deliveries created at or after a moment). Deliveries are kept for thirty days.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"The endpoint id.","example":"00000000-0000-4000-8000-00000000e001"},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["pending","delivered","failed","cancelled"]},"description":"Only deliveries in this state. An unknown value is 400 VALIDATION_FAILED.","example":"failed"},{"name":"eventType","in":"query","required":false,"schema":{"$ref":"#/components/schemas/WebhookEventType"},"description":"Only deliveries of this event type. An unknown name is 400 VALIDATION_FAILED.","example":"booking.cancelled"},{"name":"since","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"Only deliveries created at or after this moment (ISO 8601). A value that does not parse is 400 VALIDATION_FAILED.","example":"2026-09-25T00:00:00Z"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"Rows per page, 1 to 100 (a value above 100 is treated as 100).","example":50},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"Opaque keyset cursor from the previous page's nextCursor. Store and pass it back unchanged with the same filters; absent on the first page. A cursor never expires; one from another list, or from different filters, is refused with 400 INVALID_CURSOR.","example":"eyJ2IjoyLCJsIjoid2ViaG9vay1kZWxpdmVyaWVzIiwiayI6WyIyMDI2LTA5LTI1VDEwOjE2OjAwLjAwMFoiLCIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMGQwMDEiXSwiZiI6ImUzYjBjNDQyIn0"}],"responses":{"200":{"description":"The log page, newest first","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"type":"object","properties":{"deliveries":{"type":"array","items":{"$ref":"#/components/schemas/WebhookDelivery"}},"nextCursor":{"type":["string","null"],"description":"Pass back as cursor to fetch the next (older) page; null when this page was the last."}},"required":["deliveries","nextCursor"]},"examples":{"log":{"summary":"A dead letter and a pending retry after a 429","value":{"deliveries":[{"id":"00000000-0000-4000-8000-00000000d002","eventId":"evt_9a1b2c3d4e5f60718293a4b5","eventType":"client.created","occurredAt":"2026-09-25T10:21:05.000Z","status":"pending","attempts":1,"nextAttemptAt":"2026-09-25T10:23:10.000Z","lastStatusCode":429,"lastError":"http_429","deliveredAt":null,"failedAt":null,"createdAt":"2026-09-25T10:22:00.000Z","attemptLog":[{"attempt":1,"kind":"scheduled","requestedAt":"2026-09-25T10:22:10.000Z","durationMs":92,"statusCode":429,"error":"http_429","responseExcerpt":"slow down"}]},{"id":"00000000-0000-4000-8000-00000000d001","eventId":"evt_3f2c9a7d1e4b8c0a5d6f7e8b","eventType":"booking.cancelled","occurredAt":"2026-09-25T10:15:32.000Z","status":"failed","attempts":8,"nextAttemptAt":null,"lastStatusCode":503,"lastError":"http_503","deliveredAt":null,"failedAt":"2026-09-27T07:51:32.000Z","createdAt":"2026-09-25T10:16:00.000Z","attemptLog":[{"attempt":1,"kind":"scheduled","requestedAt":"2026-09-25T10:18:00.000Z","durationMs":10004,"statusCode":null,"error":"timeout","responseExcerpt":null},{"attempt":2,"kind":"scheduled","requestedAt":"2026-09-25T10:19:00.000Z","durationMs":1204,"statusCode":503,"error":"http_503","responseExcerpt":"{\"error\":\"Temporarily unavailable\"}"}]}],"nextCursor":null}}}}}},"400":{"description":"The status is not one of pending, delivered, failed or cancelled, the eventType is not a catalogue name, or since is not an ISO 8601 timestamp (code VALIDATION_FAILED, with field naming it); or the cursor does not decode, belongs to another list or was issued under different filters (code INVALID_CURSOR)","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/v1/webhooks/{id}/deliveries/{deliveryId}/replay":{"post":{"tags":["Webhooks"],"summary":"Replay a delivery","operationId":"replayWebhookDelivery","x-pamprr-required-scope":"webhooks:manage","security":[{"bearerAuth":["webhooks:manage"]}],"description":"Attempts a delivered, failed or cancelled delivery once more, at once and outside the retry ladder: the same event bytes under the same webhook-id, with a fresh timestamp and signature. A 2xx marks the delivery delivered; anything else records the attempt and leaves the status as it was. A pending delivery is refused with 409 WEBHOOK_DELIVERY_PENDING (it will be attempted on schedule); a disabled endpoint with 409 WEBHOOK_ENDPOINT_DISABLED. The partner's recovery from a dead letter. Send an Idempotency-Key so a retried request replays the first outcome.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"The endpoint id.","example":"00000000-0000-4000-8000-00000000e001"},{"name":"deliveryId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"The delivery id, from the delivery log.","example":"00000000-0000-4000-8000-00000000d001"},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","minLength":1,"maxLength":255},"description":"Optional: a retried replay replays the first outcome rather than attempting twice.","example":"7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d"}],"responses":{"200":{"description":"The delivery after the attempt, and the attempt","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"Idempotent-Replayed":{"$ref":"#/components/headers/Idempotent-Replayed"},"X-Original-Request-Id":{"$ref":"#/components/headers/X-Original-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"type":"object","properties":{"delivery":{"$ref":"#/components/schemas/WebhookDelivery"},"attempt":{"$ref":"#/components/schemas/WebhookDeliveryAttempt"}},"required":["delivery","attempt"]},"examples":{"replayed":{"summary":"The dead letter delivered on replay","value":{"delivery":{"id":"00000000-0000-4000-8000-00000000d001","eventId":"evt_3f2c9a7d1e4b8c0a5d6f7e8b","eventType":"booking.cancelled","occurredAt":"2026-09-25T10:15:32.000Z","status":"delivered","attempts":8,"nextAttemptAt":null,"lastStatusCode":200,"lastError":null,"deliveredAt":"2026-09-25T10:25:00.000Z","failedAt":"2026-09-27T07:51:32.000Z","createdAt":"2026-09-25T10:16:00.000Z","attemptLog":[{"attempt":1,"kind":"scheduled","requestedAt":"2026-09-25T10:18:00.000Z","durationMs":10004,"statusCode":null,"error":"timeout","responseExcerpt":null},{"attempt":2,"kind":"scheduled","requestedAt":"2026-09-25T10:19:00.000Z","durationMs":1204,"statusCode":503,"error":"http_503","responseExcerpt":"{\"error\":\"Temporarily unavailable\"}"},{"attempt":3,"kind":"replay","requestedAt":"2026-09-25T10:25:00.000Z","durationMs":188,"statusCode":200,"error":null,"responseExcerpt":"{\"received\":true}"}]},"attempt":{"attempt":3,"kind":"replay","requestedAt":"2026-09-25T10:25:00.000Z","durationMs":188,"statusCode":200,"error":null,"responseExcerpt":"{\"received\":true}"}}}}}}},"400":{"description":"The Idempotency-Key is malformed (code IDEMPOTENCY_KEY_INVALID)","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The delivery is still pending and will be attempted on schedule (code WEBHOOK_DELIVERY_PENDING); the endpoint is disabled (code WEBHOOK_ENDPOINT_DISABLED); or a retry while the first request carrying this Idempotency-Key is still being processed (code IDEMPOTENCY_REQUEST_IN_PROGRESS)","x-pamprr-variants":[{"$ref":"#/components/responses/WebhookDeliveryPending"},{"$ref":"#/components/responses/WebhookEndpointDisabled"},{"$ref":"#/components/responses/Conflict"}],"headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"$ref":"#/components/responses/IdempotencyKeyReused"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}}},"webhooks":{"booking.created":{"post":{"summary":"A booking was created, from any source: the online calendar, the dashboard, the app or this API.","description":"data.booking is the booking as the API answers it, read when the event was picked up; source says where it came from.","parameters":[{"name":"webhook-id","in":"header","required":true,"description":"The event id; the same on every attempt and replay of this event, so a partner can deduplicate.","schema":{"type":"string","pattern":"^evt_[0-9a-f]{24}$"},"example":"evt_000000000000000000000000"},{"name":"webhook-timestamp","in":"header","required":true,"description":"Unix seconds when this attempt was signed; refuse a delivery whose timestamp is more than five minutes from now.","schema":{"type":"string","pattern":"^[0-9]{1,12}$"},"example":"1791275517"},{"name":"webhook-signature","in":"header","required":true,"description":"One or more signatures separated by spaces, each v1, then base64: HMAC SHA256 over id, a full stop, timestamp, a full stop, the raw body bytes, keyed by the base64 decoded secret after whsec_. Two during the day after a rotation. This example verifies the example body, serialised compactly, under the document's example secret.","schema":{"type":"string"},"example":"v1,fijqfIkuQspxb8qTRM+DL4uGS4XCPb7itFrmo3kV4H0="}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookEvent"},{"type":"object","properties":{"type":{"type":"string","enum":["booking.created"]},"data":{"$ref":"#/components/schemas/BookingEventData"}}}]},"examples":{"delivery":{"summary":"A booking.created delivery","value":{"id":"evt_000000000000000000000000","type":"booking.created","apiVersion":"v1","payloadVersion":"1","occurredAt":"2026-10-06T08:31:12.000Z","business":{"id":"00000000-0000-4000-8000-00000000b001"},"data":{"booking":{"id":"00000000-0000-4000-8000-0000000000b3","customerName":"Alex Example","customerEmail":"alex@example.test","customerPhone":"+44 7700 900123","serviceName":"Signature Facial","serviceDuration":60,"staffName":"Maya Okafor","status":"confirmed","source":"api","paymentStatus":"unpaid","startTime":"2026-10-06T13:30:00.000Z","endTime":"2026-10-06T14:30:00.000Z","priceInPence":6500,"notes":"Booked by the hotel concierge","createdAt":"2026-10-06T08:31:12.000Z","serviceId":"00000000-0000-4000-8000-0000000000e2","staffProfileId":"00000000-0000-4000-8000-0000000000a1","locationId":"00000000-0000-4000-8000-000000000c02","updatedAt":null,"cancelledAt":null}}}}}}}},"responses":{"2XX":{"description":"Acknowledged. Any 2xx within ten seconds marks the delivery delivered; answer first, then do the work. Anything else, a 3xx included, is retried by the ladder."}}}},"booking.confirmed":{"post":{"summary":"A pending booking was confirmed.","description":"data.booking with status confirmed.","parameters":[{"name":"webhook-id","in":"header","required":true,"description":"The event id; the same on every attempt and replay of this event, so a partner can deduplicate.","schema":{"type":"string","pattern":"^evt_[0-9a-f]{24}$"},"example":"evt_000000000000000000000001"},{"name":"webhook-timestamp","in":"header","required":true,"description":"Unix seconds when this attempt was signed; refuse a delivery whose timestamp is more than five minutes from now.","schema":{"type":"string","pattern":"^[0-9]{1,12}$"},"example":"1791275518"},{"name":"webhook-signature","in":"header","required":true,"description":"One or more signatures separated by spaces, each v1, then base64: HMAC SHA256 over id, a full stop, timestamp, a full stop, the raw body bytes, keyed by the base64 decoded secret after whsec_. Two during the day after a rotation. This example verifies the example body, serialised compactly, under the document's example secret.","schema":{"type":"string"},"example":"v1,DUofaHo8yWHms3XIEy7MjvrQvp9PXv6vFHu0znkdS3E="}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookEvent"},{"type":"object","properties":{"type":{"type":"string","enum":["booking.confirmed"]},"data":{"$ref":"#/components/schemas/BookingEventData"}}}]},"examples":{"delivery":{"summary":"A booking.confirmed delivery","value":{"id":"evt_000000000000000000000001","type":"booking.confirmed","apiVersion":"v1","payloadVersion":"1","occurredAt":"2026-10-06T08:31:12.000Z","business":{"id":"00000000-0000-4000-8000-00000000b001"},"data":{"booking":{"id":"00000000-0000-4000-8000-0000000000b3","customerName":"Alex Example","customerEmail":"alex@example.test","customerPhone":"+44 7700 900123","serviceName":"Signature Facial","serviceDuration":60,"staffName":"Maya Okafor","status":"confirmed","source":"api","paymentStatus":"unpaid","startTime":"2026-10-06T13:30:00.000Z","endTime":"2026-10-06T14:30:00.000Z","priceInPence":6500,"notes":"Booked by the hotel concierge","createdAt":"2026-10-06T08:31:12.000Z","serviceId":"00000000-0000-4000-8000-0000000000e2","staffProfileId":"00000000-0000-4000-8000-0000000000a1","locationId":"00000000-0000-4000-8000-000000000c02","updatedAt":"2026-10-06T09:00:00.000Z","cancelledAt":null}}}}}}}},"responses":{"2XX":{"description":"Acknowledged. Any 2xx within ten seconds marks the delivery delivered; answer first, then do the work. Anything else, a 3xx included, is retried by the ladder."}}}},"booking.started":{"post":{"summary":"A booking was marked in progress.","description":"data.booking with status in_progress.","parameters":[{"name":"webhook-id","in":"header","required":true,"description":"The event id; the same on every attempt and replay of this event, so a partner can deduplicate.","schema":{"type":"string","pattern":"^evt_[0-9a-f]{24}$"},"example":"evt_000000000000000000000002"},{"name":"webhook-timestamp","in":"header","required":true,"description":"Unix seconds when this attempt was signed; refuse a delivery whose timestamp is more than five minutes from now.","schema":{"type":"string","pattern":"^[0-9]{1,12}$"},"example":"1791275519"},{"name":"webhook-signature","in":"header","required":true,"description":"One or more signatures separated by spaces, each v1, then base64: HMAC SHA256 over id, a full stop, timestamp, a full stop, the raw body bytes, keyed by the base64 decoded secret after whsec_. Two during the day after a rotation. This example verifies the example body, serialised compactly, under the document's example secret.","schema":{"type":"string"},"example":"v1,H7SLBhSub1cChlvj4rNMQiAd4fcQzR6pRLvS5c5g97Y="}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookEvent"},{"type":"object","properties":{"type":{"type":"string","enum":["booking.started"]},"data":{"$ref":"#/components/schemas/BookingEventData"}}}]},"examples":{"delivery":{"summary":"A booking.started delivery","value":{"id":"evt_000000000000000000000002","type":"booking.started","apiVersion":"v1","payloadVersion":"1","occurredAt":"2026-10-06T08:31:12.000Z","business":{"id":"00000000-0000-4000-8000-00000000b001"},"data":{"booking":{"id":"00000000-0000-4000-8000-0000000000b3","customerName":"Alex Example","customerEmail":"alex@example.test","customerPhone":"+44 7700 900123","serviceName":"Signature Facial","serviceDuration":60,"staffName":"Maya Okafor","status":"in_progress","source":"api","paymentStatus":"unpaid","startTime":"2026-10-06T13:30:00.000Z","endTime":"2026-10-06T14:30:00.000Z","priceInPence":6500,"notes":"Booked by the hotel concierge","createdAt":"2026-10-06T08:31:12.000Z","serviceId":"00000000-0000-4000-8000-0000000000e2","staffProfileId":"00000000-0000-4000-8000-0000000000a1","locationId":"00000000-0000-4000-8000-000000000c02","updatedAt":"2026-10-06T13:31:00.000Z","cancelledAt":null}}}}}}}},"responses":{"2XX":{"description":"Acknowledged. Any 2xx within ten seconds marks the delivery delivered; answer first, then do the work. Anything else, a 3xx included, is retried by the ladder."}}}},"booking.completed":{"post":{"summary":"A booking was marked completed.","description":"data.booking with status completed; paymentStatus is the state at pick up.","parameters":[{"name":"webhook-id","in":"header","required":true,"description":"The event id; the same on every attempt and replay of this event, so a partner can deduplicate.","schema":{"type":"string","pattern":"^evt_[0-9a-f]{24}$"},"example":"evt_000000000000000000000003"},{"name":"webhook-timestamp","in":"header","required":true,"description":"Unix seconds when this attempt was signed; refuse a delivery whose timestamp is more than five minutes from now.","schema":{"type":"string","pattern":"^[0-9]{1,12}$"},"example":"1791275520"},{"name":"webhook-signature","in":"header","required":true,"description":"One or more signatures separated by spaces, each v1, then base64: HMAC SHA256 over id, a full stop, timestamp, a full stop, the raw body bytes, keyed by the base64 decoded secret after whsec_. Two during the day after a rotation. This example verifies the example body, serialised compactly, under the document's example secret.","schema":{"type":"string"},"example":"v1,kG32nopHwAGrgdMF/WaxAdwvKK/Irkh3qQZxcxlChl4="}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookEvent"},{"type":"object","properties":{"type":{"type":"string","enum":["booking.completed"]},"data":{"$ref":"#/components/schemas/BookingEventData"}}}]},"examples":{"delivery":{"summary":"A booking.completed delivery","value":{"id":"evt_000000000000000000000003","type":"booking.completed","apiVersion":"v1","payloadVersion":"1","occurredAt":"2026-10-06T08:31:12.000Z","business":{"id":"00000000-0000-4000-8000-00000000b001"},"data":{"booking":{"id":"00000000-0000-4000-8000-0000000000b3","customerName":"Alex Example","customerEmail":"alex@example.test","customerPhone":"+44 7700 900123","serviceName":"Signature Facial","serviceDuration":60,"staffName":"Maya Okafor","status":"completed","source":"api","paymentStatus":"paid","startTime":"2026-10-06T13:30:00.000Z","endTime":"2026-10-06T14:30:00.000Z","priceInPence":6500,"notes":"Booked by the hotel concierge","createdAt":"2026-10-06T08:31:12.000Z","serviceId":"00000000-0000-4000-8000-0000000000e2","staffProfileId":"00000000-0000-4000-8000-0000000000a1","locationId":"00000000-0000-4000-8000-000000000c02","updatedAt":"2026-10-06T14:32:00.000Z","cancelledAt":null}}}}}}}},"responses":{"2XX":{"description":"Acknowledged. Any 2xx within ten seconds marks the delivery delivered; answer first, then do the work. Anything else, a 3xx included, is retried by the ladder."}}}},"booking.cancelled":{"post":{"summary":"A booking was cancelled by the customer, the business or the system.","description":"data.booking with status cancelled and cancelledAt set, data.reason and data.cancelledBy.","parameters":[{"name":"webhook-id","in":"header","required":true,"description":"The event id; the same on every attempt and replay of this event, so a partner can deduplicate.","schema":{"type":"string","pattern":"^evt_[0-9a-f]{24}$"},"example":"evt_000000000000000000000004"},{"name":"webhook-timestamp","in":"header","required":true,"description":"Unix seconds when this attempt was signed; refuse a delivery whose timestamp is more than five minutes from now.","schema":{"type":"string","pattern":"^[0-9]{1,12}$"},"example":"1791275521"},{"name":"webhook-signature","in":"header","required":true,"description":"One or more signatures separated by spaces, each v1, then base64: HMAC SHA256 over id, a full stop, timestamp, a full stop, the raw body bytes, keyed by the base64 decoded secret after whsec_. Two during the day after a rotation. This example verifies the example body, serialised compactly, under the document's example secret.","schema":{"type":"string"},"example":"v1,mOc49yHvNBWZBCUJE3ELvtE3Qhya0muAgbOe6NbpY2k="}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookEvent"},{"type":"object","properties":{"type":{"type":"string","enum":["booking.cancelled"]},"data":{"$ref":"#/components/schemas/BookingCancelledEventData"}}}]},"examples":{"delivery":{"summary":"A booking.cancelled delivery","value":{"id":"evt_000000000000000000000004","type":"booking.cancelled","apiVersion":"v1","payloadVersion":"1","occurredAt":"2026-10-06T08:31:12.000Z","business":{"id":"00000000-0000-4000-8000-00000000b001"},"data":{"booking":{"id":"00000000-0000-4000-8000-0000000000b3","customerName":"Alex Example","customerEmail":"alex@example.test","customerPhone":"+44 7700 900123","serviceName":"Signature Facial","serviceDuration":60,"staffName":"Maya Okafor","status":"cancelled","source":"api","paymentStatus":"unpaid","startTime":"2026-10-06T13:30:00.000Z","endTime":"2026-10-06T14:30:00.000Z","priceInPence":6500,"notes":"Booked by the hotel concierge","createdAt":"2026-10-06T08:31:12.000Z","serviceId":"00000000-0000-4000-8000-0000000000e2","staffProfileId":"00000000-0000-4000-8000-0000000000a1","locationId":"00000000-0000-4000-8000-000000000c02","updatedAt":"2026-10-05T18:02:00.000Z","cancelledAt":"2026-10-05T18:02:00.000Z"},"reason":"Client unwell","cancelledBy":"consumer"}}}}}}},"responses":{"2XX":{"description":"Acknowledged. Any 2xx within ten seconds marks the delivery delivered; answer first, then do the work. Anything else, a 3xx included, is retried by the ladder."}}}},"booking.no_show":{"post":{"summary":"A booking was marked a no show.","description":"data.booking with status no_show.","parameters":[{"name":"webhook-id","in":"header","required":true,"description":"The event id; the same on every attempt and replay of this event, so a partner can deduplicate.","schema":{"type":"string","pattern":"^evt_[0-9a-f]{24}$"},"example":"evt_000000000000000000000005"},{"name":"webhook-timestamp","in":"header","required":true,"description":"Unix seconds when this attempt was signed; refuse a delivery whose timestamp is more than five minutes from now.","schema":{"type":"string","pattern":"^[0-9]{1,12}$"},"example":"1791275522"},{"name":"webhook-signature","in":"header","required":true,"description":"One or more signatures separated by spaces, each v1, then base64: HMAC SHA256 over id, a full stop, timestamp, a full stop, the raw body bytes, keyed by the base64 decoded secret after whsec_. Two during the day after a rotation. This example verifies the example body, serialised compactly, under the document's example secret.","schema":{"type":"string"},"example":"v1,Hi81kLqMLMi0XosBlgTdoQGUrfTZjr1YhKqMUWKJPFo="}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookEvent"},{"type":"object","properties":{"type":{"type":"string","enum":["booking.no_show"]},"data":{"$ref":"#/components/schemas/BookingEventData"}}}]},"examples":{"delivery":{"summary":"A booking.no_show delivery","value":{"id":"evt_000000000000000000000005","type":"booking.no_show","apiVersion":"v1","payloadVersion":"1","occurredAt":"2026-10-06T08:31:12.000Z","business":{"id":"00000000-0000-4000-8000-00000000b001"},"data":{"booking":{"id":"00000000-0000-4000-8000-0000000000b3","customerName":"Alex Example","customerEmail":"alex@example.test","customerPhone":"+44 7700 900123","serviceName":"Signature Facial","serviceDuration":60,"staffName":"Maya Okafor","status":"no_show","source":"api","paymentStatus":"unpaid","startTime":"2026-10-06T13:30:00.000Z","endTime":"2026-10-06T14:30:00.000Z","priceInPence":6500,"notes":"Booked by the hotel concierge","createdAt":"2026-10-06T08:31:12.000Z","serviceId":"00000000-0000-4000-8000-0000000000e2","staffProfileId":"00000000-0000-4000-8000-0000000000a1","locationId":"00000000-0000-4000-8000-000000000c02","updatedAt":"2026-10-06T13:50:00.000Z","cancelledAt":null}}}}}}}},"responses":{"2XX":{"description":"Acknowledged. Any 2xx within ten seconds marks the delivery delivered; answer first, then do the work. Anything else, a 3xx included, is retried by the ladder."}}}},"booking.rescheduled":{"post":{"summary":"A booking was moved to a new time.","description":"data.booking at its new time and data.previous.startTime, the start it moved from.","parameters":[{"name":"webhook-id","in":"header","required":true,"description":"The event id; the same on every attempt and replay of this event, so a partner can deduplicate.","schema":{"type":"string","pattern":"^evt_[0-9a-f]{24}$"},"example":"evt_000000000000000000000006"},{"name":"webhook-timestamp","in":"header","required":true,"description":"Unix seconds when this attempt was signed; refuse a delivery whose timestamp is more than five minutes from now.","schema":{"type":"string","pattern":"^[0-9]{1,12}$"},"example":"1791275523"},{"name":"webhook-signature","in":"header","required":true,"description":"One or more signatures separated by spaces, each v1, then base64: HMAC SHA256 over id, a full stop, timestamp, a full stop, the raw body bytes, keyed by the base64 decoded secret after whsec_. Two during the day after a rotation. This example verifies the example body, serialised compactly, under the document's example secret.","schema":{"type":"string"},"example":"v1,OPx3+CZ6iGUg//trIHz70TO4d42ENBiZjJ5HBcb2RNg="}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookEvent"},{"type":"object","properties":{"type":{"type":"string","enum":["booking.rescheduled"]},"data":{"$ref":"#/components/schemas/BookingRescheduledEventData"}}}]},"examples":{"delivery":{"summary":"A booking.rescheduled delivery","value":{"id":"evt_000000000000000000000006","type":"booking.rescheduled","apiVersion":"v1","payloadVersion":"1","occurredAt":"2026-10-06T08:31:12.000Z","business":{"id":"00000000-0000-4000-8000-00000000b001"},"data":{"booking":{"id":"00000000-0000-4000-8000-0000000000b3","customerName":"Alex Example","customerEmail":"alex@example.test","customerPhone":"+44 7700 900123","serviceName":"Signature Facial","serviceDuration":60,"staffName":"Maya Okafor","status":"confirmed","source":"api","paymentStatus":"unpaid","startTime":"2026-10-07T10:00:00.000Z","endTime":"2026-10-07T11:00:00.000Z","priceInPence":6500,"notes":"Booked by the hotel concierge","createdAt":"2026-10-06T08:31:12.000Z","serviceId":"00000000-0000-4000-8000-0000000000e2","staffProfileId":"00000000-0000-4000-8000-0000000000a1","locationId":"00000000-0000-4000-8000-000000000c02","updatedAt":"2026-10-05T16:40:00.000Z","cancelledAt":null},"previous":{"startTime":"2026-10-06T13:30:00.000Z"}}}}}}}},"responses":{"2XX":{"description":"Acknowledged. Any 2xx within ten seconds marks the delivery delivered; answer first, then do the work. Anything else, a 3xx included, is retried by the ladder."}}}},"client.created":{"post":{"summary":"A client was added to the client book, by any route.","description":"data.client as the API answers it.","parameters":[{"name":"webhook-id","in":"header","required":true,"description":"The event id; the same on every attempt and replay of this event, so a partner can deduplicate.","schema":{"type":"string","pattern":"^evt_[0-9a-f]{24}$"},"example":"evt_000000000000000000000007"},{"name":"webhook-timestamp","in":"header","required":true,"description":"Unix seconds when this attempt was signed; refuse a delivery whose timestamp is more than five minutes from now.","schema":{"type":"string","pattern":"^[0-9]{1,12}$"},"example":"1791275524"},{"name":"webhook-signature","in":"header","required":true,"description":"One or more signatures separated by spaces, each v1, then base64: HMAC SHA256 over id, a full stop, timestamp, a full stop, the raw body bytes, keyed by the base64 decoded secret after whsec_. Two during the day after a rotation. This example verifies the example body, serialised compactly, under the document's example secret.","schema":{"type":"string"},"example":"v1,MFfdriZscK7wpZBEQ7yener06ScpzhYgs25Mp7yhYLw="}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookEvent"},{"type":"object","properties":{"type":{"type":"string","enum":["client.created"]},"data":{"$ref":"#/components/schemas/ClientEventData"}}}]},"examples":{"delivery":{"summary":"A client.created delivery","value":{"id":"evt_000000000000000000000007","type":"client.created","apiVersion":"v1","payloadVersion":"1","occurredAt":"2026-10-06T08:31:12.000Z","business":{"id":"00000000-0000-4000-8000-00000000b001"},"data":{"client":{"id":"00000000-0000-4000-8000-0000000000c1","name":"Alex Example","email":"alex@example.test","phone":"+44 7700 900123","createdAt":"2026-04-11T10:00:00.000Z","updatedAt":null}}}}}}}},"responses":{"2XX":{"description":"Acknowledged. Any 2xx within ten seconds marks the delivery delivered; answer first, then do the work. Anything else, a 3xx included, is retried by the ladder."}}}},"client.updated":{"post":{"summary":"A client's details changed, a merge included.","description":"data.client after the change, with updatedAt set.","parameters":[{"name":"webhook-id","in":"header","required":true,"description":"The event id; the same on every attempt and replay of this event, so a partner can deduplicate.","schema":{"type":"string","pattern":"^evt_[0-9a-f]{24}$"},"example":"evt_000000000000000000000008"},{"name":"webhook-timestamp","in":"header","required":true,"description":"Unix seconds when this attempt was signed; refuse a delivery whose timestamp is more than five minutes from now.","schema":{"type":"string","pattern":"^[0-9]{1,12}$"},"example":"1791275525"},{"name":"webhook-signature","in":"header","required":true,"description":"One or more signatures separated by spaces, each v1, then base64: HMAC SHA256 over id, a full stop, timestamp, a full stop, the raw body bytes, keyed by the base64 decoded secret after whsec_. Two during the day after a rotation. This example verifies the example body, serialised compactly, under the document's example secret.","schema":{"type":"string"},"example":"v1,QkolWB1qXZYuCZW2mZW92V9TWFEZJWNNGBAVIucoWcg="}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookEvent"},{"type":"object","properties":{"type":{"type":"string","enum":["client.updated"]},"data":{"$ref":"#/components/schemas/ClientEventData"}}}]},"examples":{"delivery":{"summary":"A client.updated delivery","value":{"id":"evt_000000000000000000000008","type":"client.updated","apiVersion":"v1","payloadVersion":"1","occurredAt":"2026-10-06T08:31:12.000Z","business":{"id":"00000000-0000-4000-8000-00000000b001"},"data":{"client":{"id":"00000000-0000-4000-8000-0000000000c1","name":"Alex Example","email":"alex@example.test","phone":"+44 7700 900123","createdAt":"2026-04-11T10:00:00.000Z","updatedAt":"2026-09-25T09:15:32.000Z"}}}}}}}},"responses":{"2XX":{"description":"Acknowledged. Any 2xx within ten seconds marks the delivery delivered; answer first, then do the work. Anything else, a 3xx included, is retried by the ladder."}}}},"client.archived":{"post":{"summary":"A client was archived.","description":"data.client and data.active false.","parameters":[{"name":"webhook-id","in":"header","required":true,"description":"The event id; the same on every attempt and replay of this event, so a partner can deduplicate.","schema":{"type":"string","pattern":"^evt_[0-9a-f]{24}$"},"example":"evt_000000000000000000000009"},{"name":"webhook-timestamp","in":"header","required":true,"description":"Unix seconds when this attempt was signed; refuse a delivery whose timestamp is more than five minutes from now.","schema":{"type":"string","pattern":"^[0-9]{1,12}$"},"example":"1791275526"},{"name":"webhook-signature","in":"header","required":true,"description":"One or more signatures separated by spaces, each v1, then base64: HMAC SHA256 over id, a full stop, timestamp, a full stop, the raw body bytes, keyed by the base64 decoded secret after whsec_. Two during the day after a rotation. This example verifies the example body, serialised compactly, under the document's example secret.","schema":{"type":"string"},"example":"v1,Vi6sOvzSm7Eb4vk1mMAcPN4PYqRiouksShpnpaYnvA4="}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookEvent"},{"type":"object","properties":{"type":{"type":"string","enum":["client.archived"]},"data":{"$ref":"#/components/schemas/ClientArchivedEventData"}}}]},"examples":{"delivery":{"summary":"A client.archived delivery","value":{"id":"evt_000000000000000000000009","type":"client.archived","apiVersion":"v1","payloadVersion":"1","occurredAt":"2026-10-06T08:31:12.000Z","business":{"id":"00000000-0000-4000-8000-00000000b001"},"data":{"client":{"id":"00000000-0000-4000-8000-0000000000c1","name":"Alex Example","email":"alex@example.test","phone":"+44 7700 900123","createdAt":"2026-04-11T10:00:00.000Z","updatedAt":"2026-09-25T09:15:32.000Z"},"active":false}}}}}}},"responses":{"2XX":{"description":"Acknowledged. Any 2xx within ten seconds marks the delivery delivered; answer first, then do the work. Anything else, a 3xx included, is retried by the ladder."}}}},"payment.succeeded":{"post":{"summary":"A payment for a booking was recorded in the ledger: a deposit, a saved card charge, a till payment or a manual record.","description":"data.payment (the entry, the amount in pence, the currency, when it was recorded) and data.booking with the paymentStatus after the entry.","parameters":[{"name":"webhook-id","in":"header","required":true,"description":"The event id; the same on every attempt and replay of this event, so a partner can deduplicate.","schema":{"type":"string","pattern":"^evt_[0-9a-f]{24}$"},"example":"evt_00000000000000000000000a"},{"name":"webhook-timestamp","in":"header","required":true,"description":"Unix seconds when this attempt was signed; refuse a delivery whose timestamp is more than five minutes from now.","schema":{"type":"string","pattern":"^[0-9]{1,12}$"},"example":"1791275527"},{"name":"webhook-signature","in":"header","required":true,"description":"One or more signatures separated by spaces, each v1, then base64: HMAC SHA256 over id, a full stop, timestamp, a full stop, the raw body bytes, keyed by the base64 decoded secret after whsec_. Two during the day after a rotation. This example verifies the example body, serialised compactly, under the document's example secret.","schema":{"type":"string"},"example":"v1,ML343ey4w1KSGRO7zadptu5Sa8xR4ucR26dUjXjntTc="}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookEvent"},{"type":"object","properties":{"type":{"type":"string","enum":["payment.succeeded"]},"data":{"$ref":"#/components/schemas/PaymentEventData"}}}]},"examples":{"delivery":{"summary":"A payment.succeeded delivery","value":{"id":"evt_00000000000000000000000a","type":"payment.succeeded","apiVersion":"v1","payloadVersion":"1","occurredAt":"2026-10-06T08:31:12.000Z","business":{"id":"00000000-0000-4000-8000-00000000b001"},"data":{"payment":{"id":"00000000-0000-4000-8000-00000000d001","bookingId":"00000000-0000-4000-8000-0000000000b3","amountInPence":6500,"currency":"GBP","recordedAt":"2026-10-06T14:35:00.000Z"},"booking":{"id":"00000000-0000-4000-8000-0000000000b3","customerName":"Alex Example","customerEmail":"alex@example.test","customerPhone":"+44 7700 900123","serviceName":"Signature Facial","serviceDuration":60,"staffName":"Maya Okafor","status":"completed","source":"api","paymentStatus":"paid","startTime":"2026-10-06T13:30:00.000Z","endTime":"2026-10-06T14:30:00.000Z","priceInPence":6500,"notes":"Booked by the hotel concierge","createdAt":"2026-10-06T08:31:12.000Z","serviceId":"00000000-0000-4000-8000-0000000000e2","staffProfileId":"00000000-0000-4000-8000-0000000000a1","locationId":"00000000-0000-4000-8000-000000000c02","updatedAt":"2026-10-06T14:35:00.000Z","cancelledAt":null}}}}}}}},"responses":{"2XX":{"description":"Acknowledged. Any 2xx within ten seconds marks the delivery delivered; answer first, then do the work. Anything else, a 3xx included, is retried by the ladder."}}}},"payment.refunded":{"post":{"summary":"A refund for a booking was recorded in the ledger.","description":"data.payment with amountInPence the refunded amount, positive, and data.booking.","parameters":[{"name":"webhook-id","in":"header","required":true,"description":"The event id; the same on every attempt and replay of this event, so a partner can deduplicate.","schema":{"type":"string","pattern":"^evt_[0-9a-f]{24}$"},"example":"evt_00000000000000000000000b"},{"name":"webhook-timestamp","in":"header","required":true,"description":"Unix seconds when this attempt was signed; refuse a delivery whose timestamp is more than five minutes from now.","schema":{"type":"string","pattern":"^[0-9]{1,12}$"},"example":"1791275528"},{"name":"webhook-signature","in":"header","required":true,"description":"One or more signatures separated by spaces, each v1, then base64: HMAC SHA256 over id, a full stop, timestamp, a full stop, the raw body bytes, keyed by the base64 decoded secret after whsec_. Two during the day after a rotation. This example verifies the example body, serialised compactly, under the document's example secret.","schema":{"type":"string"},"example":"v1,mfxlyyundJZTxwZ00M0h8RX4tGYo6JlKFnchfLa6vbg="}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookEvent"},{"type":"object","properties":{"type":{"type":"string","enum":["payment.refunded"]},"data":{"$ref":"#/components/schemas/PaymentEventData"}}}]},"examples":{"delivery":{"summary":"A payment.refunded delivery","value":{"id":"evt_00000000000000000000000b","type":"payment.refunded","apiVersion":"v1","payloadVersion":"1","occurredAt":"2026-10-06T08:31:12.000Z","business":{"id":"00000000-0000-4000-8000-00000000b001"},"data":{"payment":{"id":"00000000-0000-4000-8000-00000000d002","bookingId":"00000000-0000-4000-8000-0000000000b3","amountInPence":6500,"currency":"GBP","recordedAt":"2026-10-07T09:12:00.000Z"},"booking":{"id":"00000000-0000-4000-8000-0000000000b3","customerName":"Alex Example","customerEmail":"alex@example.test","customerPhone":"+44 7700 900123","serviceName":"Signature Facial","serviceDuration":60,"staffName":"Maya Okafor","status":"cancelled","source":"api","paymentStatus":"paid","startTime":"2026-10-06T13:30:00.000Z","endTime":"2026-10-06T14:30:00.000Z","priceInPence":6500,"notes":"Booked by the hotel concierge","createdAt":"2026-10-06T08:31:12.000Z","serviceId":"00000000-0000-4000-8000-0000000000e2","staffProfileId":"00000000-0000-4000-8000-0000000000a1","locationId":"00000000-0000-4000-8000-000000000c02","updatedAt":"2026-10-07T09:12:00.000Z","cancelledAt":"2026-10-07T09:10:00.000Z"}}}}}}}},"responses":{"2XX":{"description":"Acknowledged. Any 2xx within ten seconds marks the delivery delivered; answer first, then do the work. Anything else, a 3xx included, is retried by the ladder."}}}},"payment.refund_failed":{"post":{"summary":"A refund attempt for a booking failed.","description":"data.payment without an id (no ledger entry), amountInPence the attempted amount or null, and data.booking.","parameters":[{"name":"webhook-id","in":"header","required":true,"description":"The event id; the same on every attempt and replay of this event, so a partner can deduplicate.","schema":{"type":"string","pattern":"^evt_[0-9a-f]{24}$"},"example":"evt_00000000000000000000000c"},{"name":"webhook-timestamp","in":"header","required":true,"description":"Unix seconds when this attempt was signed; refuse a delivery whose timestamp is more than five minutes from now.","schema":{"type":"string","pattern":"^[0-9]{1,12}$"},"example":"1791275529"},{"name":"webhook-signature","in":"header","required":true,"description":"One or more signatures separated by spaces, each v1, then base64: HMAC SHA256 over id, a full stop, timestamp, a full stop, the raw body bytes, keyed by the base64 decoded secret after whsec_. Two during the day after a rotation. This example verifies the example body, serialised compactly, under the document's example secret.","schema":{"type":"string"},"example":"v1,xJ8AyQYoqYSN7dK7AjaSeJzc/IvO/Rcyd0BNDDul504="}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookEvent"},{"type":"object","properties":{"type":{"type":"string","enum":["payment.refund_failed"]},"data":{"$ref":"#/components/schemas/PaymentEventData"}}}]},"examples":{"delivery":{"summary":"A payment.refund_failed delivery","value":{"id":"evt_00000000000000000000000c","type":"payment.refund_failed","apiVersion":"v1","payloadVersion":"1","occurredAt":"2026-10-06T08:31:12.000Z","business":{"id":"00000000-0000-4000-8000-00000000b001"},"data":{"payment":{"bookingId":"00000000-0000-4000-8000-0000000000b3","amountInPence":6500,"currency":"GBP","recordedAt":"2026-10-07T09:12:00.000Z"},"booking":{"id":"00000000-0000-4000-8000-0000000000b3","customerName":"Alex Example","customerEmail":"alex@example.test","customerPhone":"+44 7700 900123","serviceName":"Signature Facial","serviceDuration":60,"staffName":"Maya Okafor","status":"cancelled","source":"api","paymentStatus":"paid","startTime":"2026-10-06T13:30:00.000Z","endTime":"2026-10-06T14:30:00.000Z","priceInPence":6500,"notes":"Booked by the hotel concierge","createdAt":"2026-10-06T08:31:12.000Z","serviceId":"00000000-0000-4000-8000-0000000000e2","staffProfileId":"00000000-0000-4000-8000-0000000000a1","locationId":"00000000-0000-4000-8000-000000000c02","updatedAt":"2026-10-07T09:10:00.000Z","cancelledAt":"2026-10-07T09:10:00.000Z"}}}}}}}},"responses":{"2XX":{"description":"Acknowledged. Any 2xx within ten seconds marks the delivery delivered; answer first, then do the work. Anything else, a 3xx included, is retried by the ladder."}}}},"ping":{"post":{"summary":"A test delivery, sent from the dashboard or by the test operation, regardless of the endpoint's filter and never retried.","description":"data.endpointId and data.message.","parameters":[{"name":"webhook-id","in":"header","required":true,"description":"The event id; the same on every attempt and replay of this event, so a partner can deduplicate.","schema":{"type":"string","pattern":"^evt_[0-9a-f]{24}$"},"example":"evt_00000000000000000000000d"},{"name":"webhook-timestamp","in":"header","required":true,"description":"Unix seconds when this attempt was signed; refuse a delivery whose timestamp is more than five minutes from now.","schema":{"type":"string","pattern":"^[0-9]{1,12}$"},"example":"1791275530"},{"name":"webhook-signature","in":"header","required":true,"description":"One or more signatures separated by spaces, each v1, then base64: HMAC SHA256 over id, a full stop, timestamp, a full stop, the raw body bytes, keyed by the base64 decoded secret after whsec_. Two during the day after a rotation. This example verifies the example body, serialised compactly, under the document's example secret.","schema":{"type":"string"},"example":"v1,PM7f0SBDmVgsNNP3jy26T1yDn+SjNEHcQygizDZutc0="}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookEvent"},{"type":"object","properties":{"type":{"type":"string","enum":["ping"]},"data":{"$ref":"#/components/schemas/PingEventData"}}}]},"examples":{"delivery":{"summary":"A ping delivery","value":{"id":"evt_00000000000000000000000d","type":"ping","apiVersion":"v1","payloadVersion":"1","occurredAt":"2026-10-06T08:31:12.000Z","business":{"id":"00000000-0000-4000-8000-00000000b001"},"data":{"endpointId":"00000000-0000-4000-8000-00000000e001","message":"A test delivery from pamprr."}}}}}}},"responses":{"2XX":{"description":"Acknowledged. Any 2xx within ten seconds marks the delivery delivered; answer first, then do the work. Anything else, a 3xx included, is retried by the ladder."}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Provide your pamprr_live_<secret> API key as Bearer <token>. Keys are minted in the operator Settings UI and revealed exactly once. Every key carries an access setting (a list of scopes such as bookings:read); each operation names the one scope it requires in x-pamprr-required-scope, and a key without it receives 403 FORBIDDEN. The security requirement on each operation names that scope as a role, the OpenAPI 3.1 form; x-pamprr-required-scope carries the same name for tools that ignore roles on http schemes."}},"headers":{"X-Request-Id":{"schema":{"type":"string","pattern":"^req_[0-9a-f]{24}$"},"description":"Server generated id for this request, present on every response. Quote it to support; it is meaningless otherwise."},"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"The allowance in effect for this key and endpoint, per sliding sixty second window (100 today). Authoritative: read it rather than hard coding the number. It may rise without notice and never falls without a deprecation notice. Present on every authenticated response; absent from a 429 raised by abuse protection rather than the allowance."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window after this one; 0 on a 429. Every authenticated request counts, errors and idempotent replays included."},"X-RateLimit-Reset":{"schema":{"type":"string","format":"date-time"},"description":"ISO 8601 UTC timestamp by which the window will have fully cleared. An upper bound, not the exact moment one slot frees."},"Retry-After":{"schema":{"type":"integer"},"description":"Whole seconds to wait before retrying, rounded up. Present on every 429."},"WWW-Authenticate":{"schema":{"type":"string"},"description":"RFC 6750 bearer challenge. On a 401: Bearer error=\"invalid_token\" when a credential was presented, or the bare challenge Bearer when none was. On a 403: Bearer error=\"insufficient_scope\", scope=\"<the required scope>\"."},"Idempotent-Replayed":{"schema":{"type":"string","enum":["true"]},"description":"Present, with the value true, only when this response is the stored outcome of an earlier request that carried the same Idempotency-Key. The body is byte identical to the original."},"X-Original-Request-Id":{"schema":{"type":"string","pattern":"^req_[0-9a-f]{24}$"},"description":"On a replay: the id of the request that did the work. X-Request-Id names this request."}},"responses":{"BadRequest":{"description":"Malformed request (codes INVALID_JSON, VALIDATION_FAILED, INVALID_START_TIME, PAST_START_TIME, INVALID_LOCATION, INVALID_DATE_RANGE, on a list INVALID_CURSOR, on a write IDEMPOTENCY_KEY_INVALID)","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"INVALID_JSON":{"summary":"The body is not JSON","value":{"error":"Request body must be valid JSON.","code":"INVALID_JSON","requestId":"req_000000000000000000000001"}},"VALIDATION_FAILED":{"summary":"A value fails its rule; field names it where one is named","value":{"error":"locationId must be a UUID.","code":"VALIDATION_FAILED","field":"locationId","requestId":"req_000000000000000000000002"}},"INVALID_START_TIME":{"summary":"startTime does not parse","value":{"error":"Booking start time is not a valid ISO date.","code":"INVALID_START_TIME","requestId":"req_000000000000000000000003"}},"PAST_START_TIME":{"summary":"startTime is in the past","value":{"error":"Booking start time is in the past.","code":"PAST_START_TIME","requestId":"req_000000000000000000000004"}},"INVALID_LOCATION":{"summary":"The branch does not offer the service","value":{"error":"This service is not offered at the requested location.","code":"INVALID_LOCATION","requestId":"req_000000000000000000000005"}},"INVALID_DATE_RANGE":{"summary":"A date bound does not parse","value":{"error":"startDate and endDate must be valid ISO dates.","code":"INVALID_DATE_RANGE","requestId":"req_000000000000000000000006"}},"INVALID_CURSOR":{"summary":"The cursor does not decode or belongs to other filters","value":{"error":"The cursor is not valid for this list and these filters. Start again from the first page.","code":"INVALID_CURSOR","requestId":"req_000000000000000000000007"}},"IDEMPOTENCY_KEY_INVALID":{"summary":"The Idempotency-Key is malformed","value":{"error":"Idempotency-Key must be 1 to 255 printable ASCII characters; a UUID is recommended.","code":"IDEMPOTENCY_KEY_INVALID","requestId":"req_000000000000000000000008"}}}}}},"InvalidLocation":{"description":"The branch named is not one of your business's visible branches, does not offer the service, or the staff member does not work there (code INVALID_LOCATION)","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"INVALID_LOCATION":{"summary":"The staff member does not work at the branch","value":{"error":"This staff member is not available at the requested location.","code":"INVALID_LOCATION","requestId":"req_000000000000000000000009"}}}}}},"Unauthorized":{"description":"Missing or invalid API key (code UNAUTHORIZED). A key that verified but is revoked or expired says so in reason; anything that did not verify is the one collapsed refusal with no reason.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"WWW-Authenticate":{"$ref":"#/components/headers/WWW-Authenticate"}},"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"},{"type":"object","properties":{"reason":{"type":"string","enum":["revoked","expired"],"description":"Present only when the key verified: why it is refused."}}}]},"examples":{"invalid":{"summary":"No key, or a key that does not verify: the one collapsed refusal","value":{"error":"Invalid API key.","code":"UNAUTHORIZED","requestId":"req_00000000000000000000000a"}},"revoked":{"summary":"A verified key that was revoked","value":{"error":"This API key has been revoked.","code":"UNAUTHORIZED","reason":"revoked","requestId":"req_00000000000000000000000b"}},"expired":{"summary":"A verified key past its expiry","value":{"error":"This API key expired on 2026-12-01.","code":"UNAUTHORIZED","reason":"expired","requestId":"req_00000000000000000000000c"}}}}}},"Forbidden":{"description":"The key is valid but its access setting does not include the scope this endpoint requires (code FORBIDDEN; requiredScope names it). The request was authenticated and counted against the allowance.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"WWW-Authenticate":{"$ref":"#/components/headers/WWW-Authenticate"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"},{"type":"object","properties":{"requiredScope":{"type":"string","description":"The scope the endpoint requires, e.g. bookings:write."}},"required":["requiredScope"]}]},"examples":{"FORBIDDEN":{"summary":"A read only key attempting a write","value":{"error":"This API key does not have the bookings:write scope.","code":"FORBIDDEN","requiredScope":"bookings:write","requestId":"req_00000000000000000000000d"}}}}}},"InvalidCursor":{"description":"The cursor does not decode, belongs to another list, was issued under different filters, or fails its type checks (code INVALID_CURSOR). Start again from the first page.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"INVALID_CURSOR":{"summary":"Start again from the first page","value":{"error":"The cursor is not valid for this list and these filters. Start again from the first page.","code":"INVALID_CURSOR","requestId":"req_00000000000000000000000e"}}}}}},"IdempotencyKeyReused":{"description":"The Idempotency-Key was already used for a different request (a different method, path or body). Use a new key for a new request (code IDEMPOTENCY_KEY_REUSED).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"IDEMPOTENCY_KEY_REUSED":{"summary":"The same key with a different request","value":{"error":"This idempotency key was used for a different request. Use a new key for a new request.","code":"IDEMPOTENCY_KEY_REUSED","requestId":"req_00000000000000000000000f"}}}}}},"NotFound":{"description":"Resource not found (code NOT_FOUND)","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"NOT_FOUND":{"summary":"Not yours, gone, archived, or not a UUID; the API never says which","value":{"error":"Booking not found.","code":"NOT_FOUND","requestId":"req_000000000000000000000010"}}}}}},"Conflict":{"description":"Resource conflict: an overlapping booking slot (code BOOKING_OVERLAP), or a retry while the first request carrying this Idempotency-Key is still being processed (code IDEMPOTENCY_REQUEST_IN_PROGRESS; retry shortly)","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"BOOKING_OVERLAP":{"summary":"Another booking has the slot","value":{"error":"This time slot is already booked.","code":"BOOKING_OVERLAP","requestId":"req_000000000000000000000011"}},"IDEMPOTENCY_REQUEST_IN_PROGRESS":{"summary":"A retry while the first request is still running","value":{"error":"A request with this idempotency key is still being processed. Retry shortly.","code":"IDEMPOTENCY_REQUEST_IN_PROGRESS","requestId":"req_000000000000000000000012"}}}}}},"BookingCompleted":{"description":"The booking is completed; a completed booking cannot be cancelled or rescheduled through the API (code BOOKING_COMPLETED)","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"BOOKING_COMPLETED":{"summary":"A completed booking cannot be cancelled or rescheduled through the API","value":{"error":"This booking is completed and cannot be cancelled through the API.","code":"BOOKING_COMPLETED","requestId":"req_000000000000000000000013"}}}}}},"BookingNoShow":{"description":"The booking was marked as a no show; it cannot be cancelled or rescheduled through the API (code BOOKING_NO_SHOW)","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"BOOKING_NO_SHOW":{"summary":"A no show booking cannot be cancelled or rescheduled through the API","value":{"error":"This booking was marked as a no show and cannot be cancelled through the API.","code":"BOOKING_NO_SHOW","requestId":"req_000000000000000000000014"}}}}}},"BookingCancelled":{"description":"The booking is cancelled and cannot be rescheduled (code BOOKING_CANCELLED)","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"BOOKING_CANCELLED":{"summary":"A cancelled booking cannot be rescheduled","value":{"error":"This booking is cancelled and cannot be rescheduled.","code":"BOOKING_CANCELLED","requestId":"req_000000000000000000000015"}}}}}},"SlotHeld":{"description":"A customer is mid checkout for this time with this staff member; the slot frees itself within ten minutes if they abandon checkout (code SLOT_HELD)","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"SLOT_HELD":{"summary":"A customer is mid checkout for the slot","value":{"error":"Someone else is booking this time right now. It frees up in a few minutes if they do not complete their booking. Please choose another time or try again shortly.","code":"SLOT_HELD","requestId":"req_000000000000000000000016"}}}}}},"DepositRequired":{"description":"The service's effective booking protection includes a deposit and the business has Stripe live; the booking must be made through the consumer booking flow (code DEPOSIT_REQUIRED)","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"DEPOSIT_REQUIRED":{"summary":"The service's effective booking protection includes a deposit and Stripe is live","value":{"error":"A deposit is required for this service. Bookings for this service must be made through the consumer booking flow.","code":"DEPOSIT_REQUIRED","requestId":"req_000000000000000000000017"}}}}}},"CardCaptureRequired":{"description":"The service's effective booking protection is card capture and the business has Stripe live; the booking needs a saved payment method and the customer's consent, which only the consumer booking flow can take (code CARD_CAPTURE_REQUIRED)","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"CARD_CAPTURE_REQUIRED":{"summary":"The service's effective booking protection is card capture and Stripe is live","value":{"error":"This service requires a saved payment method and consent before booking. Bookings for this service must be made through the consumer booking flow.","code":"CARD_CAPTURE_REQUIRED","requestId":"req_000000000000000000000018"}}}}}},"RateLimited":{"description":"Rate limit exceeded (code RATE_LIMIT_EXCEEDED). Wait Retry-After seconds and retry. With X-RateLimit-Limit present the key's sliding sixty second allowance was exhausted; without it the refusal came from abuse protection and the same Retry-After rule applies.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"Retry-After":{"$ref":"#/components/headers/Retry-After"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"allowance":{"summary":"The key's sliding sixty second allowance is spent; X-RateLimit-Limit is present","value":{"error":"Rate limit exceeded.","code":"RATE_LIMIT_EXCEEDED","requestId":"req_000000000000000000000019"}},"abuseCeiling":{"summary":"Abuse protection refused before the key was checked; X-RateLimit-Limit is absent and the same Retry-After rule applies","value":{"error":"Rate limit exceeded.","code":"RATE_LIMIT_EXCEEDED","requestId":"req_00000000000000000000001a"}}}}}},"ServerError":{"description":"Unexpected server error (code INTERNAL_ERROR). The body carries no internal detail; quote the request id to support. The allowance headers are present when the request had authenticated and its allowance had been answered.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"INTERNAL_ERROR":{"summary":"An unexpected failure; the body carries no internal detail","value":{"error":"Could not list bookings. Please try again.","code":"INTERNAL_ERROR","requestId":"req_00000000000000000000001b"}}}}}},"WebhookLimitReached":{"description":"The business already has ten webhook endpoints; delete one before creating another (code WEBHOOK_LIMIT_REACHED)","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"WEBHOOK_LIMIT_REACHED":{"summary":"Ten live endpoints already","value":{"error":"This business already has 10 webhook endpoints.","code":"WEBHOOK_LIMIT_REACHED","requestId":"req_00000000000000000000001c"}}}}}},"WebhookEndpointDisabled":{"description":"The endpoint is disabled (manually, after three days of failures, or after a 410); enable it before sending a test or replaying (code WEBHOOK_ENDPOINT_DISABLED)","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"WEBHOOK_ENDPOINT_DISABLED":{"summary":"A test on a disabled endpoint","value":{"error":"This endpoint is disabled (failing). Enable it before sending a test.","code":"WEBHOOK_ENDPOINT_DISABLED","requestId":"req_00000000000000000000001d"}}}}}},"WebhookDeliveryPending":{"description":"The delivery is still pending and will be attempted on schedule; replay a delivered, failed or cancelled delivery instead (code WEBHOOK_DELIVERY_PENDING)","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"WEBHOOK_DELIVERY_PENDING":{"summary":"A replay of a delivery still on the ladder","value":{"error":"This delivery is still pending and will be attempted on schedule.","code":"WEBHOOK_DELIVERY_PENDING","requestId":"req_00000000000000000000001e"}}}}}}},"schemas":{"Error":{"type":"object","description":"The one error envelope every 4xx and 5xx carries. Code specific fields may be added beside these.","properties":{"error":{"type":"string","description":"A sentence for a human."},"code":{"type":"string","description":"The machine readable code; branch on this, never on the message.","enum":["INVALID_JSON","VALIDATION_FAILED","INVALID_START_TIME","PAST_START_TIME","INVALID_LOCATION","INVALID_DATE_RANGE","INVALID_CURSOR","IDEMPOTENCY_KEY_INVALID","UNAUTHORIZED","DEPOSIT_REQUIRED","CARD_CAPTURE_REQUIRED","FORBIDDEN","NOT_FOUND","BOOKING_OVERLAP","BOOKING_COMPLETED","BOOKING_NO_SHOW","BOOKING_CANCELLED","SLOT_HELD","IDEMPOTENCY_REQUEST_IN_PROGRESS","IDEMPOTENCY_KEY_REUSED","RATE_LIMIT_EXCEEDED","INTERNAL_ERROR","WEBHOOK_LIMIT_REACHED","WEBHOOK_ENDPOINT_DISABLED","WEBHOOK_DELIVERY_PENDING"]},"requestId":{"type":"string","pattern":"^req_[0-9a-f]{24}$","description":"The same id as the X-Request-Id header. Quote it to support."}},"required":["error","code","requestId"]},"Business":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"slug":{"type":"string"},"displayName":{"type":"string"},"email":{"type":["string","null"]},"timezone":{"type":"string","description":"The IANA zone the business runs on; Europe/London for every business today. Render local times with it; every time in this API is UTC."},"currency":{"type":"string","description":"The ISO 4217 code every price is in; GBP for every business today. Prices are in pence."}},"required":["id","name","slug","displayName","email","timezone","currency"]},"Client":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"email":{"type":["string","null"]},"phone":{"type":["string","null"]},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time","description":"When the client record last changed; null until it first changes after creation."}},"required":["id","name","email","phone","createdAt","updatedAt"]},"Service":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":["string","null"]},"durationMinutes":{"type":"integer"},"priceInPence":{"type":"integer"},"isActive":{"type":"boolean"},"isOnlineBookable":{"type":"boolean"},"isAddOn":{"type":"boolean"},"locationIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Branch ids the service is offered at, from the service_locations junction (Catalogue Stage 1). Always present; empty when the service has no offering rows."}},"required":["id","name","description","durationMinutes","priceInPence","isActive","isOnlineBookable","isAddOn","locationIds"]},"EligibleStaff":{"type":"object","description":"The members who can perform one service, and where.","properties":{"serviceId":{"type":"string","format":"uuid"},"serviceName":{"type":"string"},"locationId":{"type":["string","null"],"format":"uuid","description":"The branch asked for; null when the answer covers every offering branch."},"staff":{"type":"array","items":{"type":"object","description":"The Staff projection's presentation fields, with locationIds narrowed to the branches where this member can perform this service.","properties":{"id":{"type":"string","format":"uuid","description":"The staffProfileId a booking names."},"name":{"type":"string"},"initials":{"type":"string"},"colour":{"type":["string","null"]},"jobTitle":{"type":["string","null"]},"profileImageUrl":{"type":["string","null"]},"isBookable":{"type":"boolean"},"acceptsOnlineBookings":{"type":"boolean"},"deliveryMode":{"type":"string","enum":["fixed","mobile","both"]},"locationIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"The visible branches where this member can perform this service; the branch asked for alone when locationId was given."}},"required":["id","name","initials","isBookable","acceptsOnlineBookings","deliveryMode","locationIds"]}}},"required":["serviceId","serviceName","locationId","staff"]},"Location":{"type":"object","description":"A branch of the business. Never the whole row: no coordinates, catchment, travel or hours columns.","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"address":{"type":["string","null"]},"city":{"type":["string","null"]},"postcode":{"type":["string","null"]},"phone":{"type":["string","null"],"description":"The branch's public contact, as shown to customers."},"email":{"type":["string","null"]},"website":{"type":["string","null"]},"locationModel":{"type":["string","null"],"enum":["fixed","mobile","both",null],"description":"Whether the branch takes customers at its address, travels to them, or both. Null on a legacy row."},"isPrimary":{"type":"boolean"},"isVisible":{"type":"boolean","description":"False when the business has hidden the branch from customers: it is live for the business but cannot take a booking through the API or the online calendar."},"timezone":{"type":"string","description":"The IANA zone the branch's hours and its bookings' local times are in. Europe/London for every business today; the field is where a per business zone would appear."},"createdAt":{"type":"string","format":"date-time"}},"required":["id","name","address","city","postcode","phone","email","website","locationModel","isPrimary","isVisible","timezone","createdAt"]},"Staff":{"type":"object","description":"A staff member's partner projection. Never email, phone, gender, pronouns, bio, specialisms, the not bookable reason or commission.","properties":{"id":{"type":"string","format":"uuid","description":"The staffProfileId a booking names."},"name":{"type":"string","description":"The display name, else the first and last names; the identity a booking's staffName snapshot carries."},"initials":{"type":"string"},"colour":{"type":["string","null"],"description":"The calendar colour."},"jobTitle":{"type":["string","null"]},"profileImageUrl":{"type":["string","null"]},"isBookable":{"type":"boolean"},"acceptsOnlineBookings":{"type":"boolean"},"deliveryMode":{"type":"string","enum":["fixed","mobile","both"]},"homeLocationId":{"type":["string","null"],"format":"uuid"},"locationIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Every branch the member works at, the home branch included."},"serviceIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"The services the member is assigned to perform."},"createdAt":{"type":"string","format":"date-time"}},"required":["id","name","initials","colour","jobTitle","profileImageUrl","isBookable","acceptsOnlineBookings","deliveryMode","homeLocationId","locationIds","serviceIds","createdAt"]},"AvailabilitySlot":{"type":"object","properties":{"startTime":{"type":"string","format":"date-time","description":"The slot's start, UTC."},"endTime":{"type":"string","format":"date-time","description":"The start plus the service's duration: the span a booking at this slot will occupy. Buffers between appointments are applied by the search and never appear here."},"localTime":{"type":"string","description":"The start as the business sees it, HH:MM in the response's timezone."},"staff":{"type":"array","description":"Who can take this slot, and the branch each would take it at.","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"locationId":{"type":"string","format":"uuid"}},"required":["id","name","locationId"]}}},"required":["startTime","endTime","localTime","staff"]},"AvailabilityDay":{"type":"object","description":"The date mode: one day of slots.","properties":{"date":{"type":"string","format":"date"},"timezone":{"type":"string"},"serviceId":{"type":"string","format":"uuid"},"slotMinutes":{"type":"integer","description":"The step between candidate starts (15)."},"durationMinutes":{"type":"integer","description":"The service's duration."},"slots":{"type":"array","items":{"$ref":"#/components/schemas/AvailabilitySlot"}},"message":{"type":["string","null"],"description":"Why there are no slots when there are none: the business is closed that day, or no staff member can take the service in the requested mode."}},"required":["date","timezone","serviceId","slotMinutes","durationMinutes","slots","message"]},"AvailabilitySummary":{"type":"object","description":"The summary mode: a status per day over the window.","properties":{"from":{"type":"string","format":"date"},"timezone":{"type":"string"},"serviceId":{"type":"string","format":"uuid"},"days":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string","format":"date"},"status":{"type":"string","enum":["available","fully_booked","closed"]}},"required":["date","status"]}},"message":{"type":["string","null"]}},"required":["from","timezone","serviceId","days","message"]},"AvailabilityWindow":{"type":"object","description":"The slot window mode (detail=slots): the slots per day over up to seven days.","properties":{"from":{"type":"string","format":"date"},"timezone":{"type":"string"},"serviceId":{"type":"string","format":"uuid"},"slotMinutes":{"type":"integer"},"durationMinutes":{"type":"integer"},"days":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string","format":"date"},"status":{"type":"string","enum":["available","fully_booked","closed"]},"slots":{"type":"array","items":{"$ref":"#/components/schemas/AvailabilitySlot"}}},"required":["date","status","slots"]}}},"required":["from","timezone","serviceId","slotMinutes","durationMinutes","days"]},"Booking":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"customerName":{"type":"string"},"customerEmail":{"type":["string","null"]},"customerPhone":{"type":["string","null"]},"serviceName":{"type":"string"},"serviceDuration":{"type":"integer"},"staffName":{"type":"string"},"status":{"type":"string","enum":["pending","confirmed","in_progress","completed","cancelled","no_show"]},"source":{"type":"string","enum":["online","assisted","walk_in","phone","api"],"description":"How the booking was made; api for a booking created through this API (the dashboard shows it as Partner)."},"paymentStatus":{"type":"string","enum":["unpaid","deposit","paid","balance_due"],"description":"unpaid: nothing recorded. deposit: a deposit recorded, the balance owed. paid: paid in full at the price it was paid at; a refund may stand on record. balance_due: paid, then re priced above what was paid; the difference is owed (SEPT42J)."},"startTime":{"type":"string","format":"date-time"},"endTime":{"type":"string","format":"date-time"},"priceInPence":{"type":["integer","null"]},"notes":{"type":["string","null"]},"createdAt":{"type":"string","format":"date-time"},"serviceId":{"type":["string","null"],"format":"uuid","description":"The service booked; null once that service is deleted (the serviceName snapshot remains)."},"staffProfileId":{"type":["string","null"],"format":"uuid","description":"The staff member; null once unassigned (the staffName snapshot remains)."},"locationId":{"type":["string","null"],"format":"uuid","description":"The branch; null on bookings made before branches were recorded."},"updatedAt":{"type":["string","null"],"format":"date-time","description":"When the booking last changed; null until it first changes after creation. The updatedSince filter reads this, else createdAt."},"cancelledAt":{"type":["string","null"],"format":"date-time","description":"When the booking was cancelled; null unless cancelled."}},"required":["id","customerName","customerEmail","customerPhone","serviceName","serviceDuration","staffName","status","source","paymentStatus","startTime","endTime","priceInPence","notes","createdAt","serviceId","staffProfileId","locationId","updatedAt","cancelledAt"]},"CreateBookingRequest":{"type":"object","properties":{"customerName":{"type":"string","description":"The customer name to record on the booking."},"customerEmail":{"type":["string","null"],"description":"Optional customer email address; omit or use null when unavailable."},"customerPhone":{"type":["string","null"],"description":"Optional customer phone number; omit or use null when unavailable."},"serviceId":{"type":"string","format":"uuid","description":"The id of the service to book, from GET /api/v1/services."},"staffProfileId":{"type":"string","format":"uuid","description":"The id of the staff member who will provide the service, from GET /api/v1/staff."},"startTime":{"type":"string","format":"date-time","description":"The appointment start as an ISO 8601 timestamp, including the time zone offset or Z for UTC."},"notes":{"type":["string","null"],"description":"Optional notes to record on the booking; omit or use null when none are needed."},"locationId":{"type":"string","format":"uuid","description":"Optional branch id. Validated when supplied (the branch must offer the service, be live and visible, and the staff member must belong to it; failures return 400 INVALID_LOCATION). When omitted the booking resolves to the single offering branch, else the staff member's home branch, else the primary location."}},"required":["customerName","serviceId","staffProfileId","startTime"]},"CancelBookingRequest":{"type":"object","description":"Optional; an empty body is allowed.","properties":{"reason":{"type":["string","null"],"maxLength":500,"description":"Free text recorded on the booking and its event, up to 500 characters."}}},"RescheduleBookingRequest":{"type":"object","properties":{"startTime":{"type":"string","format":"date-time","description":"The new start, in the future. The end follows from the booking's duration."},"staffProfileId":{"type":"string","format":"uuid","description":"Optional: move the booking to this staff member. One of your staff who works at the booking's branch; the staff name on the booking follows from the profile."}},"required":["startTime"]},"CreateClientRequest":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200,"description":"The name to record for the client, between 1 and 200 characters."},"email":{"type":["string","null"],"maxLength":320,"description":"Stored lower cased; the duplicate check compares it case insensitively."},"phone":{"type":["string","null"],"maxLength":30,"description":"Optional client phone number, up to 30 characters; omit or use null when unavailable."}},"required":["name"]},"WebhookEventType":{"type":"string","enum":["booking.created","booking.confirmed","booking.started","booking.completed","booking.cancelled","booking.no_show","booking.rescheduled","client.created","client.updated","client.archived","payment.succeeded","payment.refunded","payment.refund_failed","ping"],"description":"The catalogue of events an endpoint can subscribe to. ping is a test delivery and is sent regardless of the filter."},"WebhookEndpoint":{"type":"object","description":"A business's webhook endpoint. Never the secret: it is shown once at creation and once at rotation.","properties":{"id":{"type":"string","format":"uuid"},"url":{"type":"string","description":"The https URL deliveries are posted to, as stored (normalised)."},"description":{"type":["string","null"],"description":"Your own label, up to 200 characters."},"events":{"type":"array","items":{"type":"string"},"minItems":1,"description":"[\"*\"] for every event, or the catalogue names subscribed to. ping is always delivered."},"status":{"type":"string","enum":["enabled","disabled"]},"disabledReason":{"type":["string","null"],"enum":["manual","failing","gone",null],"description":"Why a disabled endpoint is disabled: manual (switched off), failing (three days without a successful delivery) or gone (it answered 410). Null while enabled."},"payloadVersion":{"type":"string","enum":["1"],"description":"The payload version pinned at creation. \"1\" today; a breaking change would ship as a new version chosen per endpoint."},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"lastDeliveryAt":{"type":["string","null"],"format":"date-time","description":"When a delivery was last attempted, success or failure."},"lastSuccessAt":{"type":["string","null"],"format":"date-time"},"lastFailureAt":{"type":["string","null"],"format":"date-time"},"secretRotatedAt":{"type":["string","null"],"format":"date-time","description":"When the secret was last rotated; null for a never rotated endpoint."}},"required":["id","url","description","events","status","disabledReason","payloadVersion","createdAt","updatedAt","lastDeliveryAt","lastSuccessAt","lastFailureAt","secretRotatedAt"]},"CreateWebhookEndpointRequest":{"type":"object","properties":{"url":{"type":"string","minLength":12,"maxLength":2048,"description":"https only; no credentials, no fragment; must resolve to a public address. pamprr's own hosts are refused."},"events":{"type":"array","items":{"type":"string"},"minItems":1,"description":"[\"*\"] (the default) or a non empty list of catalogue event names."},"description":{"type":["string","null"],"maxLength":200,"description":"Optional label to help identify this endpoint, up to 200 characters."},"payloadVersion":{"type":"string","enum":["1"],"description":"Optional; \"1\" is the only value today."}},"required":["url"]},"UpdateWebhookEndpointRequest":{"type":"object","description":"Any of the five fields; at least one.","properties":{"url":{"type":"string","minLength":12,"maxLength":2048,"description":"Re checked against the same rules as on create."},"events":{"type":"array","items":{"type":"string"},"minItems":1,"description":"Replace the event filter with [\"*\"] for the whole catalogue or a non empty list of catalogue event names. Omit to keep the current filter."},"description":{"type":["string","null"],"maxLength":200,"description":"Replace the endpoint label, up to 200 characters; use null to clear it or omit to keep it."},"enabled":{"type":"boolean","description":"false disables with reason manual and cancels pending deliveries; true re enables and clears the reason."},"payloadVersion":{"type":"string","enum":["1"],"description":"The payload version to use; \"1\" is the only value today. Omit to keep the current version."}}},"WebhookDeliveryAttempt":{"type":"object","description":"One HTTP attempt of a delivery.","properties":{"attempt":{"type":"integer","minimum":1,"description":"The attempt's number in the delivery's log."},"kind":{"type":"string","enum":["scheduled","test","replay"]},"requestedAt":{"type":"string","format":"date-time"},"durationMs":{"type":["integer","null"]},"statusCode":{"type":["integer","null"],"description":"The endpoint's HTTP status; null on a timeout, a refused URL or a network error."},"error":{"type":["string","null"],"description":"The error class: http_<status>, redirect (a 3xx is never followed), timeout, network, url_refused (the URL no longer resolves to a public address) or secret_unavailable; null on a 2xx."},"responseExcerpt":{"type":["string","null"],"maxLength":1024,"description":"Up to 1 KiB of the endpoint's response body."}},"required":["attempt","kind","requestedAt","durationMs","statusCode","error","responseExcerpt"]},"WebhookDelivery":{"type":"object","description":"One event's delivery to one endpoint, with its attempts. The event's payload is not repeated here.","properties":{"id":{"type":"string","format":"uuid"},"eventId":{"type":"string","pattern":"^evt_[0-9a-f]{24}$","description":"The event id, the webhook-id header on every attempt."},"eventType":{"$ref":"#/components/schemas/WebhookEventType"},"occurredAt":{"type":"string","format":"date-time","description":"When the event happened at pamprr (the envelope's occurredAt)."},"status":{"type":"string","enum":["pending","delivered","failed","cancelled"],"description":"pending (waiting for its next attempt), delivered (a 2xx), failed (the eighth attempt failed, a 410, or a test ping that failed; replayable), cancelled (the endpoint was disabled or deleted while it waited; replayable)."},"attempts":{"type":"integer","minimum":0,"description":"Scheduled attempts made so far (the retry ladder's position); tests and replays do not count."},"nextAttemptAt":{"type":["string","null"],"format":"date-time","description":"When a pending delivery is next attempted; null otherwise."},"lastStatusCode":{"type":["integer","null"]},"lastError":{"type":["string","null"]},"deliveredAt":{"type":["string","null"],"format":"date-time"},"failedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":"string","format":"date-time","description":"When the delivery was created (the log's order)."},"attemptLog":{"type":"array","items":{"$ref":"#/components/schemas/WebhookDeliveryAttempt"},"description":"Every attempt, oldest first."}},"required":["id","eventId","eventType","occurredAt","status","attempts","nextAttemptAt","lastStatusCode","lastError","deliveredAt","failedAt","createdAt","attemptLog"]},"WebhookEvent":{"type":"object","description":"The envelope of every delivery: one document per event, serialised once when the event was picked up and sent byte identical on every attempt and replay, so the signature covers the same bytes each time.","properties":{"id":{"type":"string","pattern":"^evt_[0-9a-f]{24}$","description":"The event id, also the webhook-id header; deduplicate on it, since delivery is at least once."},"type":{"$ref":"#/components/schemas/WebhookEventType"},"apiVersion":{"type":"string","enum":["v1"],"description":"The path prefix whose shapes data carries."},"payloadVersion":{"type":"string","enum":["1"],"description":"The endpoint's pinned payload version; everything within it is additive."},"occurredAt":{"type":"string","format":"date-time","description":"The source row's own time, not the pick up time. Apply events in this order, not arrival order."},"business":{"type":"object","description":"The business the event belongs to, so a partner serving several pamprr businesses can route.","properties":{"id":{"type":"string","format":"uuid"}},"required":["id"]},"data":{"description":"The event's data, one shape per type; each webhooks entry below names its shape.","oneOf":[{"$ref":"#/components/schemas/BookingEventData"},{"$ref":"#/components/schemas/BookingCancelledEventData"},{"$ref":"#/components/schemas/BookingRescheduledEventData"},{"$ref":"#/components/schemas/ClientEventData"},{"$ref":"#/components/schemas/ClientArchivedEventData"},{"$ref":"#/components/schemas/PaymentEventData"},{"$ref":"#/components/schemas/PingEventData"}]}},"required":["id","type","apiVersion","payloadVersion","occurredAt","business","data"]},"BookingEventData":{"type":"object","description":"booking.created, booking.confirmed, booking.started, booking.completed and booking.no_show: the booking as the API answers it, read when the event was picked up.","properties":{"booking":{"$ref":"#/components/schemas/Booking"}},"required":["booking"]},"BookingCancelledEventData":{"type":"object","description":"booking.cancelled: the booking (status cancelled, cancelledAt set), the reason given and who cancelled.","properties":{"booking":{"$ref":"#/components/schemas/Booking"},"reason":{"type":["string","null"],"description":"The reason given at cancellation, or null when none was."},"cancelledBy":{"type":["string","null"],"enum":["consumer","business","system",null],"description":"Who cancelled: the customer, the business, or pamprr itself; null when the record does not say."}},"required":["booking","reason","cancelledBy"]},"BookingRescheduledEventData":{"type":"object","description":"booking.rescheduled: the booking at its new time and the start it moved from.","properties":{"booking":{"$ref":"#/components/schemas/Booking"},"previous":{"type":"object","properties":{"startTime":{"type":["string","null"],"format":"date-time","description":"The start before the move, or null when the record does not carry it."}},"required":["startTime"]}},"required":["booking","previous"]},"ClientEventData":{"type":"object","description":"client.created and client.updated: the client as the API answers it.","properties":{"client":{"$ref":"#/components/schemas/Client"}},"required":["client"]},"ClientArchivedEventData":{"type":"object","description":"client.archived: the client and the fact of the archive.","properties":{"client":{"$ref":"#/components/schemas/Client"},"active":{"type":"boolean","enum":[false],"description":"Always false: the client is archived."}},"required":["client","active"]},"PaymentEventData":{"type":"object","description":"payment.succeeded, payment.refunded and payment.refund_failed: the ledger entry and the booking it belongs to, whose paymentStatus is the state after the entry.","properties":{"payment":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"The ledger entry. Absent on payment.refund_failed, which records no entry."},"bookingId":{"type":"string","format":"uuid"},"amountInPence":{"type":["integer","null"],"description":"The amount in pence, positive for a refund too; null on a refund failure whose amount is not known."},"currency":{"type":"string","description":"The upper case ISO code; GBP for every business today."},"recordedAt":{"type":"string","format":"date-time","description":"When the ledger recorded it."}},"required":["bookingId","amountInPence","currency","recordedAt"]},"booking":{"$ref":"#/components/schemas/Booking"}},"required":["payment","booking"]},"PingEventData":{"type":"object","description":"ping: a test delivery sent from the dashboard or by the API, regardless of the endpoint's filter, never retried.","properties":{"endpointId":{"type":"string","format":"uuid","description":"The endpoint the ping was sent to."},"message":{"type":"string","enum":["A test delivery from pamprr."]}},"required":["endpointId","message"]}}},"x-pamprr-consumer-discovery":{"url":"/api/businesses/openapi.json","description":"The separate consumer search contract, including additive home card footer fields. Consumer authentication and pagination differ from this partner API."}}