Booking API
Reservations and Public Booking
Create reservations, expose public booking, and track payment-required flows
This page documents the current enterprise reservation surface: internal reservations scoped to the active organisation, organisation-based public booking, legacy token booking, and Stripe webhook handling for payment confirmation.
Authentication and Permissions
- Internal reservation CRUD requires
JwtAuthGuardplusReservationAccessGuard. POST /api/reservationsrequires the requesting user to have anOrganisationUserrecord in the target organisation. Without it the endpoint returns403 Forbidden(RBAC permission check). The user'sactiveOrganisationIdmust match the organisation that owns the resource being reserved.- Public booking routes are unauthenticated.
- Reservation, resource, and public-booking availability flows all reuse the same organisation-scoped availability logic.
- Payment-required public reservations are confirmed from Stripe webhook events, not from the initial browser request.
Endpoint Reference
Internal Reservations
| Method | Path | Purpose | Request or query | Auth | Source |
|---|---|---|---|---|---|
POST | /api/reservations | Create a reservation in the active organisation. | Body: reservation fields | JWT or user API key | reservations/reservations.controller.ts |
GET | /api/reservations | List reservations in the active organisation. | Query: filter fields | JWT or user API key | reservations/reservations.controller.ts |
GET | /api/reservations/:id | Get one reservation. | Path: id | JWT or user API key | reservations/reservations.controller.ts |
PATCH | /api/reservations/:id | Update one reservation. | Path: id, body: partial reservation fields | JWT or user API key | reservations/reservations.controller.ts |
DELETE | /api/reservations/:id | Delete one reservation or a whole reservation group. | Path: id, optional query: scope=group | JWT or user API key | reservations/reservations.controller.ts |
Public Booking
| Method | Path | Purpose | Request or query | Auth | Source |
|---|---|---|---|---|---|
GET | /api/public/booking/organisations/:slug | Resolve the branded public booking catalog for one organisation. | Path: slug | Public | resources/public-booking.controller.ts |
GET | /api/public/booking/organisations/:slug/availability | Read public availability for one service on one day. | Path: slug, query: date,resourceTypeId,resourceId? | Public | resources/public-booking.controller.ts |
POST | /api/public/booking/organisations/:slug/reserve | Create a public reservation from the organisation booking page. | Path: slug, body: booking fields | Public | resources/public-booking.controller.ts |
GET | /api/public/booking/organisations/:slug/checkout-status | Poll the Stripe-backed payment state for a public reservation. | Path: slug, query: sessionId | Public | resources/public-booking.controller.ts |
GET | /api/public/booking/:token | Resolve public booking metadata for a legacy resource token. | Path: token | Public | resources/public-booking.controller.ts |
GET | /api/public/booking/:token/availability | Read available slots for a day for one legacy published resource. | Path: token, query: date | Public | resources/public-booking.controller.ts |
POST | /api/public/booking/:token/reserve | Create a public reservation from a legacy resource token. | Path: token, body: booking fields | Public | resources/public-booking.controller.ts |
GET | /api/public/booking/:token/checkout-status | Poll the Stripe-backed payment state for a legacy public reservation. | Path: token, query: sessionId | Public | resources/public-booking.controller.ts |
Stripe Webhook
| Method | Path | Purpose | Request or query | Auth | Source |
|---|---|---|---|---|---|
POST | /api/payments/stripe/webhook | Handle payment success, failure, and checkout-expiration events. | Header: Stripe-Signature, raw body | Stripe signature | payments/stripe-webhook.controller.ts |
Request Shapes
Internal reservations
CreateReservationDto and UpdateReservationDto
startTime: required on create, ISO date-timeendTime: required on create, ISO date-time, must be afterstartTimequantity: optional int, minimum1; in pool allocation this stays1per assigned resourcerequestedQuantity: optional int, minimum1; number of concrete resources to allocate from the poolcustomerInfo: optional objectnotes: optional sanitized string, max 2048 charsresourceTypeId: optional positive int, required for the enterprise type flowresourceId: optional positive int, used as a compatibility bridge or single-resource writeresourceIds: optional unique positive integer array for multi-resource same-type reservationsreservationGroupId: optional string; when omitted for multi-slot creates the backend generates one automaticallyslots: optional array of child slot selections; each item acceptsstartTime,endTime, plus optionalresourceId,resourceIds,resourceTypeId,quantity, andrequestedQuantitystatus: update-only enumpending|confirmed|completed|cancelled|waitlist
Grouped-slot creation rules:
- Each slot can now rely on
resourceTypeIdalone to let the backend auto-allocate the best matching resource lane. resourceIdremains an optional explicit override for advanced flows that must target a concrete lane.- Mixed grouped payloads are supported, so one slot can auto-allocate while another targets a specific resource.
Query:
resourceId: optional int>= 1resourceTypeId: optional int>= 1status: optional reservation statusstartFrom: optional ISO date-timeendTo: optional ISO date-time
Public booking
CreateOrganisationPublicBookingDto
resourceTypeId: required positive intresourceId: optional positive int for specific-resource bookingstartTime: required ISO date-timeendTime: required ISO date-timequantity: required int, minimum1requestedQuantity: optional positive int for pool allocation on organisation-scoped routescustomerName: required stringcustomerEmail: required emailcustomerPhone: optional stringnotes: optional string
Phase 4 note:
- The public booking endpoints keep the same external single-slot request shape in this phase.
- Internally, they now reuse the same grouped allocation engine as staff reservations, so slot resolution, pool selection, and schedule validation stay aligned across flows.
Phase 5 note:
- Every reservation flow that includes a customer email now runs through the same customer link-or-create resolution logic.
- Matching active users are linked immediately.
- Matching dormant reservation-seeded users stay in
createdaccount status until the customer completes sign-up, even across repeated bookings and payment webhooks. - New dormant customers receive a private personal calendar named exactly
Reservation, and reservation mirrors target that calendar by default.
PublicOrganisationAvailabilityQueryDto
date: required ISO date stringresourceTypeId: required positive intresourceId: optional positive intrequestedQuantity: optional positive int
Specific-resource availability responses also expose resolved scheduling metadata:
operatingHours: the effective open/close window for that local dayschedule.availabilityMode:continuous|shiftsschedule.operatingHours: the resolved weekly open-hours setschedule.shiftSchedule: the resolved weekly shift-slot set
This is the contract the calendar UI uses to shade closed hours and enforce exact-slot booking when a resource type is in shifts mode.
When a resource type uses seeded capacity child lanes, the availability and booking flows flatten the parent resource out of the candidate set and expose the child lanes as the real allocatable resources.
CreatePublicBookingDto for the legacy token route
startTime: required ISO date-timeendTime: required ISO date-timequantity: required int, minimum1customerName: required stringcustomerEmail: required emailcustomerPhone: optional stringnotes: optional string
Example Calls
Create a reservation
curl -X POST "$PRIMECAL_API/api/reservations" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"startTime": "2026-04-01T08:00:00.000Z",
"endTime": "2026-04-01T09:00:00.000Z",
"resourceTypeId": 8,
"requestedQuantity": 2
}'
Create a grouped multi-slot reservation
curl -X POST "$PRIMECAL_API/api/reservations" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"startTime": "2026-04-01T08:00:00.000Z",
"endTime": "2026-04-01T09:00:00.000Z",
"customerInfo": {
"name": "Ada Lovelace",
"email": "ada@example.com",
"phone": "+36301112222"
},
"slots": [
{
"resourceId": 15,
"startTime": "2026-04-01T08:00:00.000Z",
"endTime": "2026-04-01T09:00:00.000Z"
},
{
"resourceId": 18,
"startTime": "2026-04-02T10:00:00.000Z",
"endTime": "2026-04-02T11:00:00.000Z"
}
]
}'
Read organisation public availability
curl "$PRIMECAL_API/api/public/booking/organisations/acme-spa/availability?date=2026-05-20&resourceTypeId=8&requestedQuantity=2"
Create a public booking
curl -X POST "$PRIMECAL_API/api/public/booking/organisations/acme-spa/reserve" \
-H "Content-Type: application/json" \
-d '{
"resourceTypeId": 8,
"requestedQuantity": 2,
"startTime": "2026-04-01T08:00:00.000Z",
"endTime": "2026-04-01T09:00:00.000Z",
"quantity": 1,
"customerName": "May B. Late",
"customerEmail": "may@example.com",
"customerPhone": "+36301112222"
}'
Example response for a payment-required booking:
{
"reservationId": 901,
"status": "pending_payment",
"paymentStatus": "pending",
"requiresPayment": true,
"quotedAmount": 2500,
"quotedCurrency": "eur",
"checkoutUrl": "https://checkout.stripe.com/c/pay/cs_test_123",
"stripeCheckoutSessionId": "cs_test_123",
"paymentData": {
"provider": "stripe",
"mode": "checkout",
"checkoutUrl": "https://checkout.stripe.com/c/pay/cs_test_123",
"stripeCheckoutSessionId": "cs_test_123",
"amount": 2500,
"currency": "eur",
"status": "pending",
"statusUrl": "/api/public/booking/organisations/acme-spa/checkout-status?sessionId=cs_test_123"
}
}
Read public checkout status
curl "$PRIMECAL_API/api/public/booking/organisations/acme-spa/checkout-status?sessionId=cs_test_123"
Example response:
{
"reservationId": 901,
"status": "confirmed",
"paymentStatus": "succeeded",
"requiresPayment": true,
"canRetryPayment": false,
"confirmationMessage": "Your booking is confirmed.",
"customerAccount": {
"status": "created",
"requiresPasswordSetup": true,
"profile": {
"name": "May B. Late",
"email": "may@example.com",
"phone": "+15550001015"
}
},
"navigation": {
"organisationSlug": "acme-spa",
"calendarPageUrl": "/book/acme-spa"
}
}
Response Status Codes
Reservation conflict
POST /api/reservations and POST /api/public/booking/:token/reserve return 409 Conflict (not 400) when capacity is exceeded or a booking overlap is detected. The response body identifies the conflict type:
{
"statusCode": 409,
"error": "Conflict",
"message": "The requested slot is unavailable. Requested 3 but only 1 slot(s) available."
}
400 Bad Request is reserved exclusively for malformed input and DTO validation failures. If your client was branching on 400 to detect conflict, update it to check for 409.
Response and Behavior Notes
- Internal reservations are scoped to the active organisation.
requestedQuantityandresourceCountare distinct fromquantity:requestedQuantitychooses how many resources to allocate, whilequantityremains per-resource capacity.- Reservation quote fields are stored canonically on the reservation:
quotedUnitAmount,quotedTotalAmount,quotedCurrency, andpaymentRequiredSnapshot. - Multi-resource reservations can assign multiple concrete resources of the same resource type.
- Multi-slot grouped reservations persist one shared
reservationGroupIdacross the created child reservations, andDELETE /api/reservations/:id?scope=groupremoves the whole set in one action. - When payment is required, public booking creates a
pending_paymentreservation, returnspaymentData, and waits for Stripe webhook confirmation before moving it toconfirmed. paymentDatacurrently uses Stripe Checkout with:provider: "stripe"mode: "checkout"checkoutUrlstripeCheckoutSessionId- quote snapshot fields such as amount and currency
- Public catalog payloads expose whether the organisation can currently accept Stripe-backed payments so the client can disable payment-required booking types before reservation creation.
- The public booking client can keep a reservation in a retryable
pending_paymentstate and pollcheckout-statusafter the Stripe return. checkout-statuscan now also return:customerAccount.statuscustomerAccount.requiresPasswordSetupcustomerAccount.profile.name|email|phonenavigation.organisationSlugnavigation.calendarPageUrlnavigation.accountSetupUrl
customerAccount.requiresPasswordSetupshould be interpreted as “this reservation customer still needs to finish claiming their seeded account,” not as a raw password-reset token state.- PrimeCal does not store raw card data.
Pool Booking Notes
For the full bookingMode matrix, pricing math, availability payloads, and
pool-specific error shapes, continue with
Pool Booking API.
Best Practices
- Set the active organisation before listing reservations in multi-org clients.
- Validate date ordering client-side before submitting reservation writes.
- Treat public booking tokens as secrets. Regenerate them when links leak or staff changes occur.
- Add rate limiting or anti-bot protection in front of public booking forms.
- Use Stripe webhooks as the source of truth for payment-required booking confirmation.