> ## 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](https://docs.anny.co/developers/guides/admin/booking-creation.md#editing-an-existing-order).

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

***

## 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](https://docs.anny.co/developers/guides/customer/booking-lifecycle.md) |

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](https://docs.anny.co/developers/guides/authentication.md)
* 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](https://docs.anny.co/developers/guides/admin/invoices.md).
* **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.

***

## 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,
  "send_notification": true,
  "status_reason": "Moved to the afternoon slot you requested."
}
```

| 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. On a sub-booking, sending `duration` or a different start or end returns `400` `sub_booking_dates_locked`. |
| `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. |
| `send_notification` | boolean | No | Whether to email or push the customer about this edit. Defaults to `true` when omitted. Set `false` for a silent admin calendar move. |
| `status_reason` | string | No | Customer-facing message included in the booking-updated email. Distinct from the admin-only `note`. Stored on the booking under `status_reason` keyed by the new start unix time. |

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.

### Extras and sub-bookings on a base-data edit

`edit/base-data` does not take an add-on or sub-booking payload. A booking with `super_booking_id` keeps the main booking's period: a different start, end, or `duration` is rejected with `400` and code `sub_booking_dates_locked` ("Sub-booking dates locked"). Change the date on the main booking. When the requested window already matches the stored instants, those stored values are kept.

Existing extras follow the booking:

* **Date change, same service** — extras are re-attached on the new slot. Combined placeholders that only exist on the rebuilt booking are ignored instead of failing the request.
* **Resource change, same service** — leftover extra rows stay on the original booking as-is.
* **Service change** — extras belong to the old service. Leftover sub-bookings are canceled so the correction draft can refund them.

To add, change, or remove extras after a base-data edit, call `edit/service-options` (step 3).

The response is the refreshed [Booking](https://docs.anny.co/developers/models/bookings.md). 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](https://docs.anny.co/developers/guides/internal/admin-ui-forms.md).

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 in admin org context. That override still wins when `always_apply` is `true` — the API does not floor an admin `0` back to `min_quantity`. Customer checkout is different: omitted or zero quantity is floored to `min_quantity` when `always_apply` is `true`, and skipped when `always_apply` is `false`. See [Add-On](https://docs.anny.co/developers/models/add-ons.md).

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](https://docs.anny.co/developers/guides/internal/add-ons-subbooking.md). 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](https://docs.anny.co/developers/guides/admin/booking-creation.md#editing-an-existing-order).

> [!NOTE]
>
> `POST /api/v1/orders/calculate` with the same `order_id` previews the edit before it is applied and reports a `calculation_scope` of `order` (regenerated totals) or `correction` (the delta a locked invoice will receive) — see [Preview the Edit](https://docs.anny.co/developers/guides/admin/booking-creation.md#step-2-preview-the-edit-optional). The per-booking `/edit/*` actions on this page have no preview step.

***

## 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 |
| `400` — `sub_booking_dates_locked` | `edit/base-data` tried to move a sub-booking (`super_booking_id` set) | Change the date on the main booking |
| `400` — `service_and_resource_not_related` | The resource does not offer the service (or the booking has no service) | Send a `resource_id` / `service_id` pair that is linked. The API no longer swaps in another service. Blockers are exempt. |
| `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 |


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