# reservations (/docs/reference/api/reservations)

> A reservation is a tenant's claim on devices of one pool for a time window. `create` first writes the request as a draft, Better IAM decides once on that draft with the request as attributes, and only an allowed draft is charged against quota and placed. A reservation holds its quota charge until it reaches a terminal state, and the `capacity.reservations` reconciler drives it through `placed`, `binding` and `active` to `released`. See [Reservations](/docs/guides/capacity/reservations).



A reservation is a tenant's claim on devices of one pool for a time window. `create` first writes the request as a draft, Better IAM decides once on that draft with the request as attributes, and only an allowed draft is charged against quota and placed. A reservation holds its quota charge until it reaches a terminal state, and the `capacity.reservations` reconciler drives it through `placed`, `binding` and `active` to `released`. See [Reservations](/docs/guides/capacity/reservations).

All 7 methods are served by this release.

| Method                | Kind     | Authorization                                          | Phase | Status     |
| --------------------- | -------- | ------------------------------------------------------ | ----- | ---------- |
| [`create`](#create)   | mutation | `compute:reservations:create` @ `compute-reservation`  | v0.1  | registered |
| [`get`](#get)         | read     | `compute:reservations:read` @ `compute-reservation`    | v0.1  | registered |
| [`list`](#list)       | read     | `compute:reservations:read` @ `compute-tenant` + items | v0.1  | registered |
| [`release`](#release) | mutation | `compute:reservations:release` @ `compute-reservation` | v0.1  | registered |
| [`cancel`](#cancel)   | mutation | `compute:reservations:release` @ `compute-reservation` | v0.2  | registered |
| [`extend`](#extend)   | mutation | `compute:reservations:extend` @ `compute-reservation`  | v0.1  | registered |
| [`renew`](#renew)     | mutation | `compute:reservations:extend` @ `compute-reservation`  | v0.1  | registered |

## create [#create]

`POST /api/compute/reservations/create`

Books devices of one pool for a time window after one IAM decision on the draft; returns the reservation, `placed` or `pending`.

* **Idempotency.** The reservation id is derived from the tenant, the caller's identity and `idempotencyKey`. The same key with the same request returns the stored reservation without a second decision once it is past the draft stage, and decides the same draft again while it is still a draft. The same key with a different request is `CONFLICT`, with `details.reservationId`; a key whose request was refused stays `ACCESS_DENIED`. `wait` is not part of the compared request.
* **Decision.** `compute:reservations:create` is decided on the draft (`compute-reservation`), with attributes such as `gpuModel`, `gpuCount`, `durationHours` and `region`. When an agent in a delegated session is refused and IAM opens a confirmation request, the call fails with `CONFIRMATION_REQUIRED` and the draft waits in `awaiting-confirmation` for at most 30 minutes; a retry with the same key after approval continues it.
* **Errors.** `NOT_FOUND` when the pool is not owned by or offered to the tenant, or is archived. `INVALID_INPUT` when a device class or sharing mode does not match the pool's, `binding: 'node'` names a pool without `policy.tenantRoot`, `binding: 'kubernetes'` names a pool without `policy.kubernetes` or comes from a tenant other than the pool's owner (the kubelet gives bound GPUs to any pod of the cluster, so only the cluster's owner books into it), or a node binding asks for `resources`. `UNSUPPORTED` for any binding other than `hold`, `node` or `kubernetes`. `QUOTA_EXCEEDED` when the charge does not fit; the draft stays, so the same key can be retried. `SPEND_LIMIT_REACHED` when the pool or the tenant's offer asks for a budget check and the budget is spent.
* **Host resources.** `resources` (`vcpu`, `memoryGiB`, `hugepages1Gi`, per node for a gang) books host CPU, memory and 1 GiB hugepages on the same node as the devices, and the node enforces them. Without it, a request for whole GPUs books the pool's per-GPU share, stored on the reservation at creation. That share is `policy.perGpu` where it is set, or otherwise the smallest share any of the pool's GPU hosts can give (its allocatable CPUs or memory divided by its bookable GPUs). Requests for MIG instances or slots alone, and pools whose hosts report no CPU or memory rows, book none. The share is also the floor: `resources` below it (or `{}`) are raised to it, so a reservation's workload is always limited. The share rides on the GPUs, and needs no quota beyond theirs. Anything booked above it is charged to the tenant's `vcpu` and `memoryGiB` quota; 1 GiB hugepages count as memory. A missing quota row grants 0, so the excess is `QUOTA_EXCEEDED` unless the offer names these resources. IAM sees `vcpu` and `memoryGiB` as attributes (summed over a gang).
* **Placement.** A request that does not fit, and is not spot, may preempt spot work. Otherwise it is `pending` with an `Unschedulable` condition and retried every 30 seconds, or, with the pool-lease placer, `pending` with a `Queued` condition. `wait.timeoutMs` (at most 30 seconds) holds the answer while the reservation is queued; a timeout returns the reservation still queued, not an error.

Emits `reservation.placed`, `reservation.pending`, `reservation.queued` or `reservation.awaiting-confirmation`.

* **Kind:** mutation
* **Authorization:** `compute:reservations:create` @ `compute-reservation`
* **Module:** `capacity`
* **Phase:** v0.1 (registered)

**Input**

| Field                    | Type                                                          | Required | Constraints                                                       |
| ------------------------ | ------------------------------------------------------------- | -------- | ----------------------------------------------------------------- |
| `tenantId`               | string                                                        | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$`        |
| `poolId`                 | string                                                        | yes      | at most 57 characters; matches `^pool_[0-9a-hjkmnp-tv-z]{26,52}$` |
| `projectId`              | string                                                        |          | at most 56 characters; matches `^prj_[0-9a-hjkmnp-tv-z]{26,52}$`  |
| `devices`                | list of object                                                | yes      | 1-8 items                                                         |
| `devices[].class`        | one of `gpu`, `gpu.mig`, `gpu.slot`, `nic.vf`, `disk`, `node` | yes      |                                                                   |
| `devices[].count`        | integer                                                       | yes      | 1-64                                                              |
| `devices[].selector`     | object                                                        |          |                                                                   |
| `devices[].topology`     | one of `any`, `nvlink-all`                                    |          |                                                                   |
| `devices[].sharing`      | one of `exclusive`, `mig`, `timeslice`, `mps`                 |          |                                                                   |
| `topology`               | one of `any`, `nvlink-all`                                    |          |                                                                   |
| `start`                  | integer                                                       |          | 0-140737488355328                                                 |
| `duration`               | string,integer                                                | yes      |                                                                   |
| `binding`                | string                                                        |          | default `"hold"`                                                  |
| `resources`              | object                                                        |          |                                                                   |
| `resources.vcpu`         | integer                                                       |          | 0-4096                                                            |
| `resources.memoryGiB`    | integer                                                       |          | 0-65536                                                           |
| `resources.hugepages1Gi` | integer                                                       |          | 0-4096                                                            |
| `node`                   | object                                                        |          |                                                                   |
| `node.profileId`         | string                                                        | yes      | at most 128 characters; matches `^[A-Za-z0-9._:/-]{1,128}$`       |
| `node.profileVersion`    | integer                                                       |          | at least 1                                                        |
| `leaseSeconds`           | integer                                                       |          | 30-86400                                                          |
| `spot`                   | boolean                                                       |          |                                                                   |
| `idempotencyKey`         | string                                                        | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$`        |
| `wait`                   | object                                                        |          |                                                                   |
| `wait.timeoutMs`         | integer                                                       | yes      | 0-30000                                                           |
| `gang`                   | object                                                        |          |                                                                   |
| `gang.size`              | integer                                                       | yes      | 2-64                                                              |
| `gang.topologyKey`       | string                                                        |          | at most 128 characters                                            |

## get [#get]

`POST /api/compute/reservations/get`

Returns one reservation of the tenant with its `operation` and, once a node reported its devices bound, its `binding`.

Drafts are internal: the call is `NOT_FOUND` for them, as for a reservation that does not exist in the tenant.

* **Kind:** read
* **Authorization:** `compute:reservations:read` @ `compute-reservation`
* **Module:** `capacity`
* **Phase:** v0.1 (registered)

**Input**

| Field           | Type   | Required | Constraints                                                      |
| --------------- | ------ | -------- | ---------------------------------------------------------------- |
| `tenantId`      | string | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$`       |
| `reservationId` | string | yes      | at most 56 characters; matches `^rsv_[0-9a-hjkmnp-tv-z]{26,52}$` |

## list [#list]

`POST /api/compute/reservations/list`

Returns the tenant's reservations newest first, skipping drafts, filtered by `state` and `poolId`, one page at a time.

With `scope: 'mine'` (the default) only the caller's own reservations are listed. With `scope: 'all'` the tenant's reservations are loaded and each one is checked as `compute:reservations:read` on the reservation, so only the allowed ones are returned. `limit` is 1 to 200, default 50; `cursor` is the id of the last reservation of the previous page, and the answer carries the next `cursor` while more remain.

* **Kind:** read
* **Authorization:** `compute:reservations:read` @ `compute-tenant` + items
* **Module:** `capacity`
* **Phase:** v0.1 (registered)

**Input**

| Field      | Type                                                                                                                                                   | Required | Constraints                                                       |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- | ----------------------------------------------------------------- |
| `tenantId` | string                                                                                                                                                 | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$`        |
| `scope`    | one of `mine`, `all`                                                                                                                                   |          | default `"mine"`                                                  |
| `state`    | one of `draft`, `denied`, `awaiting-confirmation`, `pending`, `placed`, `binding`, `active`, `releasing`, `released`, `failed`, `cancelled`, `expired` |          |                                                                   |
| `poolId`   | string                                                                                                                                                 |          | at most 57 characters; matches `^pool_[0-9a-hjkmnp-tv-z]{26,52}$` |
| `limit`    | integer                                                                                                                                                |          | 1-200                                                             |
| `cursor`   | string                                                                                                                                                 |          | at most 56 characters; matches `^rsv_[0-9a-hjkmnp-tv-z]{26,52}$`  |

## release [#release]

`POST /api/compute/reservations/release`

Ends a reservation now: its bookings are released at `compute_now()`, its quota is refunded, and it becomes `released`; returns the reservation.

A `pending` reservation is `cancelled` instead. A reservation already in a terminal state is returned unchanged. A bound `binding: 'kubernetes'` reservation is `released` at once, and its node withdraws the devices from the kubelet. Its devices and host resources stay booked, and billed, until the node reports that no pod holds them (`AllocationReleased`) or the node is lost, and the reservation carries the `DevicesHeld` condition meanwhile. Until then nothing else can book them. If pods still hold them at the booking's end, the booking is carried on in 5-minute steps while the devices are free, unbilled, for at most 24 hours; the node is `KubernetesReleaseStuck` (NoSchedule) meanwhile. A booking made for those devices in advance takes them at its start. A request still in `draft` or `awaiting-confirmation` is `INVALID_STATE`. Freed capacity wakes the pool's pending reservations. Emits `reservation.released` or `reservation.cancelled`, with `reason: 'release'`.

* **Kind:** mutation
* **Authorization:** `compute:reservations:release` @ `compute-reservation`
* **Module:** `capacity`
* **Phase:** v0.1 (registered)

**Input**

| Field           | Type   | Required | Constraints                                                      |
| --------------- | ------ | -------- | ---------------------------------------------------------------- |
| `tenantId`      | string | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$`       |
| `reservationId` | string | yes      | at most 56 characters; matches `^rsv_[0-9a-hjkmnp-tv-z]{26,52}$` |

## cancel [#cancel]

`POST /api/compute/reservations/cancel`

Withdraws a reservation that has not started (`pending` or `placed`): its bookings are released, its quota is refunded, and it becomes `cancelled`; returns the reservation.

A reservation already `cancelled` is returned unchanged. Any other state is `INVALID_STATE`; use `release` for a reservation that has started. Drafts are `NOT_FOUND`. It is decided as `compute:reservations:release`. Emits `reservation.cancelled`.

* **Kind:** mutation
* **Authorization:** `compute:reservations:release` @ `compute-reservation`
* **Module:** `capacity`
* **Phase:** v0.2 (registered)

**Input**

| Field           | Type   | Required | Constraints                                                      |
| --------------- | ------ | -------- | ---------------------------------------------------------------- |
| `tenantId`      | string | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$`       |
| `reservationId` | string | yes      | at most 56 characters; matches `^rsv_[0-9a-hjkmnp-tv-z]{26,52}$` |

## extend [#extend]

`POST /api/compute/reservations/extend`

Moves the end of a `placed`, `binding` or `active` reservation later, all or nothing, after one IAM decision; returns the reservation and whether it was extended.

* Give exactly one of `until` (a new end, epoch milliseconds) or `by` (a duration); both or neither is `INVALID_INPUT`. Other states are `INVALID_STATE`.
* It is admitted like `create`: the pending change goes into the reservation's authz row with `durationHours` set to the total after the change, so a policy that caps duration also caps extensions. A refusal is `ACCESS_DENIED` and clears the pending change. `SPEND_LIMIT_REACHED` applies as for `create`.
* When another booking starts before the new end, nothing moves and the answer is `extended: false` with `availableUntil`, the latest end the bookings can reach.
* **Idempotency.** An `until` the reservation already reaches answers `extended: false` without a decision, so a retried `until` is safe. `idempotencyKey` does not replay: a repeated `by` extends again from the new end.
* While another extend of the same reservation is being decided, the call is `CONFLICT` with `details.reason: 'extend-in-flight'` and `retryAfterSeconds`.

Emits `reservation.extended` with the new `end`.

* **Kind:** mutation
* **Authorization:** `compute:reservations:extend` @ `compute-reservation`
* **Module:** `capacity`
* **Phase:** v0.1 (registered)

**Input**

| Field            | Type           | Required | Constraints                                                      |
| ---------------- | -------------- | -------- | ---------------------------------------------------------------- |
| `tenantId`       | string         | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$`       |
| `reservationId`  | string         | yes      | at most 56 characters; matches `^rsv_[0-9a-hjkmnp-tv-z]{26,52}$` |
| `until`          | integer        |          | 0-140737488355328                                                |
| `by`             | string,integer |          |                                                                  |
| `idempotencyKey` | string         | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$`       |

## renew [#renew]

`POST /api/compute/reservations/renew`

Moves the lease deadline of a renewable reservation to now plus its `leaseSeconds`, without changing its window; returns the reservation.

A reservation created without `leaseSeconds` is `INVALID_STATE`, as is one not in `placed`, `binding` or `active`. A lease that lapses without renewal releases the reservation with reason `lease-lapsed`. It is decided as `compute:reservations:extend`. Renewing emits no event.

* **Kind:** mutation
* **Authorization:** `compute:reservations:extend` @ `compute-reservation`
* **Module:** `capacity`
* **Phase:** v0.1 (registered)

**Input**

| Field           | Type   | Required | Constraints                                                      |
| --------------- | ------ | -------- | ---------------------------------------------------------------- |
| `tenantId`      | string | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$`       |
| `reservationId` | string | yes      | at most 56 characters; matches `^rsv_[0-9a-hjkmnp-tv-z]{26,52}$` |
