The partner API, documented.

Every endpoint of the partner API: a request and a response for each, the errors, the limits, the promises.

The first call

GET /api/v1/business
Authorization: Bearer $PAMPRR_API_KEY

200 OK

{
  "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"
  }
}

Getting started

From a key to the first successful call in three steps.

  1. Mint a key

    In Settings, Operations, API keys, create a key with the Read only access setting, copy it from the one time reveal, and store it in your server's environment as PAMPRR_API_KEY.

  2. Make the first call

    Ask who the key belongs to. The answer is your business record: its id, names, slug, email, time zone and currency.

  3. Then the diary

    List services, staff and locations to learn the ids a booking needs, search availability for a free slot, and, with a key that carries bookings:write, create the booking.

The first call
curl "https://pro.pamprr.me/api/v1/business" \
  -H "Authorization: Bearer $PAMPRR_API_KEY"

Authentication

Every call carries the key as a bearer token; the key is the business.

Send the key as Authorization: Bearer with every request. The API is server to server: keep the key in your backend's environment, never in a browser page or a mobile app.

  • A key is minted by an owner or admin in Settings, Operations, API keys, revealed once, and never stored in plain text by pamprr.
  • The first eight characters after the prefix are what the Settings list shows, so they are safe to quote; the rest is the secret.
  • A missing key, a malformed key and a key that does not verify all receive the same 401 with the code UNAUTHORIZED and no reason.
  • A key that verified but is revoked or expired receives the same code with a reason, because only the holder of the full secret can learn it.
  • Every 401 carries a WWW-Authenticate challenge.

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.

HeaderWhat it says
WWW-AuthenticateRFC 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>".

Access and scopes

A key carries an access setting; each endpoint requires exactly one scope.

The Settings picker offers Read only (the default, every read scope), Full access (every scope) and Custom (read or write per resource).

  • Your business record is always readable.
  • A write scope stores its read scope beside it.
  • A call outside the key's access receives 403 FORBIDDEN with requiredScope naming the scope it lacks; the request was authenticated and counted against the allowance.

Conventions

One set of shapes everywhere.

ConventionThe rule
Requests and responsesJSON. Send Content-Type: application/json on a write. Every response body is a JSON object.
TimesISO 8601 in UTC with a Z suffix. Every business runs on Europe/London today; the business and location records name the zone, and availability answers carry a localTime beside each UTC start.
MoneyIntegers in pence. The business record names the currency; GBP for every business today.
IdsUUIDs. A path id that is not a UUID, or that names another business's record, answers 404.
NamescamelCase fields. Absent values are null, never omitted.
EnvelopesA list answers { <resource>: [...], nextCursor }; a single record answers { business }, { booking } or { client }; a cancel adds alreadyCancelled and a client create may add existing; the availability search and the eligible staff answer their own object.
HostOne host, https://pro.pamprr.me, and one version prefix, /api/v1, carried by every path.

Errors

One envelope on every 4xx and 5xx; branch on the code, never on the sentence.

Every error body is { error, code, requestId }: a sentence for a human, the word your software branches on, and the id to quote to support. Some codes add a field beside those three, shown in the catalogue below.

StatusMeaning on this API
200The call worked: a read, a change, or a client that already existed.
201A booking or a client was created.
400The request is malformed, a value is out of bounds, a cursor is bad, an idempotency key is malformed, or a branch is not usable.
401No key, a key that does not verify, or a key that is revoked or expired.
402The booking must be made through the consumer booking flow: a deposit or a saved card is required.
403The key verified but lacks the scope the endpoint requires.
404Not yours, gone, archived, or not a UUID; the API never says which.
409The state refuses the change: a taken or held slot, a terminal booking, or a retry still in flight.
422The idempotency key was already used for a different request.
429Too many requests: the allowance, or abuse protection.
500Something went wrong at pamprr; quote the request id.
CodeStatusWhenExample body
INVALID_JSON400The body is not JSON{"error":"Request body must be valid JSON.","code":"INVALID_JSON","requestId":"req_000000000000000000000001"}
VALIDATION_FAILED400A value fails its rule; field names it where one is named{"error":"locationId must be a UUID.","code":"VALIDATION_FAILED","field":"locationId","requestId":"req_000000000000000000000002"}
INVALID_START_TIME400startTime does not parse{"error":"Booking start time is not a valid ISO date.","code":"INVALID_START_TIME","requestId":"req_000000000000000000000003"}
PAST_START_TIME400startTime is in the past{"error":"Booking start time is in the past.","code":"PAST_START_TIME","requestId":"req_000000000000000000000004"}
INVALID_LOCATION400The branch does not offer the service{"error":"This service is not offered at the requested location.","code":"INVALID_LOCATION","requestId":"req_000000000000000000000005"}
INVALID_DATE_RANGE400A date bound does not parse{"error":"startDate and endDate must be valid ISO dates.","code":"INVALID_DATE_RANGE","requestId":"req_000000000000000000000006"}
INVALID_CURSOR400The cursor does not decode or belongs to other filters{"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_INVALID400The Idempotency-Key is malformed{"error":"Idempotency-Key must be 1 to 255 printable ASCII characters; a UUID is recommended.","code":"IDEMPOTENCY_KEY_INVALID","requestId":"req_000000000000000000000008"}
UNAUTHORIZED401No key, or a key that does not verify: the one collapsed refusal{"error":"Invalid API key.","code":"UNAUTHORIZED","requestId":"req_00000000000000000000000a"}
DEPOSIT_REQUIRED402The service's effective booking protection includes a deposit and Stripe is live{"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"}
CARD_CAPTURE_REQUIRED402The service's effective booking protection is card capture and Stripe is live{"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"}
FORBIDDEN403A read only key attempting a write{"error":"This API key does not have the bookings:write scope.","code":"FORBIDDEN","requiredScope":"bookings:write","requestId":"req_00000000000000000000000d"}
NOT_FOUND404Not yours, gone, archived, or not a UUID; the API never says which{"error":"Booking not found.","code":"NOT_FOUND","requestId":"req_000000000000000000000010"}
BOOKING_OVERLAP409Another booking has the slot{"error":"This time slot is already booked.","code":"BOOKING_OVERLAP","requestId":"req_000000000000000000000011"}
BOOKING_COMPLETED409A completed booking cannot be cancelled or rescheduled through the API{"error":"This booking is completed and cannot be cancelled through the API.","code":"BOOKING_COMPLETED","requestId":"req_000000000000000000000013"}
BOOKING_NO_SHOW409A no show booking cannot be cancelled or rescheduled through the API{"error":"This booking was marked as a no show and cannot be cancelled through the API.","code":"BOOKING_NO_SHOW","requestId":"req_000000000000000000000014"}
BOOKING_CANCELLED409A cancelled booking cannot be rescheduled{"error":"This booking is cancelled and cannot be rescheduled.","code":"BOOKING_CANCELLED","requestId":"req_000000000000000000000015"}
SLOT_HELD409A customer is mid checkout for the slot{"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"}
IDEMPOTENCY_REQUEST_IN_PROGRESS409A retry while the first request is still running{"error":"A request with this idempotency key is still being processed. Retry shortly.","code":"IDEMPOTENCY_REQUEST_IN_PROGRESS","requestId":"req_000000000000000000000012"}
WEBHOOK_LIMIT_REACHED409Ten live endpoints already{"error":"This business already has 10 webhook endpoints.","code":"WEBHOOK_LIMIT_REACHED","requestId":"req_00000000000000000000001c"}
WEBHOOK_ENDPOINT_DISABLED409A test on a disabled endpoint{"error":"This endpoint is disabled (failing). Enable it before sending a test.","code":"WEBHOOK_ENDPOINT_DISABLED","requestId":"req_00000000000000000000001d"}
WEBHOOK_DELIVERY_PENDING409A replay of a delivery still on the ladder{"error":"This delivery is still pending and will be attempted on schedule.","code":"WEBHOOK_DELIVERY_PENDING","requestId":"req_00000000000000000000001e"}
IDEMPOTENCY_KEY_REUSED422The same key with a different request{"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"}
RATE_LIMIT_EXCEEDED429The key's sliding sixty second allowance is spent; X-RateLimit-Limit is present{"error":"Rate limit exceeded.","code":"RATE_LIMIT_EXCEEDED","requestId":"req_000000000000000000000019"}
INTERNAL_ERROR500An unexpected failure; the body carries no internal detail{"error":"Could not list bookings. Please try again.","code":"INTERNAL_ERROR","requestId":"req_00000000000000000000001b"}

Rate limits

One allowance per key over a sliding sixty second window, stated on every authenticated response.

HeaderWhat it says
X-RateLimit-LimitThe 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-RemainingRequests left in the current window after this one; 0 on a 429. Every authenticated request counts, errors and idempotent replays included.
X-RateLimit-ResetISO 8601 UTC timestamp by which the window will have fully cleared. An upper bound, not the exact moment one slot frees.
Retry-AfterWhole seconds to wait before retrying, rounded up. Present on every 429.

Pagination

The lists page by an opaque cursor; the eligible staff answer is the whole set.

limit: Rows per page, 1 to 100 (a value above 100 is treated as 100).

cursor: 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.

nextCursor: Pass back as cursor to fetch the next page; null when this page was the last.

Idempotency

A write retried with the same key replays the first outcome.

Idempotency-Key: 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.

HeaderWhat it says
Idempotent-ReplayedPresent, 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-IdOn a replay: the id of the request that did the work. X-Request-Id names this request.

Request ids

Every response names its request.

HeaderWhat it says
X-Request-IdServer generated id for this request, present on every response. Quote it to support; it is meaningless otherwise.

requestId: The same id as the X-Request-Id header. Quote it to support.

Business

The record the key belongs to.

The Business object

FieldTypeAlwaysMeaning
iduuidyes
namestringyes
slugstringyes
displayNamestringyes
emailstring or nullyes
timezonestringyesThe IANA zone the business runs on; Europe/London for every business today. Render local times with it; every time in this API is UTC.
currencystringyesThe ISO 4217 code every price is in; GBP for every business today. Prices are in pence.
GET

/api/v1/business

business:read

The business the key belongs to.

No parameters: the key selects the business.

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).

