> ## Documentation Index
> Fetch the complete documentation index at: https://docs.anny.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Admin Booking Lifecycle

Admin-specific booking operations: status changes, check-in/out, and document generation.

For customer booking management, see [Customer Booking Lifecycle](/developers/guides/customer/booking-lifecycle). For creating bookings, see [Admin Booking Creation](/developers/guides/admin/booking-creation). For changing a confirmed booking's resource, dates, fields, add-ons, or sub-bookings, see [Admin Booking Edit](/developers/guides/admin/booking-edit). For general conventions, see [JSON-API Conventions](/developers/guides/json-api-conventions).

***

## Prerequisites

* A valid Bearer token with admin access — see [Authentication](/developers/guides/authentication)
* Organization context (`?o={organization_id}`)

***

## Status Changes

Request path documentation:

* `paths/admin/bookings.yaml` for `PATCH /api/v1/bookings/{booking_id}`
* `paths/admin/bookings.yaml` for `GET /api/v1/bookings/{booking_id}/cancel`

### Accept a Requested Booking

When a booking has status `requested`, accept it by updating the booking status:

```http theme={null}
PATCH /api/v1/bookings/{booking_id}?o={org_id}
```

Example payload:

```json theme={null}
{
	"data": {
		"id": "{booking_id}",
		"type": "bookings",
		"attributes": {
			"status": "accepted"
		}
	}
}
```

### Reject a Requested Booking

Reject a requested booking with the same update endpoint:

```http theme={null}
PATCH /api/v1/bookings/{booking_id}?o={org_id}
```

Example payload:

```json theme={null}
{
	"data": {
		"id": "{booking_id}",
		"type": "bookings",
		"attributes": {
			"status": "rejected"
		}
	}
}
```

### Cancel a Booking

Cancel an accepted or requested booking with the dedicated cancel action:

```http theme={null}
GET /api/v1/bookings/{booking_id}/cancel?o={org_id}
```

This runs the booking cancellation flow. Refunds and invoice adjustments follow the configured cancellation policy.

***

## Change resource or service

Calendar drag-and-drop and other in-place resource moves use the same JSON:API update:

```http theme={null}
PATCH /api/v1/bookings/{booking_id}?o={org_id}

{
  "data": {
    "type": "bookings",
    "id": "{booking_id}",
    "relationships": {
      "resource": {
        "data": { "type": "resources", "id": "{resource_id}" }
      }
    }
  }
}
```

The current service must already be offered on the target resource (or on its parent, when you move a child resource). If it is not, the request fails with HTTP `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](/developers/guides/admin/booking-edit).

***

## Check-In / Check-Out

Request path documentation:

* `paths/admin/bookings.yaml` for `POST /api/v1/bookings/{booking_id}/check-in`
* `paths/admin/bookings.yaml` for `POST /api/v1/bookings/{booking_id}/check-out`
* [Scan](/developers/models/scans) for `POST /api/v1/scans` (staff / kiosk scanner)

Record customer arrivals and departures.

### 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

Include `resource` on `GET /api/v1/bookings/{booking_id}`. Show the section when **all** of these are true:

| Field | JSON:API path | Show the section when |
| - | - | - |
| Editable | `data.attributes.is_editable` | `true` |
| Not a blocker | `data.attributes.is_blocker` | `false` |
| Has a resource | `data.relationships.resource.data` | present |
| Resource check-in enabled | included resource `attributes.settings.checkIn.isEnabled` | `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

| Purpose | JSON:API path |
| - | - |
| Current check-in time | booking `attributes.check_in_date` (Atom) |
| Current check-out time | booking `attributes.check_out_date` (Atom) |
| History type | scan `attributes.scan_type` (`check_in` / `check_out`) |
| History status | scan `attributes.status` (`valid`, `scanned`, `unpaid`, `too_early`, `too_late`, `invalid`, `requires_details`, …) |
| History timestamp | scan `attributes.created_at` |
| Who scanned | included `scanner` (`given_name` / `name`) |
| Device label | scan `attributes.device_identifier` |

