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

Edit a confirmed booking from the admin API, one concern at a time: **resource and dates**, then **fields**, then **add-ons and sub-bookings**. These `/bookings/{booking_id}/edit/*` actions are the per-booking counterpart of rewriting a whole order through [Editing an Existing Order](/developers/guides/admin/booking-creation#editing-an-existing-order).

For creating bookings, see [Admin Booking Creation](/developers/guides/admin/booking-creation). For status changes, check-in, and cancellation, see [Admin Booking Lifecycle](/developers/guides/admin/booking-lifecycle). Customer self-service edits live in [Customer Booking Lifecycle](/developers/guides/customer/booking-lifecycle).

***

## When to use which API

| Goal                                                                                          | Use                                                                           |
| --------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| Change one confirmed booking (resource, dates, customer, participants, add-ons, sub-bookings) | **This guide** — `POST /api/v1/bookings/{booking_id}/edit/*`                  |
| Rewrite the whole order (add or remove bookings, standalone add-ons, booking passes)          | `GET /api/v1/orders/{order_id}/config` then `POST /api/v1/orders/from-config` |
| Customer changes their own booking                                                            | [Customer Booking Lifecycle](/developers/guides/customer/booking-lifecycle)   |

The admin edit actions sit next to the public `edit-*` routes but under `edit/` so the two surfaces do not collide. They require the admin guard, `update` on `bookings`, and `read` on the booking's resource.

***

## Prerequisites

* A valid Bearer token with admin access — see [Authentication](/developers/guides/authentication)
* Organization context (`?o={organization_id}`)
* A confirmed booking (not `reserved`) that belongs to the active organization

***

## Shared behaviour

Every admin edit action:

* **Applies immediately.** There is no preview / `precalculate` step and no `cancel-edit-booking` counterpart. A `200` means the change is persisted.
* **Ignores customer edit gates.** Service settings such as `canEditPeriod` and `canEditServiceOptions` do not apply. Admins are not bound by the customer reschedule deadline.
* **Parks price corrections as an unlocked draft invoice** on the order (`tmp_invoice_id`). The already issued invoice is left untouched. A later edit replaces the previous draft rather than merging into it. Issue the draft explicitly when you are ready — see [Invoices](/developers/guides/admin/invoices).
* **Does not run the customer pay-now flow.** The customer is not sent a payment link as part of the edit.
* **Rejects reserved bookings** with HTTP `400` and `cannot_modify_reserved_booking`. Carts and still-reserved orders must go through checkout, not this API.

<Note>
  Each call is self-contained. Walk the steps below in UI order (resource → date → fields → add-ons / sub-bookings), or call only the action you need.
</Note>

***

## Step 1 — Resource, service, and dates

Request path documentation:

* `POST /api/v1/bookings/{booking_id}/edit/base-data`

```http theme={null}
POST /api/v1/bookings/{booking_id}/edit/base-data?o={org_id}

{
  "resource_id": "{resource_id}",
  "service_id": { "{service_id}": 1 },
  "start_date": "2026-04-16T10:00:00+02:00",
  "end_date": "2026-04-16T11:00:00+02:00",
  "booking_quota_grant_id": "{optional_grant_id}",
  "padding_before": 15,
  "padding_after": 0,
  "override_duration": false
}
```

| Field                    | Type       | Required | Description                                                                                                                                                                                                                                     |
| ------------------------ | ---------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `start_date`             | string     | Yes      | New start (Atom). Omitting it is rejected with `422` — it is not treated as "keep current".                                                                                                                                                     |
| `end_date`               | string     | Yes\*    | New end (Atom), `after_or_equal:start_date`. \*Not required when `duration` is set. Unless `override_duration` or `duration` is set, the server snaps this to the last validated end interval of the (new) service that still fits the request. |
| `duration`               | integer    | No       | Manual duration in the service unit (usually minutes). Sets `end_date` to `start_date + duration` and skips interval snapping.                                                                                                                  |
| `override_duration`      | boolean    | No       | Keep the requested `end_date` as-is instead of snapping it to a validated service interval. Implied when `duration` is set.                                                                                                                     |
| `padding_before`         | integer    | No       | Override the service's preparation padding for this edit (blocker before start). `0` clears it. Omit to use the (new) service default. Does not write the Service row.                                                                          |
| `padding_after`          | integer    | No       | Override the service's follow-up padding for this edit (blocker after end). `0` clears it. Omit to use the (new) service default.                                                                                                               |
| `resource_id`            | integer    | No       | Target resource. Omit to keep the current resource.                                                                                                                                                                                             |
| `service_id`             | object     | No       | Map of `service_id → quantity`. Omit to keep the current service configuration.                                                                                                                                                                 |
| `booking_quota_grant_id` | string/int | No       | Quota grant to charge the edited booking against. Must belong to the active organization.                                                                                                                                                       |

Availability is **not** enforced on this admin action. The booking can move onto a resource that is not wired to the booked service.

### Resource scope

| Payload                                       | What the resource lookup allows                                                                                                                        |
| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| No `service_id` (plain date / sibling switch) | If the booking already sits on a child resource, `resource_id` must be a **sibling of the same parent**. A standalone resource is rejected with `422`. |
| `service_id` present (coupled selection)      | Any resource in the active organization, including a standalone resource. Moving off a child group clears `parent_resource_id`.                        |

### Duration when the service changes

Changing `service_id` **does** update the booking duration on the backend — but through end-interval snapping, not by copying `min_duration` blindly:

* The requested `start_date` / `end_date` are resolved against the **new** service's validated end intervals.
* The last interval whose end is still `<=` the requested end wins. If none fit (for example a 60-minute window on a 90-minute fixed service), the first interval of the new service is used.
* `charged_duration` follows the new service's pricing rules: a fixed-duration service stays one charged interval; a flexible service uses `ceil(window / calculation_interval)`.
* Blocker padding (`blocker_start_date` / `blocker_end_date`) comes from the **new** service's `padding_before` / `padding_after`, unless you send those params to override them.

To keep a window the new service would not validate, send `override_duration: true` or an explicit `duration`.

### Service configuration and sequences

* A single-service `service_id` map changes the booking's service.
* Adding a second service on a resource with `create_booking_sequence` turns the booking into a **sequence parent**: `is_sequence` is true, the parent carries no `service_id`, and one sequenced child is created per selected service. The parent span is snapped to the first child's start and the last child's end.
* Dropping back to one service **collapses** the sequence: `is_sequence` is cleared, the remaining service is restored on the booking, and the sequenced children are removed.
* Quota usage follows the children while the booking is a sequence, then returns to the parent when it collapses. Prepaid (currency) grants settle against the balance instead of charging twice.

The response is the refreshed [Booking](/developers/models/bookings). Pass `include` (for example `resource.parent`) to sideload the switched-to resource.

***

## Step 2 — Fields

Customer, participants, and custom fields are separate actions. They do not go through `edit/base-data`.

### Reassign the customer

```http theme={null}
POST /api/v1/bookings/{booking_id}/edit/customer?o={org_id}

{
  "customer_id": "{customer_id}"
}
```

| Field         | Type       | Required | Description                                                       |
| ------------- | ---------- | -------- | ----------------------------------------------------------------- |
| `customer_id` | string/int | Yes      | Customer in the active organization. A foreign customer is `422`. |

* When the booking is **not personalized** (its customer matches the order customer), the change also updates the order customer.
* When the booking **is personalized**, only this booking is reassigned.
* Sub-bookings and sequenced children adopt the new customer as well.

### Sync participants

```http theme={null}
POST /api/v1/bookings/{booking_id}/edit/participants?o={org_id}

{
  "participants": [
    {
      "email": "keep@example.com",
      "name": "Keep Me",
      "can_edit": false,
      "attending_virtually": false
    }
  ]
}
```

| Field                 | Type    | Description                                     |
| --------------------- | ------- | ----------------------------------------------- |
| `email`               | string  | Participant email (max 200 chars)               |
| `name`                | string  | Display name (nullable, max 200)                |
| `can_edit`            | boolean | Whether the participant can edit their own data |
| `attending_virtually` | boolean | Virtual attendance flag                         |

This is a **sync of the participants you send**: matching emails are updated in place. Participants missing from the payload are **kept** — the action never deletes attendees.

### Custom fields

Booking custom fields are not on `/edit/*`. Load the standalone admin form, collect values, then apply them:

```http theme={null}
POST /api/v1/ui/admin/booking-form?o={org_id}

{ "booking_id": "{booking_id}" }
```

```http theme={null}
POST /api/v1/ui/admin/booking-form/apply?o={org_id}

{
  "booking_id": "{booking_id}",
  "form_data": {
    "custom_entry_map": {
      "{field_uuid}": { "value": "Wheelchair access" }
    }
  }
}
```

The apply endpoint persists the submitted `custom_entry_map` values, deletes entries that were cleared to `""` / `null`, and returns the booking plus a rebuilt `meta.form_default`. Payload shape and persistence rules are in [Admin UI Forms](/developers/guides/internal/admin-ui-forms).

You can also `PATCH /api/v1/bookings/{booking_id}` with `custom_entry_map` if you already know the field UUIDs.

***

## Step 3 — Add-ons and sub-bookings

Request path documentation:

* `POST /api/v1/bookings/{booking_id}/edit/service-options`

```http theme={null}
POST /api/v1/bookings/{booking_id}/edit/service-options?o={org_id}

{
  "addons": [
    { "add_on_id": 12, "quantity": 2 }
  ],
  "sub_bookings": [
    { "service_id": "{service_id}", "resource_id": "{resource_id}", "quantity": 1 }
  ]
}
```

| Field          | Type  | Description                                                                                  |
| -------------- | ----- | -------------------------------------------------------------------------------------------- |
| `addons`       | array | Desired add-ons `{ add_on_id, quantity }`. `add_on_id` and `quantity` are required integers. |
| `sub_bookings` | array | Desired linked bookings `{ service_id, resource_id, quantity }`. `quantity` is optional.     |

Unlike the customer `edit-service-options` action, an admin may attach:

* an add-on the organization owns that is **not wired** to the booked service
* a sub-resource / sub-service pair that is **not registered** as a sub-resource of the booked service

Items that belong to **another organization** are ignored (the request still returns `200`; those rows are not written).

Quantity `0` on an add-on removes it. The replacement set is applied immediately and a correction draft is parked on the order when the issued invoice would otherwise change.

For the checkout-time `add_ons_by_service` / `sub_bookings_by_service` grouping (quantity > 1 at create time), see [Add-Ons and Sub-Bookings](/developers/guides/internal/add-ons-subbooking). This admin action takes a flat list for the one booking you are editing.

***

## Whole-order alternative

When the change is not "this one booking" but the order as a whole — extra bookings, standalone add-ons, booking passes, or removals — read the order back as a config and post the **complete desired state**:

```http theme={null}
GET /api/v1/orders/{order_id}/config?o={org_id}
```

```http theme={null}
POST /api/v1/orders/from-config?o={org_id}

{ "order_id": "{order_id}", "bookings": [ ... ], ... }
```

A booking entry with `booking_id` updates that booking; an entry without one is added; an omitted booking is soft-deleted. Sending only the bookings you changed will remove every booking you left out.

That flow, including invoice regeneration, serialization, and `notify_customer` rules, is documented in [Editing an Existing Order](/developers/guides/admin/booking-creation#editing-an-existing-order).

<Warning>
  `POST /api/v1/orders/calculate` always prices the config as a **new** order. It ignores `order_id`. After an edit, read `meta.calculation` from the `from-config` response, or rely on the draft correction parked by the per-booking `/edit/*` actions.
</Warning>

***

## Common errors

| Error                                                        | Cause                                                                                | Solution                                                          |
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------ | ----------------------------------------------------------------- |
| `400` — `cannot_modify_reserved_booking`                     | The booking (or the order, for `/config`) is still reserved, a cart, or a base order | Wait until the booking is confirmed, or complete checkout         |
| `403` — unauthorized                                         | Missing `update` on bookings or `read` on the resource                               | Check the admin role                                              |
| `404`                                                        | Booking / order is not in the active organization                                    | Pass the correct `?o=` and identifier                             |
| `422` — `start_date` / `end_date` required                   | `edit/base-data` without dates                                                       | Always send both Atom timestamps                                  |
| `422` — resource / service / grant / customer does not exist | Identifier is unknown or belongs to another organization                             | Stay inside the active organization                               |
| `422` — out-of-group resource                                | Plain `edit/base-data` tried to leave the parent resource group                      | Send `service_id` for a coupled move, or pick a sibling           |
| `422` — invalid `form_data`                                  | Custom-field apply failed validation                                                 | Fix the errors keyed by `custom_entry_map.{uuid}.value` and retry |