Request
curl "https://pro.pamprr.me/api/v1/business" \
  -H "Authorization: Bearer $PAMPRR_API_KEY"
Response 200

The business the key belongs to

  • X-Request-Id
  • X-RateLimit-Limit
  • X-RateLimit-Remaining
  • X-RateLimit-Reset
{
  "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"
  }
}

Locations

The business's branches.

The Location object

A branch of the business. Never the whole row: no coordinates, catchment, travel or hours columns.

FieldTypeAlwaysMeaning
iduuidyes
namestringyes
addressstring or nullyes
citystring or nullyes
postcodestring or nullyes
phonestring or nullyesThe branch's public contact, as shown to customers.
emailstring or nullyes
websitestring or nullyes
locationModelfixed | mobile | both or nullyesWhether the branch takes customers at its address, travels to them, or both. Null on a legacy row.
isPrimarybooleanyes
isVisiblebooleanyesFalse 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.
timezonestringyesThe 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.
createdAtdate-timeyes
GET

/api/v1/locations

locations:read

The business's branches, live ones only, the primary first.

NameWhereTypeRequiredMeaning
limitqueryinteger (1 to 100, default 50)noRows per page, 1 to 100 (a value above 100 is treated as 100). Example: 50
cursorquerystringnoOpaque 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

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.

Request
curl "https://pro.pamprr.me/api/v1/locations?limit=50" \
  -H "Authorization: Bearer $PAMPRR_API_KEY"
Response 200

Three branches, the primary first, one hidden from customers

  • X-Request-Id
  • X-RateLimit-Limit
  • X-RateLimit-Remaining
  • X-RateLimit-Reset
{
  "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
}

Staff

The roster's partner projection.

The Staff object

A staff member's partner projection. Never email, phone, gender, pronouns, bio, specialisms, the not bookable reason or commission.

FieldTypeAlwaysMeaning
iduuidyesThe staffProfileId a booking names.
namestringyesThe display name, else the first and last names; the identity a booking's staffName snapshot carries.
initialsstringyes
colourstring or nullyesThe calendar colour.
jobTitlestring or nullyes
profileImageUrlstring or nullyes
isBookablebooleanyes
acceptsOnlineBookingsbooleanyes
deliveryModefixed | mobile | bothyes
homeLocationIduuid or nullyes
locationIdsarray of uuidyesEvery branch the member works at, the home branch included.
serviceIdsarray of uuidyesThe services the member is assigned to perform.
createdAtdate-timeyes
GET

/api/v1/staff

staff:read

The roster's partner projection.

NameWhereTypeRequiredMeaning
limitqueryinteger (1 to 100, default 50)noRows per page, 1 to 100 (a value above 100 is treated as 100). Example: 50
cursorquerystringnoOpaque 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
locationIdqueryuuidnoOnly 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

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.

Request
curl "https://pro.pamprr.me/api/v1/staff?locationId=00000000-0000-4000-8000-000000000c01" \
  -H "Authorization: Bearer $PAMPRR_API_KEY"
Response 200

The members of one branch

  • X-Request-Id
  • X-RateLimit-Limit
  • X-RateLimit-Remaining
  • X-RateLimit-Reset
{
  "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
}

Services

The menu, and who performs each service.

The Service object

FieldTypeAlwaysMeaning
iduuidyes
namestringyes
descriptionstring or nullyes
durationMinutesintegeryes
priceInPenceintegeryes
isActivebooleanyes
isOnlineBookablebooleanyes
isAddOnbooleanyes
locationIdsarray of uuidyesBranch ids the service is offered at, from the service_locations junction (Catalogue Stage 1). Always present; empty when the service has no offering rows.

The EligibleStaff object

The members who can perform one service, and where.

FieldTypeAlwaysMeaning
serviceIduuidyes
serviceNamestringyes
locationIduuid or nullyesThe branch asked for; null when the answer covers every offering branch.
staffarray of objects (id, name, initials, colour, jobTitle, profileImageUrl, isBookable, acceptsOnlineBookings, deliveryMode, locationIds)yes
GET

/api/v1/services

services:read

The business's menu in its own order.

NameWhereTypeRequiredMeaning
limitqueryinteger (1 to 100, default 50)noRows per page, 1 to 100 (a value above 100 is treated as 100). Example: 50
cursorquerystringnoOpaque 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

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.

Request
curl "https://pro.pamprr.me/api/v1/services?limit=50" \
  -H "Authorization: Bearer $PAMPRR_API_KEY"
Response 200

The first page of the menu, a second page to follow

  • X-Request-Id
  • X-RateLimit-Limit
  • X-RateLimit-Remaining
  • X-RateLimit-Reset
{
  "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"
}
GET

/api/v1/services/{id}/eligible-staff

services:read

Who can perform a service, and at which branches.

NameWhereTypeRequiredMeaning
idpathuuidyesThe 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
locationIdqueryuuidnoOnly 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

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.

Request
curl "https://pro.pamprr.me/api/v1/services/00000000-0000-4000-8000-0000000000e2/eligible-staff" \
  -H "Authorization: Bearer $PAMPRR_API_KEY"
