Prerequisites
- A valid Bearer token with admin access — see Authentication
- Organization context (
?o={organization_id})
Status Changes
Request path documentation:paths/admin/bookings.yamlforPATCH /api/v1/bookings/{booking_id}paths/admin/bookings.yamlforGET /api/v1/bookings/{booking_id}/cancel
Accept a Requested Booking
When a booking has statusrequested, accept it by updating the booking status:
Reject a Requested Booking
Reject a requested booking with the same update endpoint:Cancel a Booking
Cancel an accepted or requested booking with the dedicated cancel action:Change resource or service
Calendar drag-and-drop and other in-place resource moves use the same JSON:API update:400 and service_and_resource_not_related. The API does not silently switch the booking to another service on the destination.
To keep the service when you move the booking, attach that service to the target resource first, or send both relationships.resource and relationships.service in the same PATCH so the pair is valid together.
Blockers and sequence bookings skip this check. To change resource, dates, add-ons, or sub-bookings on a confirmed booking as a dedicated admin action, use Admin Booking Edit.
Check-In / Check-Out
Request path documentation:paths/admin/bookings.yamlforPOST /api/v1/bookings/{booking_id}/check-inpaths/admin/bookings.yamlforPOST /api/v1/bookings/{booking_id}/check-out- Scan for
POST /api/v1/scans(staff / kiosk scanner)
Web admin Presence section
The current web admin booking-details Presence card (app-admin/pages/bookings/[bookingId]/index.vue) uses these JSON:API fields and endpoints. Use the same fields to decide whether to show a check-in/out section.
Fields that gate the section
Includeresource on GET /api/v1/bookings/{booking_id}. Show the section when all of these are true:
is_editable is false when any of these apply: external_calendar is set, status is reserved, is_sequence is true, is_via_protected_channel is true, or the booking meta IS_READ_ONLY is set.
The org feature Check-In & Check-Out is not a booking attribute. The UI still keys off settings.checkIn.isEnabled on the included resource.
Buttons (Check-In / Check-Out) are not booking fields. Show both when the user can create scans (user.can('create', 'scans')). They are not disabled by start/end time. The overflow-menu actions additionally require data.attributes.status === "accepted".
Fields to render presence
check_in_date is the newest check-in scan with status valid or unpaid. check_out_date is the newest check-out after that time, otherwise null. Recalculated on scan save and delete (Booking::updatePresenceDates()).
Endpoints the Presence card uses
Named routes:
api:v1:bookings.check-in, api:v1:bookings.check-out. History is the JSON:API related scans on bookings. Standalone GET /api/v1/scans is not registered (scans is except('index')).
Related endpoints not used by the Presence buttons:
Guest self check-in uses the same
POST .../check-in with access_token (and optional lat / lng). That path enforces selfCheckIn.disabledUntil / allowedUntil.
Check-In
Check-Out
- Log a
Scanrecord with the timestamp - Update the booking’s check-in/check-out status
- Can be triggered by admin users or via self-service terminals
Resources can be configured with auto-expiry — if a customer doesn’t check in within a configured window, the booking expires automatically.
X-Supports-Dynamic-Form
Clients that can render the delayed check-in form (legal documents / custom fields with collection timingcheck_in) must send:
str_to_bool (FILTER_VALIDATE_BOOLEAN: true, 1, yes, on). CORS allow-lists it (config/cors.php). Constant: CheckInFormService::DYNAMIC_FORM_HEADER.
Check-out (POST .../check-out) does not use this header. Delayed collection applies to check-in only.
Booking action (POST /api/v1/bookings/{id}/check-in)
When Booking::requiresCheckInDetails() is true and the request has no top-level form_data:
Then
GET {form_url} and retry POST /check-in with top-level form_data. On success, meta.status is the scan status (for example valid) and check_in_date is set.
Scanner create (POST /api/v1/scans)
When details are required and the request has no form payload:
Submit answers on a later
POST /scans as either:
- Top-level
form_data(same shape as the booking action), or data.attributes.meta.form_data.
ScanAdapter::extractCheckInFormData accepts both.
Delayed forms at check-in
Legal documents and custom forms can be configured with collection timingcheck_in. If those details are still missing when you check a booking in, the API does not complete the check-in until the client collects and submits them.
Advertise support for the dynamic check-in form with:
form_data), check-in fails with HTTP 400 and code scans.check_in_requires_details_unsupported.
1. First attempt — details still required:
meta.form_url (GET /api/ui/booking-check-in-form). It returns the same dynamic-form shape as checkout (components, validations), limited to still-missing items (documents.{uuid}, service_custom_fields.{uuid}).
3. Re-submit check-in with the collected values:
meta.status is the resulting scan status (for example valid) and check_in_date is set. Invalid form_data returns HTTP 422. Timing / geo / business-rule failures return HTTP 400 with a scans.* error code.
Scanner endpoint (POST /scans)
Staff and kiosk clients that create scans by booking code use the same delayed-form flow. Submit details as either top-level form_data or data.attributes.meta.form_data. See X-Supports-Dynamic-Form.
Generating Documents
Generate a PDF or ticket from a document template:
Returns a
files resource with the generated document.
Booking exports are documented with the other admin export endpoints in Exports.