Skip to main content
Was this helpful?

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.

JWT or user API keyPublic booking is unauthenticatedActive organisationStripe webhooks

Authentication and Permissions

  • Internal reservation CRUD requires JwtAuthGuard plus ReservationAccessGuard.
  • POST /api/reservations requires the requesting user to have an OrganisationUser record in the target organisation. Without it the endpoint returns 403 Forbidden (RBAC permission check). The user's activeOrganisationId must 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

MethodPathPurposeRequest or queryAuthSource
POST/api/reservationsCreate a reservation in the active organisation.Body: reservation fieldsJWT or user API keyreservations/reservations.controller.ts
GET/api/reservationsList reservations in the active organisation.Query: filter fieldsJWT or user API keyreservations/reservations.controller.ts
GET/api/reservations/:idGet one reservation.Path: idJWT or user API keyreservations/reservations.controller.ts
PATCH/api/reservations/:idUpdate one reservation.Path: id, body: partial reservation fieldsJWT or user API keyreservations/reservations.controller.ts
DELETE/api/reservations/:idDelete one reservation or a whole reservation group.Path: id, optional query: scope=groupJWT or user API keyreservations/reservations.controller.ts

Public Booking

MethodPathPurposeRequest or queryAuthSource
GET/api/public/booking/organisations/:slugResolve the branded public booking catalog for one organisation.Path: slugPublicresources/public-booking.controller.ts
GET/api/public/booking/organisations/:slug/availabilityRead public availability for one service on one day.Path: slug, query: date,resourceTypeId,resourceId?Publicresources/public-booking.controller.ts
POST/api/public/booking/organisations/:slug/reserveCreate a public reservation from the organisation booking page.Path: slug, body: booking fieldsPublicresources/public-booking.controller.ts
GET/api/public/booking/organisations/:slug/checkout-statusPoll the Stripe-backed payment state for a public reservation.Path: slug, query: sessionIdPublicresources/public-booking.controller.ts
GET/api/public/booking/:tokenResolve public booking metadata for a legacy resource token.Path: tokenPublicresources/public-booking.controller.ts
GET/api/public/booking/:token/availabilityRead available slots for a day for one legacy published resource.Path: token, query: datePublicresources/public-booking.controller.ts
POST/api/public/booking/:token/reserveCreate a public reservation from a legacy resource token.Path: token, body: booking fieldsPublicresources/public-booking.controller.ts
GET/api/public/booking/:token/checkout-statusPoll the Stripe-backed payment state for a legacy public reservation.Path: token, query: sessionIdPublicresources/public-booking.controller.ts

Stripe Webhook

MethodPathPurposeRequest or queryAuthSource
POST/api/payments/stripe/webhookHandle payment success, failure, and checkout-expiration events.Header: Stripe-Signature, raw bodyStripe signaturepayments/stripe-webhook.controller.ts

Request Shapes

Internal reservations

CreateReservationDto and UpdateReservationDto

  • startTime: required on create, ISO date-time
  • endTime: required on create, ISO date-time, must be after startTime
  • quantity: optional int, minimum 1; in pool allocation this stays 1 per assigned resource
  • requestedQuantity: optional int, minimum 1; number of concrete resources to allocate from the pool
  • customerInfo: optional object
  • notes: optional sanitized string, max 2048 chars
  • resourceTypeId: optional positive int, required for the enterprise type flow
  • resourceId: optional positive int, used as a compatibility bridge or single-resource write
  • resourceIds: optional unique positive integer array for multi-resource same-type reservations
  • reservationGroupId: optional string; when omitted for multi-slot creates the backend generates one automatically
  • slots: optional array of child slot selections; each item accepts startTime, endTime, plus optional resourceId, resourceIds, resourceTypeId, quantity, and requestedQuantity
  • status: update-only enum pending|confirmed|completed|cancelled|waitlist

Grouped-slot creation rules:

  • Each slot can now rely on resourceTypeId alone to let the backend auto-allocate the best matching resource lane.
  • resourceId remains 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 >= 1
  • resourceTypeId: optional int >= 1
  • status: optional reservation status
  • startFrom: optional ISO date-time
  • endTo: optional ISO date-time

Public booking

CreateOrganisationPublicBookingDto

  • resourceTypeId: required positive int
  • resourceId: optional positive int for specific-resource booking
  • startTime: required ISO date-time
  • endTime: required ISO date-time
  • quantity: required int, minimum 1
  • requestedQuantity: optional positive int for pool allocation on organisation-scoped routes
  • customerName: required string
  • customerEmail: required email
  • customerPhone: optional string
  • notes: 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 created account 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 string
  • resourceTypeId: required positive int
  • resourceId: optional positive int
  • requestedQuantity: optional positive int

Specific-resource availability responses also expose resolved scheduling metadata:

  • operatingHours: the effective open/close window for that local day
  • schedule.availabilityMode: continuous|shifts
  • schedule.operatingHours: the resolved weekly open-hours set
  • schedule.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-time
  • endTime: required ISO date-time
  • quantity: required int, minimum 1
  • customerName: required string
  • customerEmail: required email
  • customerPhone: optional string
  • notes: 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.
  • requestedQuantity and resourceCount are distinct from quantity: requestedQuantity chooses how many resources to allocate, while quantity remains per-resource capacity.
  • Reservation quote fields are stored canonically on the reservation: quotedUnitAmount, quotedTotalAmount, quotedCurrency, and paymentRequiredSnapshot.
  • Multi-resource reservations can assign multiple concrete resources of the same resource type.
  • Multi-slot grouped reservations persist one shared reservationGroupId across the created child reservations, and DELETE /api/reservations/:id?scope=group removes the whole set in one action.
  • When payment is required, public booking creates a pending_payment reservation, returns paymentData, and waits for Stripe webhook confirmation before moving it to confirmed.
  • paymentData currently uses Stripe Checkout with:
    • provider: "stripe"
    • mode: "checkout"
    • checkoutUrl
    • stripeCheckoutSessionId
    • 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_payment state and poll checkout-status after the Stripe return.
  • checkout-status can now also return:
    • customerAccount.status
    • customerAccount.requiresPasswordSetup
    • customerAccount.profile.name|email|phone
    • navigation.organisationSlug
    • navigation.calendarPageUrl
    • navigation.accountSetupUrl
  • customerAccount.requiresPasswordSetup should 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.