Response 200

Every bookable member assigned to the service, with the branches each can perform it at

  • X-Request-Id
  • X-RateLimit-Limit
  • X-RateLimit-Remaining
  • X-RateLimit-Reset
{
  "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"
      ]
    }
  ]
}

Availability

The bookable slots, computed live from the business's own calendar rules.

The AvailabilityDay object

The date mode: one day of slots.

FieldTypeAlwaysMeaning
datedateyes
timezonestringyes
serviceIduuidyes
slotMinutesintegeryesThe step between candidate starts (15).
durationMinutesintegeryesThe service's duration.
slotsarray of AvailabilitySlotyes
messagestring or nullyesWhy 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.

The AvailabilitySlot object

FieldTypeAlwaysMeaning
startTimedate-timeyesThe slot's start, UTC.
endTimedate-timeyesThe 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.
localTimestringyesThe start as the business sees it, HH:MM in the response's timezone.
staffarray of objects (id, name, locationId)yesWho can take this slot, and the branch each would take it at.

The AvailabilitySummary object

The summary mode: a status per day over the window.

FieldTypeAlwaysMeaning
fromdateyes
timezonestringyes
serviceIduuidyes
daysarray of objects (date, status)yes
messagestring or nullyes

The AvailabilityWindow object

The slot window mode (detail=slots): the slots per day over up to seven days.

FieldTypeAlwaysMeaning
fromdateyes
timezonestringyes
serviceIduuidyes
slotMinutesintegeryes
durationMinutesintegeryes
daysarray of objects (date, status, slots)yes
GET

/api/v1/availability

availability:read

The bookable slots for a service, computed live from the same rules the business's own online booking calendar uses.

NameWhereTypeRequiredMeaning
serviceIdqueryuuidyesThe service. Must be active and offered at a branch customers can see; otherwise 404 NOT_FOUND. Example: 00000000-0000-4000-8000-0000000000e2
datequerydatenoOne day of slots (YYYY-MM-DD). Send date or from, never both. Example: 2026-10-06
fromquerydatenoThe first day of a window (YYYY-MM-DD). Example: 2026-10-05
daysqueryinteger (1 to 31)noWith from: the window's length. 1 to 31 for the summary (default 14); 1 to 7 with detail=slots (default 7). Example: 7
detailquerysummary | slotsnoWith from: summary (a status per day, the default) or slots (the slots per day, days capped at 7). Example: slots
staffProfileIdqueryuuidnoOnly this staff member's slots. Must be one of your staff; otherwise 404 NOT_FOUND. Example: 00000000-0000-4000-8000-0000000000a2
locationIdqueryuuidnoOnly 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
deliveryModequeryfixed | mobilenoRestrict 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

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.

Request: One day of slots
curl "https://pro.pamprr.me/api/v1/availability?serviceId=00000000-0000-4000-8000-0000000000e2&date=2026-10-06" \
  -H "Authorization: Bearer $PAMPRR_API_KEY"
Request: A status per day
curl "https://pro.pamprr.me/api/v1/availability?serviceId=00000000-0000-4000-8000-0000000000e2&from=2026-10-05&days=7" \
  -H "Authorization: Bearer $PAMPRR_API_KEY"
Request: The slots per day for one therapist
curl "https://pro.pamprr.me/api/v1/availability?serviceId=00000000-0000-4000-8000-0000000000e2&from=2026-10-05&days=7&detail=slots&staffProfileId=00000000-0000-4000-8000-0000000000a2" \
  -H "Authorization: Bearer $PAMPRR_API_KEY"
Response 200

date: one day of slots

  • X-Request-Id
  • X-RateLimit-Limit
  • X-RateLimit-Remaining
  • X-RateLimit-Reset
{
  "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
}

Bookings

The diary: list, create, read, cancel and reschedule.

The Booking object

FieldTypeAlwaysMeaning
iduuidyes
customerNamestringyes
customerEmailstring or nullyes
customerPhonestring or nullyes
serviceNamestringyes
serviceDurationintegeryes
staffNamestringyes
statuspending | confirmed | in_progress | completed | cancelled | no_showyes
sourceonline | assisted | walk_in | phone | apiyesHow the booking was made; api for a booking created through this API (the dashboard shows it as Partner).
paymentStatusunpaid | deposit | paid | balance_dueyesunpaid: 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).
startTimedate-timeyes
endTimedate-timeyes
priceInPenceinteger or nullyes
notesstring or nullyes
createdAtdate-timeyes
serviceIduuid or nullyesThe service booked; null once that service is deleted (the serviceName snapshot remains).
staffProfileIduuid or nullyesThe staff member; null once unassigned (the staffName snapshot remains).
locationIduuid or nullyesThe branch; null on bookings made before branches were recorded.
updatedAtdate-time or nullyesWhen the booking last changed; null until it first changes after creation. The updatedSince filter reads this, else createdAt.
cancelledAtdate-time or nullyesWhen the booking was cancelled; null unless cancelled.
GET

/api/v1/bookings

bookings:read

The bookings in a start time window, oldest first.

NameWhereTypeRequiredMeaning
startDatequerydatenoInclusive lower bound on start_time. Defaults to 30 days ago. Example: 2026-10-05
endDatequerydatenoInclusive upper bound on start_time. Defaults to 90 days from now. Example: 2026-10-11
statusquerypending | confirmed | in_progress | completed | cancelled | no_show | allnoOnly this status; all for every status. Absent: every status but cancelled. Any other word is refused with 400 VALIDATION_FAILED. Example: all
updatedSincequerydate-timenoOnly 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
staffProfileIdqueryuuidnoOnly this staff member's bookings. A value that is not a UUID is refused with 400 VALIDATION_FAILED. Example: 00000000-0000-4000-8000-0000000000a1
locationIdqueryuuidnoOnly this branch's bookings. A value that is not a UUID is refused with 400 VALIDATION_FAILED. Example: 00000000-0000-4000-8000-000000000c01
limitqueryinteger (1 to 100, default 50)noRows per page, 1 to 100 (a value above 100 is treated as 100). Example: 50
cursorquerystringnoOpaque 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

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.

Request: A date window
curl "https://pro.pamprr.me/api/v1/bookings?startDate=2026-10-05&endDate=2026-10-11&limit=50" \
  -H "Authorization: Bearer $PAMPRR_API_KEY"
Request: A sync poll: everything changed since the last poll, or subscribe to webhooks and poll only to catch up
curl "https://pro.pamprr.me/api/v1/bookings?startDate=2026-10-05&endDate=2026-10-11&status=all&updatedSince=2026-10-06T08%3A00%3A00Z" \
  -H "Authorization: Bearer $PAMPRR_API_KEY"
Request: The next page, the same filters and the cursor

The cursor shown is the document's example; pass back the nextCursor you were given, with the same filters.

curl "https://pro.pamprr.me/api/v1/bookings?startDate=2026-10-05&endDate=2026-10-11&limit=50&cursor=eyJ2IjoyLCJsIjoiYm9va2luZ3MiLCJrIjpbIjIwMjYtMTAtMDZUMDk6MTU6MDAuMDAwWiIsIjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDBiMiJdLCJmIjoiNWQ0MTQwMmEifQ" \
  -H "Authorization: Bearer $PAMPRR_API_KEY"
Response 200

Two bookings in the window, a second page to follow

  • X-Request-Id
  • X-RateLimit-Limit
  • X-RateLimit-Remaining
  • X-RateLimit-Reset
{
  "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"
}
POST

/api/v1/bookings