`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

| Action | Method | Path | Query | Body |
| - | - | - | - | - |
| Load booking (section fields) | `GET` | `/api/v1/bookings/{booking_id}` | `include=resource` (admin page also includes more), `o={org_id}` | — |
| Load history | `GET` | `/api/v1/bookings/{booking_id}/scans` | `include=scanner`, `sort=-createdAt`, `select[users]=given_name`, `select[customers]=given_name`, `o={org_id}` | — |
| Check-in | `POST` | `/api/v1/bookings/{booking_id}/check-in` | `o={org_id}` | `{ "data": {} }` |
| Check-out | `POST` | `/api/v1/bookings/{booking_id}/check-out` | `o={org_id}` | `{ "data": {} }` |
| Delete a history row | `DELETE` | `/api/v1/scans/{scan_id}` | `o={org_id}` | — |

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

<Warning>
  The current admin `Booking.checkIn()` does **not** send `X-Supports-Dynamic-Form`. If delayed check-in documents or custom fields are still missing, `POST /check-in` returns **HTTP 400** `scans.check_in_requires_details_unsupported` instead of `requires_details`. Shop `checkInWithToken` **does** send the header. See [X-Supports-Dynamic-Form](#x-supports-dynamic-form).
</Warning>

Related endpoints **not** used by the Presence buttons:

| Action | Method | Path | Notes |
| - | - | - | - |
| Scan a QR / kiosk code | `POST` | `/api/v1/scans?o={org_id}&code={booking_code}&scannable_type=bookings` | Timing and Admin Check-In Period apply (`Scan::validate(false)`). |
| Fetch delayed check-in form | `GET` | `/api/ui/booking-check-in-form?booking={booking_id}` | Also returned as `meta.form_url`. Optional `access_token`. |

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

```http theme={null}
POST /api/v1/bookings/{booking_id}/check-in?o={org_id}
X-Supports-Dynamic-Form: true
```

Optional body fields:

| Field | Type | Description |
| - | - | - |
| `latitude` / `longitude` | number | Required when the resource uses geo check-in |
| `form_data` | object | Submitted after collecting delayed check-in details (see below) |
| `send_notification` | boolean | Defaults to `true` |

### Check-Out

```http theme={null}
POST /api/v1/bookings/{booking_id}/check-out?o={org_id}
```

Both endpoints:

* Log a `Scan` record with the timestamp
* Update the booking's check-in/check-out status
* Can be triggered by admin users or via self-service terminals

<Note>
  Resources can be configured with auto-expiry — if a customer doesn't check in within a configured window, the booking expires automatically.
</Note>

### X-Supports-Dynamic-Form

Clients that can render the delayed check-in form (legal documents / custom fields with collection timing `check_in`) must send:

```http theme={null}
X-Supports-Dynamic-Form: true
```

The API reads the header with `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`:

| Header | Result |
| - | - |
| Missing / false | **HTTP 400**. JSON:API error `scans.check_in_requires_details_unsupported`. No scan, `check_in_date` stays `null`. |
| `true` | **HTTP 200**. Booking document with `meta.status: "requires_details"` and `meta.form_url`. **Check-in is not completed** (`check_in_date` stays `null`). |

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:

| Header | Result |
| - | - |
| Missing / false | **HTTP 400** `scans.check_in_requires_details_unsupported`. **No** scan. |
| `true` | Scan **is created** with `data.attributes.status: "requires_details"` and `data.attributes.meta.form_url`. Booking is **not** checked in until form data is submitted. |

Submit answers on a later `POST /scans` as either:

1. Top-level `form_data` (same shape as the booking action), or
2. `data.attributes.meta.form_data`.

`ScanAdapter::extractCheckInFormData` accepts both.

### Delayed forms at check-in

Legal documents and custom forms can be configured with collection timing `check_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:

```http theme={null}
X-Supports-Dynamic-Form: true
```

Without that header (and without `form_data`), check-in fails with HTTP 400 and code `scans.check_in_requires_details_unsupported`.

**1. First attempt** — details still required:

```json theme={null}
{
  "meta": {
    "status": "requires_details",
    "form_url": "https://…/api/ui/booking-check-in-form?booking={id}&access_token=…"
  },
  "data": {
    "type": "bookings",
    "id": "{booking_id}",
    "attributes": {
      "check_in_date": null
    }
  }
}
```

**2. Load the form** from `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:

```http theme={null}
POST /api/v1/bookings/{booking_id}/check-in?o={org_id}
X-Supports-Dynamic-Form: true

{
  "form_data": {
    "documents": {
      "{document_uuid}": { "accepted": true, "send_copy": false }
    },
    "service_custom_fields": {
      "{field_uuid}": "value"
    }
  }
}
```

On success, `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](#x-supports-dynamic-form).

```http theme={null}
POST /api/v1/scans?o={org_id}&code={booking_code}&scannable_type=bookings
X-Supports-Dynamic-Form: true

{
  "data": {
    "type": "scans",
    "attributes": {
      "scan_type": "check_in"
    }
  }
}
```

| Situation | Result |
| - | - |
| Header present, no form payload, details pending | Scan is created with `data.attributes.status: "requires_details"` and `data.attributes.meta.form_url`; booking is **not** checked in |
| Header missing, no form payload, details pending | HTTP 400, `scans.check_in_requires_details_unsupported` |
| Top-level `form_data` or `data.attributes.meta.form_data` provided (and valid) | Details are persisted, scan resolves to `valid`, booking `check_in_date` is set |

```http theme={null}
POST /api/v1/scans?o={org_id}&code={booking_code}&scannable_type=bookings
X-Supports-Dynamic-Form: true

{
  "data": {
    "type": "scans",
    "attributes": {
      "scan_type": "check_in",
      "meta": {
        "form_data": {
          "documents": {
            "{document_uuid}": { "accepted": true, "send_copy": false }
          },
          "service_custom_fields": {
            "{field_uuid}": "value"
          }
        }
      }
    }
  }
}
```

See [Scan](/developers/models/scans) for the resource schema and create parameters.

***

## Generating Documents

Generate a PDF or ticket from a document template:

```http theme={null}
GET /api/v1/bookings/{booking_id}/generate-file/{template_id}?o={org_id}
```

| Parameter | Description |
| - | - |
| `template_id` | UUID of the document template to use |
| `override` (query) | `true` to regenerate if a file already exists |

Returns a `files` resource with the generated document.

***

Booking exports are documented with the other admin export endpoints in [Exports](/developers/guides/admin/exports).

***

## Common Errors

| Error | Cause | Solution |
| - | - | - |
| `403` — unauthorized | Insufficient admin permissions | Verify role has booking management permission |
| `400` — `service_and_resource_not_related` | `PATCH` moved the booking to a resource that does not offer the current service | Offer the service on the target resource, or send a matching `relationships.service` in the same request |
| `422` — already checked in | Duplicate check-in attempt | Check booking status before check-in |
| `400` — `scans.check_in_requires_details_unsupported` | Delayed check-in details are required but the client did not send `X-Supports-Dynamic-Form: true` (and no `form_data`) | Send the capability header, load `form_url`, then re-submit with `form_data` |
| `422` — invalid `form_data` | Missing required signature or custom field | Fix validation errors from the check-in form and retry |
| `404` — template not found | Invalid document template UUID | Verify template exists in the organization |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.