bookings:writeIdempotency-Key accepted

Creates a booking on behalf of the calling business, confirmed and unpaid, recorded with the source api (the dashboard shows it as Partner).

NameWhereTypeRequiredMeaning
Idempotency-Keyheaderstring (1 to 255 characters)noOptional 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
customerNamebodystringyesThe customer name to record on the booking.
customerEmailbodystring or nullnoOptional customer email address; omit or use null when unavailable.
customerPhonebodystring or nullnoOptional customer phone number; omit or use null when unavailable.
serviceIdbodyuuidyesThe id of the service to book, from GET /api/v1/services.
staffProfileIdbodyuuidyesThe id of the staff member who will provide the service, from GET /api/v1/staff.
startTimebodydate-timeyesThe appointment start as an ISO 8601 timestamp, including the time zone offset or Z for UTC.
notesbodystring or nullnoOptional notes to record on the booking; omit or use null when none are needed.
locationIdbodyuuidnoOptional 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.

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.

Request

curl shows the document's example Idempotency-Key; send a fresh UUID with every new request, as the other two samples do.

curl -X POST "https://pro.pamprr.me/api/v1/bookings" \
  -H "Authorization: Bearer $PAMPRR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 0b3f6c2e-8f0a-4d5b-9c1e-7a2b3c4d5e6f" \
  -d '{"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"}'
Response 201

The booking as created: confirmed, unpaid, source api

  • X-Request-Id
  • Idempotent-Replayed
  • X-Original-Request-Id
  • X-RateLimit-Limit
  • X-RateLimit-Remaining
  • X-RateLimit-Reset
{
  "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
  }
}
GET

/api/v1/bookings/{id}

bookings:read

One booking by id, the Booking projection: the same fields as a list row.

NameWhereTypeRequiredMeaning
idpathuuidyesThe booking id. Example: 00000000-0000-4000-8000-0000000000b3

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.

Request
curl "https://pro.pamprr.me/api/v1/bookings/00000000-0000-4000-8000-0000000000b3" \
  -H "Authorization: Bearer $PAMPRR_API_KEY"
Response 200

One booking by id, the same projection as a list row

  • X-Request-Id
  • X-RateLimit-Limit
  • X-RateLimit-Remaining
  • X-RateLimit-Reset
{
  "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
  }
}
POST

/api/v1/bookings/{id}/cancel

bookings:writeIdempotency-Key accepted

Cancels a booking the way the business cancels one in its dashboard.

NameWhereTypeRequiredMeaning
idpathuuidyesThe booking id. Example: 00000000-0000-4000-8000-0000000000b3
Idempotency-Keyheaderstring (1 to 255 characters)noOptional 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
reasonbodystring or nullnoFree text recorded on the booking and its event, up to 500 characters.

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.

Request

curl shows the document's example Idempotency-Key; send a fresh UUID with every new request, as the other two samples do.

curl -X POST "https://pro.pamprr.me/api/v1/bookings/00000000-0000-4000-8000-0000000000b3/cancel" \
  -H "Authorization: Bearer $PAMPRR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 4e7d1c2b-8a9f-4b3e-a1c5-6d7e8f9a0b1c" \
  -d '{"reason":"Guest checked out early"}'
Response 200

The booking cancelled by this call

  • X-Request-Id
  • Idempotent-Replayed
  • X-Original-Request-Id
  • X-RateLimit-Limit
  • X-RateLimit-Remaining
  • X-RateLimit-Reset
{
  "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
}
POST

/api/v1/bookings/{id}/reschedule

bookings:writeIdempotency-Key accepted

Moves a booking to a new start time, and optionally to another staff member, the way the business does in its dashboard.

NameWhereTypeRequiredMeaning
idpathuuidyesThe booking id. Example: 00000000-0000-4000-8000-0000000000b3
Idempotency-Keyheaderstring (1 to 255 characters)noOptional 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
startTimebodydate-timeyesThe new start, in the future. The end follows from the booking's duration.
staffProfileIdbodyuuidnoOptional: 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.

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.

Request

curl shows the document's example Idempotency-Key; send a fresh UUID with every new request, as the other two samples do.

curl -X POST "https://pro.pamprr.me/api/v1/bookings/00000000-0000-4000-8000-0000000000b3/reschedule" \
  -H "Authorization: Bearer $PAMPRR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 9c2a5e71-4d0b-4f8e-9a6c-2b1d3e4f5a6b" \
  -d '{"startTime":"2026-10-07T09:15:00.000Z","staffProfileId":"00000000-0000-4000-8000-0000000000a1"}'
Response 200

The booking at its new time; updatedAt set

  • X-Request-Id
  • Idempotent-Replayed
  • X-Original-Request-Id
  • X-RateLimit-Limit
  • X-RateLimit-Remaining
  • X-RateLimit-Reset
{
  "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
  }
}

Clients

The client book: list, create and read.

The Client object

FieldTypeAlwaysMeaning
iduuidyes
namestringyes
emailstring or nullyes
phonestring or nullyes
createdAtdate-timeyes
updatedAtdate-time or nullyesWhen the client record last changed; null until it first changes after creation.
GET

/api/v1/clients

clients:read

The business's active clients by name.

NameWhereTypeRequiredMeaning
searchquerystring (2 to 100 characters)noOnly 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
limitqueryinteger (1 to 100, default 50)noRows per page, 1 to 100 (a value above 100 is treated as 100). Example: 50
cursorquerystringnoOpaque 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

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.

Request
curl "https://pro.pamprr.me/api/v1/clients?search=example&limit=50" \
  -H "Authorization: Bearer $PAMPRR_API_KEY"
Response 200

The active clients matching the search, in name order

  • X-Request-Id
  • X-RateLimit-Limit
  • X-RateLimit-Remaining
  • X-RateLimit-Reset
{
  "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
}
POST

/api/v1/clients

clients:writeIdempotency-Key accepted

Creates a client record for the business, or returns the existing one.

NameWhereTypeRequiredMeaning
Idempotency-Keyheaderstring (1 to 255 characters)noOptional 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
namebodystring (1 to 200 characters)yesThe name to record for the client, between 1 and 200 characters.
emailbodystring or nullnoStored lower cased; the duplicate check compares it case insensitively.
phonebodystring or nullnoOptional client phone number, up to 30 characters; omit or use null when unavailable.

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.

Request

curl shows the document's example Idempotency-Key; send a fresh UUID with every new request, as the other two samples do.

curl -X POST "https://pro.pamprr.me/api/v1/clients" \
  -H "Authorization: Bearer $PAMPRR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 7b8c9d0e-1f2a-4b3c-8d4e-5f6a7b8c9d0e" \
  -d '{"name":"Robin Example","email":"Robin@Example.test","phone":"+44 7700 900456"}'
Response 200

An active client already had that email (or an archived one was restored)

  • X-Request-Id
  • Idempotent-Replayed
  • X-Original-Request-Id
  • X-RateLimit-Limit
  • X-RateLimit-Remaining
  • X-RateLimit-Reset
{
  "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
}
Response 201

A new client record

  • X-Request-Id
  • Idempotent-Replayed
  • X-Original-Request-Id
  • X-RateLimit-Limit
  • X-RateLimit-Remaining
  • X-RateLimit-Reset
{
  "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
  }
}
GET

/api/v1/clients/{id}

clients:read

One client by id, the Client projection: the same fields as a list row.

NameWhereTypeRequiredMeaning
idpathuuidyesThe client id. Example: 00000000-0000-4000-8000-0000000000c1

An archived client, a client that is not yours, or an id that is not a UUID is 404 NOT_FOUND.

Request
curl "https://pro.pamprr.me/api/v1/clients/00000000-0000-4000-8000-0000000000c1" \
  -H "Authorization: Bearer $PAMPRR_API_KEY"
Response 200

One client by id

  • X-Request-Id
  • X-RateLimit-Limit
  • X-RateLimit-Remaining
  • X-RateLimit-Reset
{
  "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
  }
}

Webhooks

pamprr tells you when a booking, a client or a payment changes; signed, retried, logged.

Register an https endpoint in Settings, Operations, Webhooks, or through the endpoints below; copy its signing secret once; subscribe to every event or to a list. Each delivery is one signed POST carrying one event.

Events

Fourteen event types across bookings, clients and payments.

  • An endpoint subscribes to every event with ["*"] or to a list of catalogue names; ping is delivered to every endpoint regardless of its filter.
  • A booking event fires for a booking from any source: the online calendar, the dashboard, the app and this API alike.
  • A payment event is read from the ledger, so it fires for a deposit, a saved card charge, a till payment or a manual record; the booking it carries shows the paymentStatus after the entry.
  • A client event is read from the client book: a create, an update (a merge included) and an archive.
  • payment.refund_failed can arrive without a preceding payment.refunded: a refund reversed before its first pick up is read as its failure alone, and the event carries the amount and the booking on its own.
TypeWhen it firesWhat data carries
booking.createdA booking was created, from any source: the online calendar, the dashboard, the app or this API.data.booking is the booking as the API answers it, read when the event was picked up; source says where it came from.
booking.confirmedA pending booking was confirmed.data.booking with status confirmed.
booking.startedA booking was marked in progress.data.booking with status in_progress.
booking.completedA booking was marked completed.data.booking with status completed; paymentStatus is the state at pick up.
booking.cancelledA booking was cancelled by the customer, the business or the system.data.booking with status cancelled and cancelledAt set, data.reason and data.cancelledBy.
booking.no_showA booking was marked a no show.data.booking with status no_show.
booking.rescheduledA booking was moved to a new time.data.booking at its new time and data.previous.startTime, the start it moved from.
client.createdA client was added to the client book, by any route.data.client as the API answers it.
client.updatedA client's details changed, a merge included.data.client after the change, with updatedAt set.
client.archivedA client was archived.data.client and data.active false.
payment.succeededA payment for a booking was recorded in the ledger: a deposit, a saved card charge, a till payment or a manual record.data.payment (the entry, the amount in pence, the currency, when it was recorded) and data.booking with the paymentStatus after the entry.
payment.refundedA refund for a booking was recorded in the ledger.data.payment with amountInPence the refunded amount, positive, and data.booking.
payment.refund_failedA refund attempt for a booking failed.data.payment without an id (no ledger entry), amountInPence the attempted amount or null, and data.booking.
pingA test delivery, sent from the dashboard or by the test operation, regardless of the endpoint's filter and never retried.data.endpointId and data.message.

Signing

Every delivery carries three Standard Webhooks headers; verify before you trust the body.

  • The signed content is the event id, a full stop, the timestamp, a full stop, the raw body bytes; the key is the base64 decoded secret after whsec_; the algorithm is HMAC SHA256; the signature is v1, then base64.
  • During the day after a rotation every delivery carries two signatures, one per secret, so a partner verifying with either passes.
  • The document's example deliveries are signed for real: each webhooks entry's example body, serialised compactly, verifies under its example headers with the example secret the create endpoint answers.
HeaderWhat it says
webhook-idThe event id; the same on every attempt and replay of this event, so a partner can deduplicate.
webhook-timestampUnix seconds when this attempt was signed; refuse a delivery whose timestamp is more than five minutes from now.
webhook-signatureOne 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.
  1. Read webhook-id, webhook-timestamp and webhook-signature; refuse the delivery if any is missing.

  2. Refuse it if webhook-timestamp is more than five minutes from now, either way: the replay window.

  3. Compute HMAC SHA256 over id, a full stop, timestamp, a full stop, the raw body, with each secret you hold (the current one; during a rotation the old one too), and accept if any space separated v1 signature equals yours by a constant time comparison.

  4. Deduplicate on webhook-id: at least once delivery means the same event can arrive twice.

  5. Answer 2xx as fast as you can; do your work after acknowledging.

Verify a delivery
// npm install standardwebhooks
import { Webhook } from "standardwebhooks";

const webhook = new Webhook(process.env.PAMPRR_WEBHOOK_SECRET);

export async function handle(request) {
  const body = await request.text();
  const headers = {
    "webhook-id": request.headers.get("webhook-id"),
    "webhook-timestamp": request.headers.get("webhook-timestamp"),
    "webhook-signature": request.headers.get("webhook-signature"),
  };
  let event;
  try {
    event = webhook.verify(body, headers);
  } catch {
    return new Response("bad signature", { status: 400 });
  }
  if (await alreadyHandled(event.id)) return new Response(null, { status: 200 });
  await enqueue(event);
  return new Response(null, { status: 200 });
}

Delivery and retries

At least once, usually within a minute of the change, retried over about two days.

  • Delivery is at least once: a retry, a replay or a send that died mid flight can repeat an event under the same webhook-id; treat a repeated id as already handled.
  • Apply events in occurredAt order, not arrival order: a retry, a burst or two runs overlapping can reorder deliveries.
  • A 2xx within ten seconds marks the delivery delivered. Anything else (a 3xx, which is never followed, a 4xx, a 5xx, a timeout) schedules the next attempt after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 24 hours: eight attempts over about 44 hours 36 minutes, then the delivery is a dead letter, visible in the log and replayable.
  • A 429 with Retry-After is honoured in place of the ladder's delay, up to twenty four hours.
  • A 410 marks the delivery failed and disables the endpoint at once with the reason gone; three days without a successful delivery disable it with the reason failing. Re enable it in Settings or with the update endpoint.
  • While an endpoint is disabled no events are queued for it: catch up by polling the API, then replay dead letters from the log.
  • Answer fast: acknowledge with a 2xx, then do the work. The log keeps thirty days of deliveries with every attempt's status code, duration, error class and the first kilobyte of the response.

Endpoints

Signed event deliveries to endpoints the business registers: the endpoints, their secrets, the test and the delivery log.

The WebhookEndpoint object

A business's webhook endpoint. Never the secret: it is shown once at creation and once at rotation.

FieldTypeAlwaysMeaning
iduuidyes
urlstringyesThe https URL deliveries are posted to, as stored (normalised).
descriptionstring or nullyesYour own label, up to 200 characters.
eventsarray of stringyes["*"] for every event, or the catalogue names subscribed to. ping is always delivered.
statusenabled | disabledyes
disabledReasonmanual | failing | gone or nullyesWhy a disabled endpoint is disabled: manual (switched off), failing (three days without a successful delivery) or gone (it answered 410). Null while enabled.
payloadVersion1yesThe payload version pinned at creation. "1" today; a breaking change would ship as a new version chosen per endpoint.
createdAtdate-timeyes
updatedAtdate-timeyes
lastDeliveryAtdate-time or nullyesWhen a delivery was last attempted, success or failure.
lastSuccessAtdate-time or nullyes
lastFailureAtdate-time or nullyes
secretRotatedAtdate-time or nullyesWhen the secret was last rotated; null for a never rotated endpoint.

The WebhookDelivery object

One event's delivery to one endpoint, with its attempts. The event's payload is not repeated here.

FieldTypeAlwaysMeaning
iduuidyes
eventIdstringyesThe event id, the webhook-id header on every attempt.
eventTypeWebhookEventTypeyesThe catalogue of events an endpoint can subscribe to. ping is a test delivery and is sent regardless of the filter.
occurredAtdate-timeyesWhen the event happened at pamprr (the envelope's occurredAt).
statuspending | delivered | failed | cancelledyespending (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).
attemptsintegeryesScheduled attempts made so far (the retry ladder's position); tests and replays do not count.
nextAttemptAtdate-time or nullyesWhen a pending delivery is next attempted; null otherwise.
lastStatusCodeinteger or nullyes
lastErrorstring or nullyes
deliveredAtdate-time or nullyes
failedAtdate-time or nullyes
createdAtdate-timeyesWhen the delivery was created (the log's order).
attemptLogarray of WebhookDeliveryAttemptyesEvery attempt, oldest first.

The WebhookEventType object

The catalogue of events an endpoint can subscribe to. ping is a test delivery and is sent regardless of the filter.

FieldTypeAlwaysMeaning

The WebhookDeliveryAttempt object

One HTTP attempt of a delivery.

FieldTypeAlwaysMeaning
attemptintegeryesThe attempt's number in the delivery's log.
kindscheduled | test | replayyes
requestedAtdate-timeyes
durationMsinteger or nullyes
statusCodeinteger or nullyesThe endpoint's HTTP status; null on a timeout, a refused URL or a network error.
errorstring or nullyesThe 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.
responseExcerptstring or nullyesUp to 1 KiB of the endpoint's response body.
GET

/api/v1/webhooks

webhooks:manage

The business's live webhook endpoints, in creation order, the whole set (at most ten; no cursor).

No parameters: the key selects the business.

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.

Request
curl "https://pro.pamprr.me/api/v1/webhooks" \
  -H "Authorization: Bearer $PAMPRR_API_KEY"
Response 200

Two endpoints, one enabled and one disabled after three days of failures

  • X-Request-Id
  • X-RateLimit-Limit
  • X-RateLimit-Remaining
  • X-RateLimit-Reset
{
  "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
    }
  ]
}
POST

/api/v1/webhooks

webhooks:manageIdempotency-Key accepted

Registers an https endpoint for signed deliveries and returns its signing secret ONCE.

NameWhereTypeRequiredMeaning
Idempotency-Keyheaderstring (1 to 255 characters)noOptional 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
urlbodystring (12 to 2048 characters)yeshttps only; no credentials, no fragment; must resolve to a public address. pamprr's own hosts are refused.
eventsbodyarray of stringno["*"] (the default) or a non empty list of catalogue event names.
descriptionbodystring or nullnoOptional label to help identify this endpoint, up to 200 characters.
payloadVersionbody1noOptional; "1" is the only value today.

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.

Request

curl shows the document's example Idempotency-Key; send a fresh UUID with every new request, as the other two samples do.

curl -X POST "https://pro.pamprr.me/api/v1/webhooks" \
  -H "Authorization: Bearer $PAMPRR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 2c6f1a3e-5b7d-4e9f-8a1b-3c5d7e9f1a2b" \
  -d '{"url":"https://partner.example/pamprr/hooks","events":["booking.created","booking.cancelled","booking.rescheduled"],"description":"Production till"}'
Response 201

A new endpoint and its one time secret

  • X-Request-Id
  • Idempotent-Replayed
  • X-Original-Request-Id
  • X-RateLimit-Limit
  • X-RateLimit-Remaining
  • X-RateLimit-Reset
{
  "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"
}
GET

/api/v1/webhooks/{id}

webhooks:manage

One endpoint by id, the same projection as a list row: never the secret.

NameWhereTypeRequiredMeaning
idpathuuidyesThe endpoint id. Example: 00000000-0000-4000-8000-00000000e001

An endpoint that is not yours, a deleted endpoint, or an id that is not a UUID is 404 NOT_FOUND.

Request
curl "https://pro.pamprr.me/api/v1/webhooks/00000000-0000-4000-8000-00000000e001" \
  -H "Authorization: Bearer $PAMPRR_API_KEY"
Response 200

One endpoint by id

  • X-Request-Id
  • X-RateLimit-Limit
  • X-RateLimit-Remaining
  • X-RateLimit-Reset
{
  "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
  }
}
PATCH

/api/v1/webhooks/{id}

webhooks:manageIdempotency-Key accepted

Changes any of the url (re checked against the same rules as on create), the events filter, the description, the payloadVersion, or enabled.

NameWhereTypeRequiredMeaning
idpathuuidyesThe endpoint id. Example: 00000000-0000-4000-8000-00000000e001
Idempotency-Keyheaderstring (1 to 255 characters)noOptional: the same rules as on create booking; a retried update replays the first outcome. Example: 8d1e2f3a-4b5c-4d6e-9f7a-8b9c0d1e2f3a
urlbodystring (12 to 2048 characters)noRe checked against the same rules as on create.
eventsbodyarray of stringnoReplace the event filter with ["*"] for the whole catalogue or a non empty list of catalogue event names. Omit to keep the current filter.
descriptionbodystring or nullnoReplace the endpoint label, up to 200 characters; use null to clear it or omit to keep it.
enabledbodybooleannofalse disables with reason manual and cancels pending deliveries; true re enables and clears the reason.
payloadVersionbody1noThe payload version to use; "1" is the only value today. Omit to keep the current version.

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.

Request

curl shows the document's example Idempotency-Key; send a fresh UUID with every new request, as the other two samples do.

curl -X PATCH "https://pro.pamprr.me/api/v1/webhooks/00000000-0000-4000-8000-00000000e001" \
  -H "Authorization: Bearer $PAMPRR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 8d1e2f3a-4b5c-4d6e-9f7a-8b9c0d1e2f3a" \
  -d '{"events":["*"]}'
Response 200

The filter widened

  • X-Request-Id
  • Idempotent-Replayed
  • X-Original-Request-Id
  • X-RateLimit-Limit
  • X-RateLimit-Remaining
  • X-RateLimit-Reset
{
  "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
  }
}
DELETE

/api/v1/webhooks/{id}

webhooks:manageIdempotency-Key accepted

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.

NameWhereTypeRequiredMeaning
idpathuuidyesThe endpoint id. Example: 00000000-0000-4000-8000-00000000e001
Idempotency-Keyheaderstring (1 to 255 characters)noOptional: a retried delete replays the first outcome. Example: 1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d

A second delete of the same id is 404 NOT_FOUND.

Send an Idempotency-Key so a retried request replays the first outcome.

Request

curl shows the document's example Idempotency-Key; send a fresh UUID with every new request, as the other two samples do.

curl -X DELETE "https://pro.pamprr.me/api/v1/webhooks/00000000-0000-4000-8000-00000000e001" \
  -H "Authorization: Bearer $PAMPRR_API_KEY" \
  -H "Idempotency-Key: 1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
Response 200

Deleted with two deliveries cancelled

  • X-Request-Id
  • Idempotent-Replayed
  • X-Original-Request-Id
  • X-RateLimit-Limit
  • X-RateLimit-Remaining
  • X-RateLimit-Reset
{
  "deleted": true,
  "cancelledDeliveries": 2
}
POST

/api/v1/webhooks/{id}/rotate-secret

webhooks:manageIdempotency-Key accepted

Mints a new signing secret and returns it ONCE.

NameWhereTypeRequiredMeaning
idpathuuidyesThe endpoint id. Example: 00000000-0000-4000-8000-00000000e001
Idempotency-Keyheaderstring (1 to 255 characters)noOptional but recommended: a retried rotation replays the first outcome, secret included, rather than rotating twice. Example: 3e4f5a6b-7c8d-4e9f-8a0b-1c2d3e4f5a6b

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.

Request

curl shows the document's example Idempotency-Key; send a fresh UUID with every new request, as the other two samples do.

curl -X POST "https://pro.pamprr.me/api/v1/webhooks/00000000-0000-4000-8000-00000000e001/rotate-secret" \
  -H "Authorization: Bearer $PAMPRR_API_KEY" \
  -H "Idempotency-Key: 3e4f5a6b-7c8d-4e9f-8a0b-1c2d3e4f5a6b"
Response 200

The replacement secret and the end of the dual signing window

  • X-Request-Id
  • Idempotent-Replayed
  • X-Original-Request-Id
  • X-RateLimit-Limit
  • X-RateLimit-Remaining
  • X-RateLimit-Reset
{
  "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"
}
POST

/api/v1/webhooks/{id}/test

webhooks:manageIdempotency-Key accepted

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.

NameWhereTypeRequiredMeaning
idpathuuidyesThe endpoint id. Example: 00000000-0000-4000-8000-00000000e001
Idempotency-Keyheaderstring (1 to 255 characters)noOptional: a retried test replays the first outcome rather than pinging twice. Example: 5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f

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.

Request

curl shows the document's example Idempotency-Key; send a fresh UUID with every new request, as the other two samples do.

curl -X POST "https://pro.pamprr.me/api/v1/webhooks/00000000-0000-4000-8000-00000000e001/test" \
  -H "Authorization: Bearer $PAMPRR_API_KEY" \
  -H "Idempotency-Key: 5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f"
Response 200

The endpoint answered 200 in 312 ms

  • X-Request-Id
  • Idempotent-Replayed
  • X-Original-Request-Id
  • X-RateLimit-Limit
  • X-RateLimit-Remaining
  • X-RateLimit-Reset
{
  "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}"
  }
}
GET

/api/v1/webhooks/{id}/deliveries

webhooks:manage

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.

NameWhereTypeRequiredMeaning
idpathuuidyesThe endpoint id. Example: 00000000-0000-4000-8000-00000000e001
statusquerypending | delivered | failed | cancellednoOnly deliveries in this state. An unknown value is 400 VALIDATION_FAILED. Example: failed
eventTypequeryWebhookEventTypenoOnly deliveries of this event type. An unknown name is 400 VALIDATION_FAILED. Example: booking.cancelled
sincequerydate-timenoOnly 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
limitqueryinteger (1 to 100, default 50)noRows per page, 1 to 100 (a value above 100 is treated as 100). Example: 50
cursorquerystringnoOpaque 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

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.

Request
curl "https://pro.pamprr.me/api/v1/webhooks/00000000-0000-4000-8000-00000000e001/deliveries?status=failed&limit=50" \
  -H "Authorization: Bearer $PAMPRR_API_KEY"
Response 200

A dead letter and a pending retry after a 429

  • X-Request-Id
  • X-RateLimit-Limit
  • X-RateLimit-Remaining
  • X-RateLimit-Reset
{
  "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
}
POST

/api/v1/webhooks/{id}/deliveries/{deliveryId}/replay

webhooks:manageIdempotency-Key accepted

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.

NameWhereTypeRequiredMeaning
idpathuuidyesThe endpoint id. Example: 00000000-0000-4000-8000-00000000e001
deliveryIdpathuuidyesThe delivery id, from the delivery log. Example: 00000000-0000-4000-8000-00000000d001
Idempotency-Keyheaderstring (1 to 255 characters)noOptional: a retried replay replays the first outcome rather than attempting twice. Example: 7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d

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.

Request

curl shows the document's example Idempotency-Key; send a fresh UUID with every new request, as the other two samples do.

curl -X POST "https://pro.pamprr.me/api/v1/webhooks/00000000-0000-4000-8000-00000000e001/deliveries/00000000-0000-4000-8000-00000000d001/replay" \
  -H "Authorization: Bearer $PAMPRR_API_KEY" \
  -H "Idempotency-Key: 7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d"
Response 200

The dead letter delivered on replay

  • X-Request-Id
  • Idempotent-Replayed
  • X-Original-Request-Id
  • X-RateLimit-Limit
  • X-RateLimit-Remaining
  • X-RateLimit-Reset
{
  "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}"
  }
}

Payloads

One envelope for every event; data carries the same projections the API answers.

  • id is the event id and the webhook-id; type the catalogue name; apiVersion the path prefix the shapes belong to; payloadVersion the endpoint's pinned version, "1" today; occurredAt the source row's own time; business.id lets a partner serving several pamprr businesses route.
  • The body is serialised once when the event is picked up and sent byte identical on every attempt and replay: data is the state at pick up, within a minute of the change; a later change is its own event with its own occurredAt.
  • Within a payload version everything is additive: fields and event types are added, never removed or renamed; ignore what you do not know. A breaking change would ship as a new version chosen per endpoint, with the old kept for at least six months.

The WebhookEvent object

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.

FieldTypeAlwaysMeaning
idstringyesThe event id, also the webhook-id header; deduplicate on it, since delivery is at least once.
typeWebhookEventTypeyesThe catalogue of events an endpoint can subscribe to. ping is a test delivery and is sent regardless of the filter.
apiVersionv1yesThe path prefix whose shapes data carries.
payloadVersion1yesThe endpoint's pinned payload version; everything within it is additive.
occurredAtdate-timeyesThe source row's own time, not the pick up time. Apply events in this order, not arrival order.
businessobjectyesThe business the event belongs to, so a partner serving several pamprr businesses can route.
dataone of BookingEventData, BookingCancelledEventData, BookingRescheduledEventData, ClientEventData, ClientArchivedEventData, PaymentEventData, PingEventDatayesThe event's data, one shape per type; each webhooks entry below names its shape.

The BookingEventData object

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.

FieldTypeAlwaysMeaning
bookingBookingyes

The BookingCancelledEventData object

booking.cancelled: the booking (status cancelled, cancelledAt set), the reason given and who cancelled.

FieldTypeAlwaysMeaning
bookingBookingyes
reasonstring or nullyesThe reason given at cancellation, or null when none was.
cancelledByconsumer | business | system or nullyesWho cancelled: the customer, the business, or pamprr itself; null when the record does not say.

The BookingRescheduledEventData object

booking.rescheduled: the booking at its new time and the start it moved from.

FieldTypeAlwaysMeaning
bookingBookingyes
previousobjectyes

The ClientEventData object

client.created and client.updated: the client as the API answers it.

FieldTypeAlwaysMeaning
clientClientyes

The ClientArchivedEventData object

client.archived: the client and the fact of the archive.

FieldTypeAlwaysMeaning
clientClientyes
activefalseyesAlways false: the client is archived.

The PaymentEventData object

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.

FieldTypeAlwaysMeaning
paymentobjectyes
bookingBookingyes

The PingEventData object

ping: a test delivery sent from the dashboard or by the API, regardless of the endpoint's filter, never retried.

FieldTypeAlwaysMeaning
endpointIduuidyesThe endpoint the ping was sent to.
messageA test delivery from pamprr.yes
One example per event
{
  "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
    }
  }
}

Notifications and partner bookings

What a partner action tells the customer, and how the business sees it.

  • A booking created through the API is recorded with the source api and shows as Partner in the business's diary.
  • 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.
  • Cancelling through the API never refunds a deposit or charges a late cancellation fee; refunds are made by the business in its dashboard. Anyone on the waiting list for a freed slot is notified either way.

Versioning and deprecation

The version is the path prefix; changes are additive; a breaking change carries at least six months' notice.

Fields, headers, parameters, enum values and endpoints are added, never removed or renamed, without notice.

  • A change that would break an integration (a removed field, a renamed header, a changed meaning) is announced in the changelog at least six months before it happens.
  • The notice period is only ever extended, never shortened.
  • The document's version tracks additive releases.

Host https://pro.pamprr.me; every path begins /api/v1. The document's version today: 1.2.0.

Changelog

Dated entries, newest first.

  1. 1.2.0Webhooks
    • Outbound webhooks: fourteen event types across bookings, clients and payments, delivered as signed POSTs (Standard Webhooks headers, HMAC SHA256, a day of dual signing on rotation), retried over about two days and logged for thirty; the Webhooks section of this page and the OpenAPI 3.1.1 document's native webhooks entries, one signed example per type.
    • Nine operations under the Webhooks tag on the scope webhooks:manage, now live: the endpoints (list, create, read, update, delete), the secret rotation, the test delivery, the delivery log and the replay; three new codes, WEBHOOK_LIMIT_REACHED, WEBHOOK_ENDPOINT_DISABLED and WEBHOOK_DELIVERY_PENDING.
  2. 1.1.0The first published reference
    • This page, its markdown twin at /developers.md, and the OpenAPI 3.1.1 document with an example on every parameter, request body and response.
    • The surface at launch: the business record, locations, staff, services and the eligible staff for one, the availability search, bookings (list, create, read, cancel, reschedule) and clients (list, create, read), on the foundations of request ids, the rate limit headers, scoped keys, cursor pagination and idempotency keys.

The OpenAPI document

The machine readable description of everything on this page, with the same examples.

Consumer search rows and card footers

GET /api/businesses/search returns business rows by default and one row per visible branch with rows=branches. For branch rows, serviceCount, priceRange and footer.startingPriceInPence describe services offered at that branch through service_locations: active, undeleted services only, excluding add ons. The count and both prices use the same population, across all categories even when a category filter selects the row. An empty branch has serviceCount 0, priceRange { min: null, max: null } and footer.startingPriceInPence null. Business rows retain business-wide figures, including when a nearby branch supplies their address; a legacy row with branch null also uses business figures. Public discovery needs no partner API key. Responses are private, no-store; their catalogue snapshot is shared for 60 seconds with immediate tag expiry after public edits. Branch aggregates and footer calculation add no database reads. The next available slot is omitted because the existing availability summary is live and is absent from both display snapshots. Open state describes cached opening hours evaluated at request time, not capacity. The partner API conventions above do not apply to this consumer endpoint. Rating fields on the row and footer use the same grain. For rows=branches with a non-null branch, only published reviews whose booking belongs to that branch and business qualify. The booking branch is authoritative, not the review's denormalised branch pointer. Business rows and legacy rows with branch null retain all published business reviews, including reviews without an attributable booking. A branch with none has reviewCount 0, row.averageRating 0 for compatibility and footer.averageRating null. Browse renders "No reviews yet at this branch". Rating sorts use branch averages and counts, with unrated rows last in both directions.

Consumer discovery OpenAPI document

The ConsumerSearchRow object

Business or branch search row. With rows=branches and a non-null branch, services and ratings describe that branch. Business rows and legacy branch-null rows retain business figures.

FieldTypeAlwaysMeaning
serviceCountintegeryesFor a branch row, the number of active, undeleted services offered at that branch through service_locations, excluding add ons. Zero when none qualify. Business rows retain their existing count across visible branches, including add ons; a service offered at multiple branches counts once at business grain.
priceRangeobjectyesMinimum and maximum current listed prices in GBP pence from the same population as serviceCount. Branch rows exclude add ons; business rows retain their existing range including add ons. Both endpoints are null without qualifying services. Conditional discounts and vouchers are not applied.
averageRatingnumber (0 to 5)yesPublished review mean rounded to one decimal. For rows=branches with a non-null branch, only published reviews whose booking belongs to that branch and business qualify. The booking branch is authoritative, not the review's denormalised branch pointer. Business rows and legacy rows with branch null retain all published business reviews, including reviews without an attributable booking. Zero without published reviews for compatibility; the footer uses null.
reviewCountintegeryesNumber of published reviews at the row's grain. For rows=branches with a non-null branch, only published reviews whose booking belongs to that branch and business qualify. The booking branch is authoritative, not the review's denormalised branch pointer. Business rows and legacy rows with branch null retain all published business reviews, including reviews without an attributable booking. Zero without qualifying reviews.

The BusinessCardFooter object

Display data from the catalogue snapshot, added in both search row modes. No next slot is supplied.

FieldTypeAlwaysMeaning
averageRatingnumber (0 to 5) or nullyesPublished review mean rounded to one decimal. For rows=branches with a non-null branch, only published reviews whose booking belongs to that branch and business qualify. The booking branch is authoritative, not the review's denormalised branch pointer. Business rows and legacy rows with branch null retain all published business reviews, including reviews without an attributable booking. Null without qualifying published reviews; never falls back to the business figure for an unrated branch.
reviewCountintegeryesNumber of published reviews at the row's grain. For rows=branches with a non-null branch, only published reviews whose booking belongs to that branch and business qualify. The booking branch is authoritative, not the review's denormalised branch pointer. Business rows and legacy rows with branch null retain all published business reviews, including reviews without an attributable booking. Zero without qualifying reviews.
distanceMilesnumber or nullyesThe existing selected row distance in miles rounded to one decimal. Null without a resolved request position or usable business coordinates.
startingPriceInPenceinteger or nullyesMinimum current listed service price in GBP pence. For rows=branches with a non-null branch, only services offered at that branch through service_locations qualify. Business rows, and legacy rows with branch null, use the minimum across visible branches of the business. Active, undeleted services only, excluding add ons in both modes. Null without a qualifying service. A listed offer uses its current service price; conditional discounts and vouchers are not applied.
openNowboolean or nullyesSchedule state at request time in Europe/London. Uses the selected branch hours with business fallback, or the primary overlay without a selected branch. Mobile businesses use mobile hours; fixed and both use fixed hours. Per day hours override legacy working days and opening and closing times. Includes yesterday's overnight interval. Opening is inclusive and closing exclusive; equal times mean closed. Null when incomplete or malformed hours prevent a conclusion. This is not bookable capacity.

The ProfileReviewSummary object

Returns published reviews and their figures from the cached profile snapshot. Omit branchId for all business reviews. Supply branchId for reviews attributed through bookings to that visible branch; an empty branch returns reviews [], totalCount 0 and averageRating null. Unknown, foreign, hidden or deleted branches return 404, never business figures. The snapshot is cached for 60 seconds with immediate expiry after public edits. Branch selection filters that snapshot without another database query or a branch-specific cache key. HTTP responses are private, no-store.

FieldTypeAlwaysMeaning
averageRatingnumber (1 to 5) or nullyesOne-decimal mean of published reviews for the selected branch, or the whole business when branchId is omitted. Null when there are no reviews.
totalCountintegeryesPublished review count for the selected branch, or the whole business when branchId is omitted. Zero when there are no reviews.
reviewsarray of objects (id, rating, reviewText, businessResponse, publishedAt, createdAt, reviewerName)yesPublished reviews at the selected grain, newest publication first. Reviewer surnames are reduced to initials.

Support

Quote the request id.

Every response carries X-Request-Id; quote it.

Write through the contact page, or through the business that minted your key, whose owner can reach pamprr from the dashboard's help centre.

Keys are minted, edited, rotated and revoked in the business's Settings.

  1. Create the account1 min

    An email address and a password. Setup is free until you go live.

  2. Add services and hours5 min

    Your menu, your prices, the week you actually work.

  3. Connect payments3 min

    Stripe onboarding, then deposits and card payments from the first booking.