---
title: "$client->bookings"
description: "Every method in this namespace: its signature, its parameters, what it returns and an example."
url: "https://nextmail.sh/docs/php/reference/bookings"
area: "PHP"
category: "Reference"
---

# $client->bookings

Every method in this namespace: its signature, its parameters, what it returns and an example.

## Methods

Public links where somebody books a meeting on a person's calendar: create a booking page, change its hours, questions and rules, pause or delete it, read the times it offers, list the meetings guests booked, and approve, decline or cancel one. An API key acts for the workspace owner, and an app for the person who connected it.

### `bookings->listPages`

List the booking pages of the person

```php
listPages(
    ?string $q = null,
    ?string $status = null,
    DateTimeInterface|string|null $since = null,
    DateTimeInterface|string|null $until = null,
    ?string $sort = null,
    ?string $apiKey = null,
): array
```

Returns the booking pages of the person, oldest first unless `sort:` says otherwise. `q:` searches the name and the link, `status:` keeps the active or the paused pages, and `since:` and `until:` keep the pages made in a window. A booking page is a public link, `/book/<slug>`, where somebody picks a free time and books a meeting on the calendar of that person. Each page comes with its settings in `config`, the link to hand to guests in `url`, and in `counts` how many of its bookings are upcoming and how many wait for approval.

`status` is `active` while the page takes bookings and `paused` while its link works but offers no times. `problem` says what stops a page from taking bookings and is null when nothing does. `away` lists the out of office periods that close the page. A person holds at most 25 pages, so the list comes whole and is never paged.

Scopes: `calendar:read`.

**Parameters**

- `q` (`string`): Only the pages whose name or link contains this text, at most 200 characters. Case and accents are ignored.
- `status` (`string`): `active` for the pages that take bookings, `paused` for the ones that do not, as in `BookingPageStatuses`. Left out, both.
- `since` (`string|DateTimeInterface`): Only the pages made at or after this instant, as a `DateTimeInterface` or an ISO 8601 string.
- `until` (`string|DateTimeInterface`): Only the pages made before this instant, later than `since:`.
- `sort` (`string`): `oldest` (the default) or `newest` by when the page was made, `name` from A to Z, or `upcoming` with the most upcoming meetings first, as in `BookingPageSorts`.
- `apiKey` (`string`): Overrides the client API key for this call only.

**Returns**

A list of booking page arrays. Each item has `id`, `slug`, `name`, `status`, `url`, `problem`, `config`, `counts`, `away`, `createdAt` and `updatedAt`.

**Example**

```php
foreach ($client->bookings->listPages() as $page) {
    echo $page['name'], ' ', $page['url'], ' is ', $page['status'], ': ', $page['counts']['upcoming'], ' upcoming, ', $page['counts']['pending'], ' waiting', PHP_EOL;
}
```

**Notes**

- A booking page belongs to one person. An API key acts as the workspace owner and reaches the pages of the owner, and a connected app reaches the pages of the person who connected it.
- `problem` is `sender` when `config.fromEmail` can no longer send, `hours` when no hours are open at all, and `host` when the person left the workspace.
- A key or an app limited to particular addresses or domains is refused with 422 `capability_unsupported`, because a booking page reads the whole calendar of its person.
- An `until:` that is not later than `since:` is 422 `invalid_parameter`.

Also available in: API [`GET /booking-pages`](https://nextmail.sh/docs/api/reference/booking-pages#get-booking-pages); TypeScript [`nextmail.bookings.listPages()`](https://nextmail.sh/docs/sdk/reference/bookings#listPages); Python [`bookings.list_pages()`](https://nextmail.sh/docs/python/reference/bookings#listPages); Ruby [`bookings.list_pages`](https://nextmail.sh/docs/ruby/reference/bookings#listPages); Go [`Bookings.ListPages`](https://nextmail.sh/docs/go/reference/bookings#listPages); Java [`bookings().listPages`](https://nextmail.sh/docs/java/reference/bookings#listPages); C# [`Bookings.ListPagesAsync`](https://nextmail.sh/docs/csharp/reference/bookings#listPages); CLI [`nextmail bookings list-pages`](https://nextmail.sh/docs/cli/reference/bookings#bookings-list-pages).

### `bookings->getPage`

Read one booking page by id

```php
getPage(string $id, ?string $apiKey = null): array
```

Looks up a single booking page and returns it in the same shape as a row of `listPages`, with every setting in `config`: the meeting lengths, where the meeting happens, the weekly hours and the dates with their own hours, the notice, the buffers and the daily limit, whether a booking waits for approval, the address its emails come from, the questions a guest is asked, how the page looks and what a guest may change afterwards.

`config.availability.weekly` holds seven lists, Sunday first, of the hours open on that weekday, each as minutes after midnight in `config.availability.timezone`. `problem` says what stops the page from taking bookings, and `away` lists the out of office periods of `config.fromEmail` that are running or still to come. While `config.busy.outOfOffice` is true the page offers no times inside them.

Scopes: `calendar:read`.

**Parameters**

- `id` (`string`, required): Booking page id such as `bkp_5c1e9a7b3d2f48e6a0b4c7d1`, as `listPages` returns it.
- `apiKey` (`string`): Overrides the client API key for this call only.

**Returns**

An array with `id`, `slug`, `name`, `status`, `url`, `problem`, `config`, `counts`, `away`, `createdAt` and `updatedAt`.

**Example**

```php
$page = $client->bookings->getPage('bkp_5c1e9a7b3d2f48e6a0b4c7d1');

echo $page['url'], ' offers ', implode(', ', $page['config']['durations']), ' minute meetings in ', $page['config']['availability']['timezone'], PHP_EOL;
echo $page['problem'] === null ? 'Taking bookings' : 'Stopped by ' . $page['problem'], PHP_EOL;
```

**Notes**

- A missing page throws `ApiException` with `isNotFound()` true and code `booking_page_not_found`. A page that belongs to somebody else in the workspace is missing too.
- A booking page belongs to one person. An API key acts as the workspace owner and reaches the pages of the owner, and a connected app reaches the pages of the person who connected it.
- A key or an app limited to particular addresses or domains is refused with 422 `capability_unsupported`, because a booking page reads the whole calendar of its person.

Also available in: API [`GET /booking-pages/{id}`](https://nextmail.sh/docs/api/reference/booking-pages#get-booking-pages-id); TypeScript [`nextmail.bookings.getPage()`](https://nextmail.sh/docs/sdk/reference/bookings#getPage); Python [`bookings.get_page()`](https://nextmail.sh/docs/python/reference/bookings#getPage); Ruby [`bookings.get_page`](https://nextmail.sh/docs/ruby/reference/bookings#getPage); Go [`Bookings.GetPage`](https://nextmail.sh/docs/go/reference/bookings#getPage); Java [`bookings().getPage`](https://nextmail.sh/docs/java/reference/bookings#getPage); C# [`Bookings.GetPageAsync`](https://nextmail.sh/docs/csharp/reference/bookings#getPage); CLI [`nextmail bookings get-page`](https://nextmail.sh/docs/cli/reference/bookings#bookings-get-page).

### `bookings->createPage`

Create a booking page

```php
createPage(array $body, ?string $apiKey = null): array
```

Creates a booking page and returns it as it is stored. The page takes bookings at once, at the link in `url`. Only `name` is required: the page starts with weekdays from 09:00 to 17:00, 30 minute meetings, four hours of notice, times up to 60 days ahead and the first address the person may send from. Anything in `config` replaces that default, and what it leaves out keeps it.

`slug` is the last part of the public link, `/book/<slug>`. Left out, it is made from the name. `timezone` says which time zone the working hours are read in when `config.availability.timezone` is not sent.

The times a page offers are its working hours with everything taken out that makes the person busy: their calendar events, invitations they have not declined, calendars they subscribed to, bookings they already have, the gaps the page keeps around meetings, and the days they are out of office. A booking is a calendar event organised by `config.fromEmail`, and the guest is told by email from that address, so the call needs `emails:send` beside `calendar:write`.

Scopes: `calendar:write`, `emails:send`.

**Parameters**

- `name` (`string`, required): What the page is called, shown to guests as its heading, such as `Intro call`. Trimmed, 1 to 80 characters.
- `slug` (`string`): The last part of the public link: 3 to 60 lowercase letters and digits, with single hyphens between words. It is unique across Nextmail. Left out, it is made from the name.
- `timezone` (`string`): The IANA time zone the working hours are read in, such as `Europe/Berlin`, when `config.availability.timezone` is not sent. Defaults to `UTC`.
- `config.description` (`string`): Text shown under the name of the page, up to 2,000 characters. Basic Markdown works: bold, italic, links and lists. Defaults to none.
- `config.hostName` (`string`): The name guests see as the person they meet, up to 80 characters. Defaults to the name of the account.
- `config.durations` (`int[]`): The meeting lengths a guest can pick, in minutes: 1 to 4 of them, each a multiple of 5 between 5 and 720. Defaults to `[30]`.
- `config.location.kind` (`string`): Where the meeting happens: `video` with a fixed link in `value`, `phone`, where the guest is asked for a number, `in-person` with the address in `value`, or `none`. Defaults to `none`.
- `config.location.value` (`string`): The `https://` link of a `video` meeting or the address of an `in-person` one, up to 500 characters. It is dropped for `phone` and `none`.
- `config.availability.timezone` (`string`): The IANA time zone the hours of the page are read in. Defaults to `timezone`.
- `config.availability.weekly` (`array`): Seven lists, Sunday first, of the hours open on that weekday. Each range is an array with `start` and `end` in minutes after midnight, so `['start' => 540, 'end' => 1020]` is 09:00 to 17:00. An empty list closes the day, and a day takes at most 6 ranges. Defaults to 09:00 to 17:00 from Monday to Friday.
- `config.availability.overrides` (`array`): Dates with their own hours, at most 180, each an array with `date` and `ranges`. `date` is `YYYY-MM-DD` in the time zone of the page, and an empty `ranges` closes that date. Defaults to none.
- `config.rules.noticeMinutes` (`int`): How many minutes ahead a booking has to be made, from 0 to 43,200. Defaults to 240.
- `config.rules.horizonDays` (`int`): How many days ahead the page offers times, from 1 to 365. Defaults to 60.
- `config.rules.intervalMinutes` (`int|null`): Minutes between two start times: 5, 10, 15, 20, 30, 45, 60, 90 or 120. Null, the default, steps by the length of the meeting.
- `config.rules.bufferBeforeMinutes` (`int`): Minutes kept free before every meeting, from 0 to 240. Defaults to 0.
- `config.rules.bufferAfterMinutes` (`int`): Minutes kept free after every meeting, from 0 to 240. Defaults to 0.
- `config.rules.dailyLimit` (`int|null`): The most bookings the page takes on one day, from 1 to 50. Null, the default, sets no limit.
- `config.approval` (`bool`): True holds each new booking as a request until `approve` or `decline` answers it. False, the default, confirms it at once.
- `config.fromEmail` (`string`): The address the booking emails are sent from and the calendar events are organised by. It has to be one the owner of the page may send from. Defaults to the first such address.
- `config.notifyHost` (`bool`): Whether `fromEmail` gets an email about each new, moved or cancelled booking. Defaults to true.
- `config.questions` (`array`): What the guest is asked besides their name and email, at most 10, each an array with `id`, `kind`, `label`, `required`, `options` and `placeholder`. `id` is a stable key for the answers: 4 to 16 lowercase letters and digits, unique on the page. `kind` is `text`, `long-text`, `phone`, `choice` or `checkbox`. `options` holds the answers of a `choice` question, from 2 to 12, and is empty for the other kinds. `placeholder` is optional example text shown in the empty field of a `text`, `long-text` or `phone` question, up to 160 characters. Defaults to none.
- `config.confirmation` (`string`): Text shown to the guest after booking and added to the confirmation email, up to 1,000 characters. Defaults to none.
- `config.redirectUrl` (`string`): An `https://` page to send the guest to after booking, or an empty string for none, which is the default.
- `config.appearance.accent` (`string`): The colour of buttons and the picked day, as `#rrggbb`. Defaults to `#18181b`.
- `config.appearance.theme` (`string`): `auto` follows the device of the guest, and `light` or `dark` forces one. Defaults to `auto`.
- `config.appearance.showAvatar` (`bool`): Whether the page shows the photo of the person, or their initials when they have none. Defaults to true.
- `config.appearance.background` (`string`): What fills the page behind the card: `default`, one of the presets `dusk`, `mist`, `sand`, `night`, `meadow`, `dots`, `brush`, `trail`, `ember`, `canvas`, `bloom`, `field`, `wanderer` and `gold`, or a colour as `#rrggbb`. Defaults to `default`. `image` shows the photo uploaded with `uploadBackground`, so a new page cannot start with it.
- `config.appearance.fonts.primary` (`string|null`): The font of the text: one of the ids in `Nextmail\Constants\BrandFontIds`, such as `inter` or `georgia`, or null for the default. Defaults to null.
- `config.appearance.fonts.secondary` (`string|null`): The font of the title, such as `instrument-serif`, or null to keep the title in the font of the text. Defaults to null.
- `config.guestChanges.cancel` (`bool`): Whether a guest may cancel from the link in their emails. Defaults to true.
- `config.guestChanges.reschedule` (`bool`): Whether a guest may move the meeting from the link in their emails. Defaults to true.
- `config.guestChanges.cutoffMinutes` (`int`): How many minutes before the meeting those changes close, from 0 to 20,160. Defaults to 0.
- `config.busy.addresses` (`string[]|null`): The addresses whose invitations block time unless declined, at most 50. Null, the default, checks every address the person holds.
- `config.busy.subscriptions` (`bool`): Whether events from calendars the person subscribed to block time. Defaults to true.
- `config.busy.outOfOffice` (`bool`): Whether the page offers no times while `fromEmail` is out of office. Defaults to true.
- `config.seo.title` (`string`): The title of the browser tab and of a search result, up to 120 characters. Empty, the default, uses the name of the page and the host.
- `config.seo.description` (`string`): The summary under the title in a search result, up to 300 characters. Empty, the default, uses the start of the description.
- `config.seo.indexable` (`bool`): Whether search engines may list the page. Defaults to false, which asks them not to.
- `config.extraGuests` (`int`): How many other people a guest may add to a booking, 0 to 10. Each one gets the invitation and every later change. Defaults to 0, which hides the field.
- `config.reminders.guest` (`list<int>`): When the guest gets a reminder email, in minutes before the meeting, up to three of 10, 30, 60, 120, 1440, 2880 and 10080. Defaults to one day before, and an empty list sends none.
- `config.reminders.host` (`list<int>`): The reminders each booked meeting carries on your own calendar, in minutes before it, up to three of 0, 5, 10, 15, 30, 60 and 1440. Defaults to 10.
- `apiKey` (`string`): Overrides the client API key for this call only.

**Returns**

An array with the new `id`, `status` `active`, the `slug` and `url` of its link, and `config` with every default filled in.

**Example**

```php
use Nextmail\Constants\BookingLocationKinds;

$page = $client->bookings->createPage([
    'name' => 'Intro call',
    'timezone' => 'Europe/Berlin',
    'config' => [
        'description' => 'A first conversation about your project.',
        'durations' => [30, 60],
        'location' => ['kind' => BookingLocationKinds::VIDEO, 'value' => 'https://meet.example.com/ada'],
        'rules' => ['bufferAfterMinutes' => 15],
        'approval' => true,
    ],
]);

echo $page['id'], ' ', $page['url'], ' sends from ', $page['config']['fromEmail'], PHP_EOL;
```

**Notes**

- A booking page belongs to one person. An API key acts as the workspace owner and makes a page of the owner, and a connected app makes a page of the person who connected it.
- A person holds at most 25 pages. One more is a 403 `booking_page_limit_reached`.
- A `slug` another page already uses is a 409 `booking_link_taken`. A slug that is not a valid link, a `fromEmail` the person may not send from, a `video` location without a link, an `in-person` one without an address and two questions with the same `id` are 422 `invalid_booking_page`, with the reason in the message. A few slugs such as `manage` and `admin` are reserved.
- A value of the wrong type or length, a range that ends before it starts and a time zone in `config.availability` that is not known are 422 `invalid_parameter` with `param` naming the field. A key beside `name`, `slug`, `timezone` and `config` is 422 `unknown_parameter`.
- A key or an app limited to particular addresses or domains is refused with 422 `capability_unsupported`, because a booking page reads the whole calendar of its person.
- Not retried automatically. A slug made from the name gets a few random characters when the plain one is taken, so a retry after a lost response can leave two pages. List them and delete the spare.

Also available in: API [`POST /booking-pages`](https://nextmail.sh/docs/api/reference/booking-pages#post-booking-pages); TypeScript [`nextmail.bookings.createPage()`](https://nextmail.sh/docs/sdk/reference/bookings#createPage); Python [`bookings.create_page()`](https://nextmail.sh/docs/python/reference/bookings#createPage); Ruby [`bookings.create_page`](https://nextmail.sh/docs/ruby/reference/bookings#createPage); Go [`Bookings.CreatePage`](https://nextmail.sh/docs/go/reference/bookings#createPage); Java [`bookings().createPage`](https://nextmail.sh/docs/java/reference/bookings#createPage); C# [`Bookings.CreatePageAsync`](https://nextmail.sh/docs/csharp/reference/bookings#createPage); CLI [`nextmail bookings create-page`](https://nextmail.sh/docs/cli/reference/bookings#bookings-create-page).

### `bookings->updatePage`

Change the name, the link, the status or any setting of a booking page

```php
updatePage(string $id, array $patch, ?string $apiKey = null): array
```

Changes a booking page and returns it as it is stored. Send only what changes: a field left out stays as it is, and a patch with nothing in it is a 422.

`config` is merged, not replaced. A top-level value such as `approval` or `fromEmail` replaces the stored one. Inside `location`, `availability`, `rules`, `appearance`, `guestChanges`, `busy` and `seo` each field is merged on its own, so `'rules' => ['dailyLimit' => 4]` leaves the notice and the buffers alone. A list is replaced whole: send every entry of `durations`, `questions`, `availability.weekly` or `availability.overrides` again, not only the ones that change.

Set `status` to `paused` to stop offering times without losing the link, and to `active` to offer them again. Bookings already made keep their time whatever changes. A new `slug` moves the page to a new link and the old one stops working, so links already handed out break.

Scopes: `calendar:write`, `emails:send`.

**Parameters**

- `id` (`string`, required): Booking page id such as `bkp_5c1e9a7b3d2f48e6a0b4c7d1`, as `listPages` returns it.
- `name` (`string`): New name, shown to guests as the heading of the page. Trimmed, 1 to 80 characters. Left out, the name stays.
- `slug` (`string`): New last part of the public link: 3 to 60 lowercase letters and digits, with single hyphens between words. It is unique across Nextmail. Left out, the link stays.
- `status` (`string`): `active` takes bookings. `paused` keeps the link working but offers no times. Left out, it stays.
- `config.description` (`string`): Text shown under the name of the page, up to 2,000 characters. Basic Markdown works: bold, italic, links and lists. An empty string clears it.
- `config.hostName` (`string`): The name guests see as the person they meet, up to 80 characters.
- `config.durations` (`int[]`): The meeting lengths a guest can pick, in minutes, replacing the list the page had: 1 to 4 of them, each a multiple of 5 between 5 and 720.
- `config.location.kind` (`string`): Where the meeting happens: `video` with a fixed link in `value`, `phone`, where the guest is asked for a number, `in-person` with the address in `value`, or `none`.
- `config.location.value` (`string`): The `https://` link of a `video` meeting or the address of an `in-person` one, up to 500 characters. It is dropped for `phone` and `none`.
- `config.availability.timezone` (`string`): The IANA time zone the hours of the page are read in, such as `Europe/Berlin`.
- `config.availability.weekly` (`array`): All seven lists again, Sunday first, of the hours open on that weekday. Each range is an array with `start` and `end` in minutes after midnight, so `['start' => 540, 'end' => 1020]` is 09:00 to 17:00. An empty list closes the day, and a day takes at most 6 ranges.
- `config.availability.overrides` (`array`): Every date with its own hours again, at most 180, each an array with `date` and `ranges`. `date` is `YYYY-MM-DD` in the time zone of the page, and an empty `ranges` closes that date. An empty array removes them all.
- `config.rules.noticeMinutes` (`int`): How many minutes ahead a booking has to be made, from 0 to 43,200.
- `config.rules.horizonDays` (`int`): How many days ahead the page offers times, from 1 to 365.
- `config.rules.intervalMinutes` (`int|null`): Minutes between two start times: 5, 10, 15, 20, 30, 45, 60, 90 or 120. Null steps by the length of the meeting.
- `config.rules.bufferBeforeMinutes` (`int`): Minutes kept free before every meeting, from 0 to 240.
- `config.rules.bufferAfterMinutes` (`int`): Minutes kept free after every meeting, from 0 to 240.
- `config.rules.dailyLimit` (`int|null`): The most bookings the page takes on one day, from 1 to 50. Null sets no limit.
- `config.approval` (`bool`): True holds each new booking as a request until `approve` or `decline` answers it. False confirms it at once. Requests already waiting keep waiting.
- `config.fromEmail` (`string`): The address the booking emails are sent from and the calendar events are organised by. It has to be one the owner of the page may send from.
- `config.notifyHost` (`bool`): Whether `fromEmail` gets an email about each new, moved or cancelled booking.
- `config.questions` (`array`): Every question again, at most 10, each an array with `id`, `kind`, `label`, `required`, `options` and `placeholder`. `id` is a stable key for the answers: 4 to 16 lowercase letters and digits, unique on the page. `kind` is `text`, `long-text`, `phone`, `choice` or `checkbox`. `options` holds the answers of a `choice` question, from 2 to 12, and is empty for the other kinds. `placeholder` is optional example text shown in the empty field of a `text`, `long-text` or `phone` question, up to 160 characters. An empty array asks nothing more.
- `config.confirmation` (`string`): Text shown to the guest after booking and added to the confirmation email, up to 1,000 characters. An empty string clears it.
- `config.redirectUrl` (`string`): An `https://` page to send the guest to after booking, or an empty string for none.
- `config.appearance.accent` (`string`): The colour of buttons and the picked day, as `#rrggbb`.
- `config.appearance.theme` (`string`): `auto` follows the device of the guest, and `light` or `dark` forces one.
- `config.appearance.showAvatar` (`bool`): Whether the page shows the photo of the person, or their initials when they have none.
- `config.appearance.background` (`string`): What fills the page behind the card: `default`, one of the presets `dusk`, `mist`, `sand`, `night`, `meadow`, `dots`, `brush`, `trail`, `ember`, `canvas`, `bloom`, `field`, `wanderer` and `gold`, a colour as `#rrggbb`, or `image` for the photo uploaded with `uploadBackground`. `image` without an uploaded photo is a 422 `invalid_booking_page`.
- `config.appearance.fonts.primary` (`string|null`): The font of the text: one of the ids in `Nextmail\Constants\BrandFontIds`, such as `inter` or `georgia`, or null for the default. Left out, it keeps its value.
- `config.appearance.fonts.secondary` (`string|null`): The font of the title, such as `instrument-serif`, or null to keep the title in the font of the text. Left out, it keeps its value.
- `config.guestChanges.cancel` (`bool`): Whether a guest may cancel from the link in their emails.
- `config.guestChanges.reschedule` (`bool`): Whether a guest may move the meeting from the link in their emails.
- `config.guestChanges.cutoffMinutes` (`int`): How many minutes before the meeting those changes close, from 0 to 20,160.
- `config.busy.addresses` (`string[]|null`): The addresses whose invitations block time unless declined, at most 50. Null checks every address the person holds.
- `config.busy.subscriptions` (`bool`): Whether events from calendars the person subscribed to block time.
- `config.busy.outOfOffice` (`bool`): Whether the page offers no times while `fromEmail` is out of office.
- `config.seo.title` (`string`): The title of the browser tab and of a search result, up to 120 characters. An empty string goes back to the name of the page and the host.
- `config.seo.description` (`string`): The summary under the title in a search result, up to 300 characters. An empty string goes back to the start of the description.
- `config.seo.indexable` (`bool`): Whether search engines may list the page.
- `config.extraGuests` (`int`): How many other people a guest may add to a booking, 0 to 10. Each one gets the invitation and every later change. 0 hides the field.
- `config.reminders.guest` (`list<int>`): When the guest gets a reminder email, in minutes before the meeting, up to three of 10, 30, 60, 120, 1440, 2880 and 10080. An empty list sends none.
- `config.reminders.host` (`list<int>`): The reminders each new booking carries on your own calendar, in minutes before it, up to three of 0, 5, 10, 15, 30, 60 and 1440.
- `apiKey` (`string`): Overrides the client API key for this call only.

**Returns**

An array with the unchanged `id`, and everything else as it is now.

**Example**

```php
$page = $client->bookings->updatePage('bkp_5c1e9a7b3d2f48e6a0b4c7d1', [
    'config' => [
        'durations' => [45],
        'rules' => ['bufferAfterMinutes' => 15, 'dailyLimit' => 4],
    ],
]);

echo implode(', ', $page['config']['durations']), ' minute meetings, at most ', $page['config']['rules']['dailyLimit'], ' a day', PHP_EOL;
```

**Notes**

- A missing page is a 404 `booking_page_not_found`, and so is a page that belongs to somebody else in the workspace.
- A `slug` another page already uses is a 409 `booking_link_taken`. A slug that is not a valid link, a `fromEmail` the person may not send from, a `video` location without a link, an `in-person` one without an address and two questions with the same `id` are 422 `invalid_booking_page`, with the reason in the message.
- An empty patch, a value of the wrong type or length, a range that ends before it starts and a time zone that is not known are 422 `invalid_parameter`. A key beside `name`, `slug`, `status` and `config` is 422 `unknown_parameter`.
- A booking page belongs to one person. An API key acts as the workspace owner and reaches the pages of the owner, and a connected app reaches the pages of the person who connected it.
- A key or an app limited to particular addresses or domains is refused with 422 `capability_unsupported`, because a booking page reads the whole calendar of its person.
- The SDK retries this call after a network failure, since the same patch sent twice leaves the same page.

Also available in: API [`PATCH /booking-pages/{id}`](https://nextmail.sh/docs/api/reference/booking-pages#patch-booking-pages-id); TypeScript [`nextmail.bookings.updatePage()`](https://nextmail.sh/docs/sdk/reference/bookings#updatePage); Python [`bookings.update_page()`](https://nextmail.sh/docs/python/reference/bookings#updatePage); Ruby [`bookings.update_page`](https://nextmail.sh/docs/ruby/reference/bookings#updatePage); Go [`Bookings.UpdatePage`](https://nextmail.sh/docs/go/reference/bookings#updatePage); Java [`bookings().updatePage`](https://nextmail.sh/docs/java/reference/bookings#updatePage); C# [`Bookings.UpdatePageAsync`](https://nextmail.sh/docs/csharp/reference/bookings#updatePage); CLI [`nextmail bookings update-page`](https://nextmail.sh/docs/cli/reference/bookings#bookings-update-page).

### `bookings->uploadBackground`

Upload the background photo of a booking page

```php
uploadBackground(
    string $id,
    mixed $data,
    ?string $contentType = null,
    ?string $apiKey = null,
): array
```

Sends the image bytes as the request body, replacing the photo there was, and returns the page. The photo fills the public page behind the card: `config.appearance.background` becomes `image` and `config.appearance.backgroundImage` holds the address of the stored photo.

Up to 5 MB goes in, and it is fitted into 2560 by 1600 pixels and stored as WebP. It takes PNG, JPEG, WebP or GIF, and an animated image keeps its first frame. The type is read from `contentType:`, or when that is left out from the media type of a PSR-7 uploaded file or the extension of the file name of an `SplFileInfo`, an uploaded file or a stream opened with `fopen`, and without either the server refuses the bytes with 422 `invalid_image`.

To go back to a preset or a colour and keep the photo for later, set `config.appearance.background` with `updatePage`. `removeBackground` deletes the photo.

Scopes: `calendar:write`, `emails:send`.

**Parameters**

- `id` (`string`, required): Booking page id such as `bkp_5c1e9a7b3d2f48e6a0b4c7d1`, as `listPages` returns it.
- `data` (`string|resource|SplFileInfo|StreamInterface`, required): The image: a string of bytes, a stream resource from `fopen`, an `SplFileInfo`, or a PSR-7 stream or uploaded file.
- `contentType` (`string`): `image/png`, `image/jpeg`, `image/webp` or `image/gif`. Required unless `data` carries its type: a PSR-7 uploaded file with a media type, or a file or stream whose name ends in `.png`, `.jpg`, `.jpeg`, `.webp` or `.gif`.
- `apiKey` (`string`): Overrides the client API key for this call only.

**Returns**

The same array as `bookings->getPage`, with the address of the new photo in `config.appearance.backgroundImage`.

**Example**

```php
$page = $client->bookings->uploadBackground('bkp_5c1e9a7b3d2f48e6a0b4c7d1', fopen('photo.jpg', 'rb'));

echo 'Background: ', $page['config']['appearance']['background'], PHP_EOL;
echo 'Photo: ', $page['config']['appearance']['backgroundImage'], PHP_EOL;

$client->bookings->uploadBackground(
    'bkp_5c1e9a7b3d2f48e6a0b4c7d1',
    file_get_contents('photo.jpg'),
    contentType: 'image/jpeg',
);
```

**Notes**

- An SVG, an image over 5 MB or one that cannot be read is refused with 422 `invalid_image`.
- A missing page is a 404 `booking_page_not_found`, and so is a page that belongs to somebody else in the workspace.
- A key or an app limited to particular addresses or domains is refused with 422 `capability_unsupported`, because a booking page reads the whole calendar of its person.
- A busy image service answers 503 `image_busy` and a failed save 502 `image_not_stored`, which the SDK retries like any other retryable status, since the same image sent twice leaves the same page.

Also available in: API [`PUT /booking-pages/{id}/background`](https://nextmail.sh/docs/api/reference/booking-pages#put-booking-pages-id-background); TypeScript [`nextmail.bookings.uploadBackground()`](https://nextmail.sh/docs/sdk/reference/bookings#uploadBackground); Python [`bookings.upload_background()`](https://nextmail.sh/docs/python/reference/bookings#uploadBackground); Ruby [`bookings.upload_background`](https://nextmail.sh/docs/ruby/reference/bookings#uploadBackground); Go [`Bookings.UploadBackground`](https://nextmail.sh/docs/go/reference/bookings#uploadBackground); Java [`bookings().uploadBackground`](https://nextmail.sh/docs/java/reference/bookings#uploadBackground); C# [`Bookings.UploadBackgroundAsync`](https://nextmail.sh/docs/csharp/reference/bookings#uploadBackground); CLI [`nextmail bookings upload-background`](https://nextmail.sh/docs/cli/reference/bookings#bookings-upload-background).

### `bookings->removeBackground`

Remove the background photo of a booking page

```php
removeBackground(string $id, ?string $apiKey = null): array
```

Deletes the stored background photo of a booking page and returns the page. A page that showed the photo goes back to the `default` background, and one that showed a preset or a colour keeps it. Removing a photo that is not set changes nothing.

Scopes: `calendar:write`, `emails:send`.

**Parameters**

- `id` (`string`, required): Booking page id such as `bkp_5c1e9a7b3d2f48e6a0b4c7d1`, as `listPages` returns it.
- `apiKey` (`string`): Overrides the client API key for this call only.

**Returns**

The same array as `bookings->getPage`, with `config.appearance.backgroundImage` null.

**Example**

```php
$page = $client->bookings->removeBackground('bkp_5c1e9a7b3d2f48e6a0b4c7d1');

echo 'Background: ', $page['config']['appearance']['background'], PHP_EOL;
echo $page['config']['appearance']['backgroundImage'] ?? 'No photo is stored', PHP_EOL;
```

**Notes**

- A missing page is a 404 `booking_page_not_found`, and so is a page that belongs to somebody else in the workspace.
- A key or an app limited to particular addresses or domains is refused with 422 `capability_unsupported`, because a booking page reads the whole calendar of its person.
- Retried automatically on network failure and retryable statuses, since a second attempt after one that went through finds nothing to remove.

Also available in: API [`DELETE /booking-pages/{id}/background`](https://nextmail.sh/docs/api/reference/booking-pages#delete-booking-pages-id-background); TypeScript [`nextmail.bookings.removeBackground()`](https://nextmail.sh/docs/sdk/reference/bookings#removeBackground); Python [`bookings.remove_background()`](https://nextmail.sh/docs/python/reference/bookings#removeBackground); Ruby [`bookings.remove_background`](https://nextmail.sh/docs/ruby/reference/bookings#removeBackground); Go [`Bookings.RemoveBackground`](https://nextmail.sh/docs/go/reference/bookings#removeBackground); Java [`bookings().removeBackground`](https://nextmail.sh/docs/java/reference/bookings#removeBackground); C# [`Bookings.RemoveBackgroundAsync`](https://nextmail.sh/docs/csharp/reference/bookings#removeBackground); CLI [`nextmail bookings remove-background`](https://nextmail.sh/docs/cli/reference/bookings#bookings-remove-background).

### `bookings->deletePage`

Delete a booking page

```php
deletePage(string $id, ?string $apiKey = null): array
```

Deletes a booking page for good. Its public link stops working and the list of its bookings goes with it, so `list` no longer shows them and guests can no longer move or cancel through the links in their emails. The meetings already booked stay on the calendar, where `calendar->cancelEvent` can still call one off.

There is no undo. To stop offering times and keep the page, its link and its bookings, use `updatePage` with `status` `paused`.

Scopes: `calendar:write`.

**Parameters**

- `id` (`string`, required): Booking page id such as `bkp_5c1e9a7b3d2f48e6a0b4c7d1`, as `listPages` returns it.
- `apiKey` (`string`): Overrides the client API key for this call only.

**Returns**

An array with `object` set to `booking_page`, the `id` and `deleted` set to true.

**Example**

```php
$removed = $client->bookings->deletePage('bkp_5c1e9a7b3d2f48e6a0b4c7d1');

echo $removed['id'], $removed['deleted'] ? ' is gone' : ' is still there', PHP_EOL;
```

**Notes**

- It sends no email, so it needs `calendar:write` alone. Guests are not told that the page is gone.
- The SDK does not retry a delete. A 404 `booking_page_not_found` on your own second attempt after a lost response means the first one worked.
- A booking page belongs to one person. An API key acts as the workspace owner and reaches the pages of the owner, and a connected app reaches the pages of the person who connected it.
- A key or an app limited to particular addresses or domains is refused with 422 `capability_unsupported`, because a booking page reads the whole calendar of its person.

Also available in: API [`DELETE /booking-pages/{id}`](https://nextmail.sh/docs/api/reference/booking-pages#delete-booking-pages-id); TypeScript [`nextmail.bookings.deletePage()`](https://nextmail.sh/docs/sdk/reference/bookings#deletePage); Python [`bookings.delete_page()`](https://nextmail.sh/docs/python/reference/bookings#deletePage); Ruby [`bookings.delete_page`](https://nextmail.sh/docs/ruby/reference/bookings#deletePage); Go [`Bookings.DeletePage`](https://nextmail.sh/docs/go/reference/bookings#deletePage); Java [`bookings().deletePage`](https://nextmail.sh/docs/java/reference/bookings#deletePage); C# [`Bookings.DeletePageAsync`](https://nextmail.sh/docs/csharp/reference/bookings#deletePage); CLI [`nextmail bookings delete-page`](https://nextmail.sh/docs/cli/reference/bookings#bookings-delete-page).

### `bookings->listTimes`

List the times a booking page offers

```php
listTimes(
    string $id,
    DateTimeInterface|string|null $from = null,
    DateTimeInterface|string|null $to = null,
    ?int $durationMinutes = null,
    ?string $bookingId = null,
    ?string $apiKey = null,
): array
```

Returns the start times a guest could book on a page inside a window, in order. They are the hours of the page with everything taken out that makes the person busy: their calendar events, invitations they have not declined, calendars they subscribed to, bookings they already have, the gaps the page keeps around meetings, its daily limit, its notice and the out of office periods that close it.

The window starts at `from:`, or now, and ends at `to:`, or 14 days later. It covers at most 62 days, and a later `to:` is cut back to that. `durationMinutes:` picks which meeting length the times are for and has to be one of the `durations` of the page. Left out, the shortest is used. Both bounds take a `DateTimeInterface` or an ISO 8601 string, and the SDK sends a `DateTimeInterface` as an ISO 8601 instant in UTC.

With `bookingId:` the times are the ones that booking can move to with `reschedule`: its own time counts as free, and they are for its length.

A paused page still answers, so the times can be checked before it is turned on. A page with a `problem` offers nothing.

Scopes: `calendar:read`.

**Parameters**

- `id` (`string`, required): Booking page id such as `bkp_5c1e9a7b3d2f48e6a0b4c7d1`, as `listPages` returns it.
- `from` (`string|DateTimeInterface`): The start of the window. Defaults to now.
- `to` (`string|DateTimeInterface`): The end of the window, later than `from:`. Defaults to 14 days after `from:`, and counts for at most 62 days after it.
- `durationMinutes` (`int`): The meeting length in minutes, one of the `durations` of the page. Defaults to the shortest.
- `bookingId` (`string`): A booking on this page to find a new time for, such as `bkg_2b8d4f6a1c3e5079b6d8f0a2`. Its own time counts as free and the times are for its length, so `durationMinutes:` is ignored.
- `apiKey` (`string`): Overrides the client API key for this call only.

**Returns**

An array with `pageId`, the `timezone` of the page, the `durationMinutes` the times are for, and `times`, every open start time in the window as an ISO 8601 instant in UTC.

**Example**

```php
$open = $client->bookings->listTimes(
    'bkp_5c1e9a7b3d2f48e6a0b4c7d1',
    from: new \DateTimeImmutable('now'),
    to: new \DateTimeImmutable('+7 days'),
    durationMinutes: 30,
);

foreach ($open['times'] as $time) {
    echo $time, PHP_EOL;
}
```

**Notes**

- A `to:` that is not later than `from:` and a `durationMinutes:` the page does not offer are 422 `invalid_parameter`. A missing page is a 404 `booking_page_not_found`, and a `bookingId:` that is not a booking of this page a 404 `booking_not_found`.
- The times are instants, so show them to a person in a time zone: `timezone` names the one the hours of the page are read in.
- It reads the calendar as it is now and holds nothing. A time it lists can be gone a moment later, when a guest books it.
- A booking page belongs to one person. An API key acts as the workspace owner and reaches the pages of the owner, and a connected app reaches the pages of the person who connected it.
- A key or an app limited to particular addresses or domains is refused with 422 `capability_unsupported`, because a booking page reads the whole calendar of its person.

Also available in: API [`GET /booking-pages/{id}/times`](https://nextmail.sh/docs/api/reference/booking-pages#get-booking-pages-id-times); TypeScript [`nextmail.bookings.listTimes()`](https://nextmail.sh/docs/sdk/reference/bookings#listTimes); Python [`bookings.list_times()`](https://nextmail.sh/docs/python/reference/bookings#listTimes); Ruby [`bookings.list_times`](https://nextmail.sh/docs/ruby/reference/bookings#listTimes); Go [`Bookings.ListTimes`](https://nextmail.sh/docs/go/reference/bookings#listTimes); Java [`bookings().listTimes`](https://nextmail.sh/docs/java/reference/bookings#listTimes); C# [`Bookings.ListTimesAsync`](https://nextmail.sh/docs/csharp/reference/bookings#listTimes); CLI [`nextmail bookings list-times`](https://nextmail.sh/docs/cli/reference/bookings#bookings-list-times).

### `bookings->list`

List the meetings guests booked, a page at a time

```php
list(
    ?string $view = null,
    ?string $pageId = null,
    array|string|null $pageIds = null,
    ?string $q = null,
    DateTimeInterface|string|null $from = null,
    DateTimeInterface|string|null $to = null,
    ?int $limit = null,
    ?string $cursor = null,
    ?string $apiKey = null,
): Page
```

Returns one page of bookings: the meetings guests booked on the booking pages of the person, or on some of them with `pageIds:`. `q:` searches the guest and the page, and `from:` and `to:` keep the meetings that start in a window. `listAll` collects every page and `iterate` walks them lazily. A booking is a calendar event with the guest invited, so it shows wherever the calendar does, and a meeting moved or cancelled in the calendar reads as moved or cancelled here.

`view:` picks which bookings are listed. `upcoming`, the default, holds the confirmed meetings that are not over. `pending` holds the requests that wait for `approve` or `decline`, on a page whose `config.approval` is true. `past` holds the meetings that are over and the requests nobody answered in time, and `ended` holds the ones that were declined or cancelled. `upcoming` and `pending` come soonest first, `past` and `ended` latest first.

Each booking names its page in `pageId` and `pageName`, the guest in `guest`, what they answered in `answers` and its calendar event in `eventId`.

Scopes: `calendar:read`.

**Parameters**

- `view` (`string`): Which bookings to list: `upcoming`, `pending`, `past` or `ended`. Defaults to `upcoming`.
- `pageId` (`string`): Only the bookings of this booking page, by its id. Left out, the bookings of every page of the person are listed.
- `pageIds` (`string|string[]`): Only the bookings of these booking pages, at most 25 ids, as an array or one comma-separated string, sent comma-separated. Left out, or an empty array, every page of the person.
- `q` (`string`): Only the bookings whose guest name, guest email or page name contains this text, at most 200 characters. Case and accents are ignored.
- `from` (`string|DateTimeInterface`): Only the meetings that start at or after this instant, as a `DateTimeInterface` or an ISO 8601 string.
- `to` (`string|DateTimeInterface`): Only the meetings that start before this instant, later than `from:`.
- `limit` (`int`): Page size, from 1 to 100. The server defaults to 30.
- `cursor` (`string`): The `nextCursor` of the previous page, passed back as it came. Leave it out for the first page.
- `apiKey` (`string`): Overrides the client API key for this call only.

**Returns**

A `Page` of booking arrays, with `items`, `hasMore` and `nextCursor`. Each item has `id`, `pageId`, `pageName`, `status`, `expired`, `past`, `start`, `end`, `durationMinutes`, `guest`, `answers`, `location`, `eventId`, `endedBy`, `endReason`, `rescheduleCount`, `guestMailFailed` and `createdAt`.

**Example**

```php
use Nextmail\Constants\BookingViews;

$page = $client->bookings->list(view: BookingViews::PENDING);

foreach ($page as $booking) {
    echo $booking['id'], ' ', $booking['pageName'], ' ', $booking['guest']['name'], ' ', $booking['start'], PHP_EOL;
}
```

**Notes**

- `status` is `pending` while a booking waits for approval, `confirmed` once the meeting stands, `declined` when a request was turned down and `cancelled` when a confirmed meeting was called off. `expired` is true for a request nobody answered before its time came, and `past` once the meeting is over.
- A `pageId:` or one of `pageIds:` that is not a page of the person answers 404 `booking_page_not_found`, more than 25 `pageIds:` or a `to:` that is not later than `from:` 422 `invalid_parameter`, and a cursor this list did not hand out 400 `invalid_cursor`.
- A search with `q:` reads up to 1000 of the newest matching bookings of the view, so narrow an old view with `from:` and `to:` to search all of it.
- The cursor marks how far into the view the last page reached. A booking that enters or leaves the view between two pages shifts the rows after it, so read the view again when an exact list matters.
- An API key acts as the workspace owner and reads the bookings of the owner, and a connected app reads the bookings of the person who connected it. One guest address holds at most 3 upcoming bookings on a page.
- A key or an app limited to particular addresses or domains is refused with 422 `capability_unsupported`, because a booking page reads the whole calendar of its person.

Also available in: API [`GET /bookings`](https://nextmail.sh/docs/api/reference/booking-pages#get-bookings); TypeScript [`nextmail.bookings.list()`](https://nextmail.sh/docs/sdk/reference/bookings#list); Python [`bookings.list()`](https://nextmail.sh/docs/python/reference/bookings#list); Ruby [`bookings.list`](https://nextmail.sh/docs/ruby/reference/bookings#list); Go [`Bookings.List`](https://nextmail.sh/docs/go/reference/bookings#list); Java [`bookings().list`](https://nextmail.sh/docs/java/reference/bookings#list); C# [`Bookings.ListAsync`](https://nextmail.sh/docs/csharp/reference/bookings#list); CLI [`nextmail bookings list`](https://nextmail.sh/docs/cli/reference/bookings#bookings-list).

### `bookings->listAll`

Collect every booking of a view into one array

```php
listAll(
    ?string $view = null,
    ?string $pageId = null,
    array|string|null $pageIds = null,
    ?string $q = null,
    DateTimeInterface|string|null $from = null,
    DateTimeInterface|string|null $to = null,
    ?int $limit = null,
    ?string $cursor = null,
    ?string $apiKey = null,
): array
```

Walks every page of `list` and returns all bookings of one view, in the order of that view: `upcoming` and `pending` soonest first, `past` and `ended` latest first. One request per page. Give `pageIds:`, `q:`, `from:` or `to:` to collect only the bookings that match.

Scopes: `calendar:read`.

**Parameters**

- `view` (`string`): Which bookings to collect: `upcoming`, `pending`, `past` or `ended`. Defaults to `upcoming`.
- `pageId` (`string`): Only the bookings of this booking page, by its id. Left out, the bookings of every page of the person are collected.
- `pageIds` (`string|string[]`): Only the bookings of these booking pages, at most 25 ids, as an array or one comma-separated string, sent comma-separated. Left out, or an empty array, every page of the person.
- `q` (`string`): Only the bookings whose guest name, guest email or page name contains this text, at most 200 characters. Case and accents are ignored.
- `from` (`string|DateTimeInterface`): Only the meetings that start at or after this instant, as a `DateTimeInterface` or an ISO 8601 string.
- `to` (`string|DateTimeInterface`): Only the meetings that start before this instant, later than `from:`.
- `limit` (`int`): Page size for each request, from 1 to 100. The server defaults to 30.
- `cursor` (`string`): Starts the walk after this cursor instead of the first page.
- `apiKey` (`string`): Overrides the client API key for every page of this walk.

**Returns**

A list of booking arrays holding every booking of the view.

**Example**

```php
use Nextmail\Constants\BookingViews;

$bookings = $client->bookings->listAll(view: BookingViews::UPCOMING, limit: 100);

foreach ($bookings as $booking) {
    if ($booking['guestMailFailed']) {
        echo $booking['id'], ' ', $booking['guest']['email'], ' ', $booking['start'], PHP_EOL;
    }
}
```

**Notes**

- If any page fails, the exception is thrown and the bookings already fetched are discarded.
- The `past` and `ended` views grow without end, so prefer `iterate` there and stop when you have read far enough back.
- A key or an app limited to particular addresses or domains is refused with 422 `capability_unsupported`, because a booking page reads the whole calendar of its person.

Also available in: API [`GET /bookings`](https://nextmail.sh/docs/api/reference/booking-pages#get-bookings); TypeScript [`nextmail.bookings.listAll()`](https://nextmail.sh/docs/sdk/reference/bookings#listAll); Python [`bookings.list_all()`](https://nextmail.sh/docs/python/reference/bookings#listAll); Ruby [`bookings.list_all`](https://nextmail.sh/docs/ruby/reference/bookings#listAll); Go [`Bookings.ListAll`](https://nextmail.sh/docs/go/reference/bookings#listAll); Java [`bookings().listAll`](https://nextmail.sh/docs/java/reference/bookings#listAll); C# [`Bookings.ListAllAsync`](https://nextmail.sh/docs/csharp/reference/bookings#listAll).

### `bookings->iterate`

Stream the bookings of a view one at a time

```php
iterate(
    ?string $view = null,
    ?string $pageId = null,
    array|string|null $pageIds = null,
    ?string $q = null,
    DateTimeInterface|string|null $from = null,
    DateTimeInterface|string|null $to = null,
    ?int $limit = null,
    ?string $cursor = null,
    ?string $apiKey = null,
): Generator
```

Returns a `Generator` that yields the bookings of one view individually, in the order of that view, and requests the next page only once the current one is drained. Nothing is fetched until you consume it, and breaking out of the loop stops the requests.

Scopes: `calendar:read`.

**Parameters**

- `view` (`string`): Which bookings to walk: `upcoming`, `pending`, `past` or `ended`. Defaults to `upcoming`.
- `pageId` (`string`): Only the bookings of this booking page, by its id. Left out, the bookings of every page of the person are walked.
- `pageIds` (`string|string[]`): Only the bookings of these booking pages, at most 25 ids, as an array or one comma-separated string, sent comma-separated. Left out, or an empty array, every page of the person.
- `q` (`string`): Only the bookings whose guest name, guest email or page name contains this text, at most 200 characters. Case and accents are ignored.
- `from` (`string|DateTimeInterface`): Only the meetings that start at or after this instant, as a `DateTimeInterface` or an ISO 8601 string.
- `to` (`string|DateTimeInterface`): Only the meetings that start before this instant, later than `from:`.
- `limit` (`int`): Page size for each request, from 1 to 100. The server defaults to 30.
- `cursor` (`string`): Starts the walk after this cursor instead of the first page.
- `apiKey` (`string`): Overrides the client API key for every page of this walk.

**Returns**

A `Generator` that yields one booking array per step.

**Example**

```php
use Nextmail\Constants\BookingViews;

foreach ($client->bookings->iterate(view: BookingViews::ENDED) as $booking) {
    echo $booking['start'], ' ', $booking['pageName'], ' was ended by the ', $booking['endedBy'], ': ', $booking['endReason'], PHP_EOL;
}
```

**Notes**

- The generator is lazy, so an abandoned loop costs only the pages you consumed.
- A key or an app limited to particular addresses or domains is refused with 422 `capability_unsupported`, because a booking page reads the whole calendar of its person.

Also available in: API [`GET /bookings`](https://nextmail.sh/docs/api/reference/booking-pages#get-bookings); TypeScript [`nextmail.bookings.iterate()`](https://nextmail.sh/docs/sdk/reference/bookings#iterate); Python [`bookings.iterate()`](https://nextmail.sh/docs/python/reference/bookings#iterate); Ruby [`bookings.iterate`](https://nextmail.sh/docs/ruby/reference/bookings#iterate); Go [`Bookings.Iterate`](https://nextmail.sh/docs/go/reference/bookings#iterate); Java [`bookings().iterate`](https://nextmail.sh/docs/java/reference/bookings#iterate); C# [`Bookings.IterateAsync`](https://nextmail.sh/docs/csharp/reference/bookings#iterate).

### `bookings->get`

Read one booking by id

```php
get(string $id, ?string $apiKey = null): array
```

Looks up a single booking and returns it in the same shape as a row of `list`: the booking page it was made on, the guest with the time zone they booked in, what they answered to the questions of the page, where the meeting happens as the guest was told, the calendar event it is and how it ended if it did.

A booking follows its calendar event. Reading one first catches up with the calendar, so a meeting that was moved there shows its new `start` and `end`, and one that was cancelled or deleted there reads `cancelled`, or `declined` when it still waited for approval. `endedBy` says whether the `host` or the `guest` declined or cancelled it, and `endReason` holds the reason they gave. `guestMailFailed` is true when the last email to the guest could not be sent, and `resendEmail` tries again.

Scopes: `calendar:read`.

**Parameters**

- `id` (`string`, required): Booking id such as `bkg_2b8d4f6a1c3e5079b6d8f0a2`, as `list` returns it.
- `apiKey` (`string`): Overrides the client API key for this call only.

**Returns**

An array with `id`, `pageId`, `pageName`, `status`, `expired`, `past`, `start`, `end`, `durationMinutes`, `guest`, `answers`, `location`, `eventId`, `endedBy`, `endReason`, `rescheduleCount`, `guestMailFailed` and `createdAt`.

**Example**

```php
$booking = $client->bookings->get('bkg_2b8d4f6a1c3e5079b6d8f0a2');

echo $booking['status'], ' ', $booking['start'], ' ', $booking['guest']['name'], ' <', $booking['guest']['email'], '>', PHP_EOL;

foreach ($booking['answers'] as $answer) {
    echo $answer['label'], ': ', $answer['value'], PHP_EOL;
}
```

**Notes**

- A missing booking throws `ApiException` with `isNotFound()` true and code `booking_not_found`. A booking on a page that belongs to somebody else in the workspace is missing too.
- `eventId` is the id `calendar->getEvent` reads. It is null once the booking ended and its event was removed.
- Each answer carries the `id`, `label` and `kind` of its question as it was asked, and its `value` as text. A `checkbox` answers `yes` or `no`.
- An API key acts as the workspace owner and reads the bookings of the owner, and a connected app reads the bookings of the person who connected it.
- A key or an app limited to particular addresses or domains is refused with 422 `capability_unsupported`, because a booking page reads the whole calendar of its person.

Also available in: API [`GET /bookings/{id}`](https://nextmail.sh/docs/api/reference/booking-pages#get-bookings-id); TypeScript [`nextmail.bookings.get()`](https://nextmail.sh/docs/sdk/reference/bookings#get); Python [`bookings.get()`](https://nextmail.sh/docs/python/reference/bookings#get); Ruby [`bookings.get`](https://nextmail.sh/docs/ruby/reference/bookings#get); Go [`Bookings.Get`](https://nextmail.sh/docs/go/reference/bookings#get); Java [`bookings().get`](https://nextmail.sh/docs/java/reference/bookings#get); C# [`Bookings.GetAsync`](https://nextmail.sh/docs/csharp/reference/bookings#get); CLI [`nextmail bookings get`](https://nextmail.sh/docs/cli/reference/bookings#bookings-get).

### `bookings->approve`

Confirm a booking that waits for approval

```php
approve(string $id, ?string $apiKey = null): array
```

Confirms a booking that waits for approval, on a booking page whose `config.approval` is true. The hold on the calendar becomes a confirmed meeting, and the guest is told by email, sent from the address of the page with the calendar invitation attached. It takes no body.

The email goes out as `config.fromEmail`, so the call needs `emails:send` beside `calendar:write`. `list` with `view:` set to `pending` reads the bookings that wait.

Scopes: `calendar:write`, `emails:send`.

**Parameters**

- `id` (`string`, required): Booking id such as `bkg_2b8d4f6a1c3e5079b6d8f0a2`, as `list` returns it.
- `apiKey` (`string`): Overrides the client API key for this call only.

**Returns**

An array, the booking as `get` returns it, with `status` `confirmed`, and `guestMailFailed` saying whether the email to the guest went out.

**Example**

```php
$booking = $client->bookings->approve('bkg_2b8d4f6a1c3e5079b6d8f0a2');

echo $booking['status'], ' for ', $booking['start'], $booking['guestMailFailed'] ? ', but the email did not go out' : '', PHP_EOL;
```

**Notes**

- A booking that is not waiting for approval answers 409 `booking_not_pending`, and one whose time has passed 409 `booking_ended`. A missing one is a 404 `booking_not_found`.
- The booking is confirmed even when the email cannot be sent. `guestMailFailed` is then true, and `resendEmail` sends it again.
- An API key acts as the workspace owner and reaches the bookings of the owner, and a connected app reaches the bookings of the person who connected it.
- A key or an app limited to particular addresses or domains is refused with 422 `capability_unsupported`, because a booking page reads the whole calendar of its person.
- The SDK does not retry it, because a retry could email the guest twice. A 409 `booking_not_pending` on your own second attempt after a lost response means the first one worked or the guest withdrew, and `get` says which.

Also available in: API [`POST /bookings/{id}/approve`](https://nextmail.sh/docs/api/reference/booking-pages#post-bookings-id-approve); TypeScript [`nextmail.bookings.approve()`](https://nextmail.sh/docs/sdk/reference/bookings#approve); Python [`bookings.approve()`](https://nextmail.sh/docs/python/reference/bookings#approve); Ruby [`bookings.approve`](https://nextmail.sh/docs/ruby/reference/bookings#approve); Go [`Bookings.Approve`](https://nextmail.sh/docs/go/reference/bookings#approve); Java [`bookings().approve`](https://nextmail.sh/docs/java/reference/bookings#approve); C# [`Bookings.ApproveAsync`](https://nextmail.sh/docs/csharp/reference/bookings#approve); CLI [`nextmail bookings approve`](https://nextmail.sh/docs/cli/reference/bookings#bookings-approve).

### `bookings->decline`

Turn down a booking that waits for approval

```php
decline(string $id, ?array $body = null, ?string $apiKey = null): array
```

Turns down a booking that waits for approval and frees its time, so the page offers that time again. The guest is told by email, sent from the address of the page, with `reason` in it when one is given. Call it with no body to decline without a reason.

The email goes out as `config.fromEmail`, so the call needs `emails:send` beside `calendar:write`.

Scopes: `calendar:write`, `emails:send`.

**Parameters**

- `id` (`string`, required): Booking id such as `bkg_2b8d4f6a1c3e5079b6d8f0a2`, as `list` returns it.
- `reason` (`string|null`): A sentence for the guest, trimmed, at most 500 characters, included in the email they get. Left out or null, the email gives no reason.
- `apiKey` (`string`): Overrides the client API key for this call only.

**Returns**

An array, the booking as `get` returns it, with `status` `declined`, `endedBy` `host` and `endReason` holding the reason, or null.

**Example**

```php
$booking = $client->bookings->decline('bkg_2b8d4f6a1c3e5079b6d8f0a2', [
    'reason' => 'I am away that week. Please pick a time after the 20th.',
]);

echo $booking['status'], ': ', $booking['endReason'], PHP_EOL;
```

**Notes**

- A booking that is not waiting for approval answers 409 `booking_not_pending`. To call off a confirmed meeting, use `cancel`. A missing booking is a 404 `booking_not_found`.
- A `reason` over 500 characters is 422 `invalid_parameter`, and a key the body does not take is 422 `unknown_parameter`.
- The booking is declined even when the email cannot be sent. `guestMailFailed` is then true, and `resendEmail` sends it again.
- An API key acts as the workspace owner and reaches the bookings of the owner, and a connected app reaches the bookings of the person who connected it.
- A key or an app limited to particular addresses or domains is refused with 422 `capability_unsupported`, because a booking page reads the whole calendar of its person.
- The SDK does not retry it, because a retry could email the guest twice. A 409 `booking_not_pending` on your own second attempt after a lost response means the first one worked or somebody else answered, and `get` says which.

Also available in: API [`POST /bookings/{id}/decline`](https://nextmail.sh/docs/api/reference/booking-pages#post-bookings-id-decline); TypeScript [`nextmail.bookings.decline()`](https://nextmail.sh/docs/sdk/reference/bookings#decline); Python [`bookings.decline()`](https://nextmail.sh/docs/python/reference/bookings#decline); Ruby [`bookings.decline`](https://nextmail.sh/docs/ruby/reference/bookings#decline); Go [`Bookings.Decline`](https://nextmail.sh/docs/go/reference/bookings#decline); Java [`bookings().decline`](https://nextmail.sh/docs/java/reference/bookings#decline); C# [`Bookings.DeclineAsync`](https://nextmail.sh/docs/csharp/reference/bookings#decline); CLI [`nextmail bookings decline`](https://nextmail.sh/docs/cli/reference/bookings#bookings-decline).

### `bookings->cancel`

Cancel a booked meeting

```php
cancel(string $id, ?array $body = null, ?string $apiKey = null): array
```

Cancels a meeting that has not ended yet and cancels its calendar event. The guest is told by email, sent from the address of the page with the cancellation attached, and with `reason` in it when one is given. Call it with no body to cancel without a reason.

A booking that still waits for approval is declined instead, so it ends as `declined`, not `cancelled`. The email goes out as `config.fromEmail`, so the call needs `emails:send` beside `calendar:write`.

Scopes: `calendar:write`, `emails:send`.

**Parameters**

- `id` (`string`, required): Booking id such as `bkg_2b8d4f6a1c3e5079b6d8f0a2`, as `list` returns it.
- `reason` (`string|null`): A sentence for the guest, trimmed, at most 500 characters, included in the email they get. Left out or null, the email gives no reason.
- `apiKey` (`string`): Overrides the client API key for this call only.

**Returns**

An array, the booking as `get` returns it, with `status` `cancelled`, or `declined` when it still waited for approval, `endedBy` `host` and `endReason` holding the reason, or null.

**Example**

```php
$booking = $client->bookings->cancel('bkg_2b8d4f6a1c3e5079b6d8f0a2', [
    'reason' => 'Something urgent came up. Please book another time.',
]);

echo $booking['status'], ' by the ', $booking['endedBy'], ': ', $booking['endReason'], PHP_EOL;
```

**Notes**

- A booking that was already declined or cancelled, or whose meeting is over, answers 409 `booking_ended`. A missing one is a 404 `booking_not_found`.
- There is no undo. The time is free again and the guest has to book once more.
- A `reason` over 500 characters is 422 `invalid_parameter`, and a key the body does not take is 422 `unknown_parameter`.
- The booking is cancelled even when the email cannot be sent. `guestMailFailed` is then true, and `resendEmail` sends it again.
- An API key acts as the workspace owner and reaches the bookings of the owner, and a connected app reaches the bookings of the person who connected it.
- A key or an app limited to particular addresses or domains is refused with 422 `capability_unsupported`, because a booking page reads the whole calendar of its person.
- The SDK does not retry it, because a retry could email the guest twice. A 409 `booking_ended` on your own second attempt after a lost response means the first one worked or the guest cancelled, and `get` says which.

Also available in: API [`POST /bookings/{id}/cancel`](https://nextmail.sh/docs/api/reference/booking-pages#post-bookings-id-cancel); TypeScript [`nextmail.bookings.cancel()`](https://nextmail.sh/docs/sdk/reference/bookings#cancel); Python [`bookings.cancel()`](https://nextmail.sh/docs/python/reference/bookings#cancel); Ruby [`bookings.cancel`](https://nextmail.sh/docs/ruby/reference/bookings#cancel); Go [`Bookings.Cancel`](https://nextmail.sh/docs/go/reference/bookings#cancel); Java [`bookings().cancel`](https://nextmail.sh/docs/java/reference/bookings#cancel); C# [`Bookings.CancelAsync`](https://nextmail.sh/docs/csharp/reference/bookings#cancel); CLI [`nextmail bookings cancel`](https://nextmail.sh/docs/cli/reference/bookings#bookings-cancel).

### `bookings->resendEmail`

Send the guest the email about their booking again

```php
resendEmail(string $id, ?string $apiKey = null): array
```

Sends the guest the email that matches where the booking stands now: the request while it waits for approval, the confirmation with its invitation once it is confirmed, or the notice that it was declined or cancelled, with the reason that was given. Use it when `guestMailFailed` is true. It takes no body and changes nothing else about the booking.

Scopes: `calendar:write`, `emails:send`.

**Parameters**

- `id` (`string`, required): Booking id such as `bkg_2b8d4f6a1c3e5079b6d8f0a2`, as `list` returns it.
- `apiKey` (`string`): Overrides the client API key for this call only.

**Returns**

An array, the booking as `get` returns it, with `guestMailFailed` as it is after the send: false when the email went out, true when it failed again.

**Example**

```php
$booking = $client->bookings->resendEmail('bkg_2b8d4f6a1c3e5079b6d8f0a2');

if ($booking['guestMailFailed']) {
    echo 'Still not sent to ', $booking['guest']['email'], PHP_EOL;
}
```

**Notes**

- A missing booking is a 404 `booking_not_found`.
- It answers 200 whether or not the email went out, so read `guestMailFailed` to know.
- An API key acts as the workspace owner and reaches the bookings of the owner, and a connected app reaches the bookings of the person who connected it.
- A key or an app limited to particular addresses or domains is refused with 422 `capability_unsupported`, because a booking page reads the whole calendar of its person.
- Not retried automatically: every call sends another email, so a retry after a lost response can reach the guest twice.

Also available in: API [`POST /bookings/{id}/resend`](https://nextmail.sh/docs/api/reference/booking-pages#post-bookings-id-resend); TypeScript [`nextmail.bookings.resendEmail()`](https://nextmail.sh/docs/sdk/reference/bookings#resendEmail); Python [`bookings.resend_email()`](https://nextmail.sh/docs/python/reference/bookings#resendEmail); Ruby [`bookings.resend_email`](https://nextmail.sh/docs/ruby/reference/bookings#resendEmail); Go [`Bookings.ResendEmail`](https://nextmail.sh/docs/go/reference/bookings#resendEmail); Java [`bookings().resendEmail`](https://nextmail.sh/docs/java/reference/bookings#resendEmail); C# [`Bookings.ResendEmailAsync`](https://nextmail.sh/docs/csharp/reference/bookings#resendEmail); CLI [`nextmail bookings resend-email`](https://nextmail.sh/docs/cli/reference/bookings#bookings-resend-email).

### `bookings->analytics`

Read how the booking pages did over a window

```php
analytics(
    array|string|null $pageIds = null,
    ?int $days = null,
    ?int $minutes = null,
    ?string $grain = null,
    ?int $offsetMinutes = null,
    ?string $apiKey = null,
): array
```

Returns the numbers behind the Analytics tab of Booking in one request: the bookings guests made over a window, what became of them, the pages and weekdays they went to, and the meetings still to come right now.

A booking counts in the window it was made in, whatever its meeting time. `totals.booked` is every booking made in the window, and `confirmed`, `pending`, `expired`, `declined` and `cancelled` split it by what became of each one, so they add up to `booked`. `cancelledByGuest` and `cancelledByHost` split `cancelled`, `rescheduled` counts the meetings moved at least once, and `minutesBooked` adds up the length of the confirmed meetings.

`upcoming` ignores the window: it counts the confirmed meetings that have not ended and the requests still waiting, and gives the start of the next meeting. `series` is sparse and oldest first, so a chart must fill the gaps. `grain:` sets the bucket width and the key shape, `YYYY-MM-DD`, `YYYY-MM-DDTHH` or `YYYY-MM-DDTHH:MM`, and `offsetMinutes:` shifts the boundaries and the weekdays so days break where the reader's day does. `pageIds:` narrows everything to some pages, such as one page for its own Analytics tab.

Scopes: `calendar:read`.

**Parameters**

- `pageIds` (`string|string[]`): Count only these booking pages, at most 25, such as `['bkp_5c1e9a7b3d2f48e6a0b4c7d1']`, as an array or one comma-separated string, sent comma-separated. Every page of the person when left out or empty.
- `days` (`int`): How far back to look, from 1 to 1095, defaulting to 30.
- `minutes` (`int`): The window in minutes, from 1 to 1576800, which wins over `days` when both are sent.
- `grain` (`string`): Bucket width: `minute`, `hour` or `day`, defaulting to `day`.
- `offsetMinutes` (`int`): Minutes east of UTC to cut the buckets and the weekdays in, from -840 to 840, defaulting to 0. Pass `intdiv((int) date('Z'), 60)` for the zone PHP is set to.
- `apiKey` (`string`): Overrides the client API key for this call only.

**Returns**

An array with `object` set to `booking_analytics`, `pageId`, `pageIds`, `since`, `until`, `grain`, `offsetMinutes`, `totals`, `upcoming`, `series`, `pages` and `weekdays`. `totals` has `booked`, `confirmed`, `pending`, `expired`, `declined`, `cancelled`, `cancelledByGuest`, `cancelledByHost`, `rescheduled`, `minutesBooked` and `cancellationRate`, `upcoming` is an array with `confirmed`, `waiting` and `nextStart`, each entry of `series` is an array with `bucket`, `booked` and `cancelled`, each entry of `pages` is an array with `pageId`, `name`, `booked` and `cancelled`, and `weekdays` holds seven arrays with `weekday` and `meetings`, from Sunday (0) to Saturday (6).

**Example**

```php
$analytics = $client->bookings->analytics(days: 90, offsetMinutes: intdiv((int) date('Z'), 60));
$totals = $analytics['totals'];

echo $totals['booked'], ' booked, cancellation rate ', $totals['cancellationRate'] ?? 'none', PHP_EOL;
echo 'Next meeting: ', $analytics['upcoming']['nextStart'] ?? 'none', PHP_EOL;

foreach ($analytics['pages'] as $page) {
    echo $page['name'], ': ', $page['booked'], ' booked, ', $page['cancelled'], ' cancelled', PHP_EOL;
}
```

**Notes**

- `totals.cancellationRate` is `cancelled` divided by `booked`, at most 1, and null when nothing was booked in the window.
- `pageIds` echoes the pages counted and is empty for every page. `pageId` is that page when exactly one is counted, and null otherwise. A page that is not one of the person is a 404 `booking_page_not_found`.
- `pages` lists at most 10 pages, the ones with the most bookings first. A key limited to particular addresses is refused with `capability_unsupported`, because a booking page reads the whole calendar.
- Read only, so the SDK retries it after a network failure like any other read.

Also available in: API [`GET /bookings/analytics`](https://nextmail.sh/docs/api/reference/booking-pages#get-bookings-analytics); TypeScript [`nextmail.bookings.analytics()`](https://nextmail.sh/docs/sdk/reference/bookings#analytics); Python [`bookings.analytics()`](https://nextmail.sh/docs/python/reference/bookings#analytics); Ruby [`bookings.analytics`](https://nextmail.sh/docs/ruby/reference/bookings#analytics); Go [`Bookings.Analytics`](https://nextmail.sh/docs/go/reference/bookings#analytics); Java [`bookings().analytics`](https://nextmail.sh/docs/java/reference/bookings#analytics); C# [`Bookings.AnalyticsAsync`](https://nextmail.sh/docs/csharp/reference/bookings#analytics); CLI [`nextmail bookings analytics`](https://nextmail.sh/docs/cli/reference/bookings#bookings-analytics).

### `bookings->reschedule`

Move a booked meeting to a new time

```php
reschedule(string $id, array $body, ?string $apiKey = null): array
```

Moves a meeting that has not started to a new time and moves its calendar event with it. The meeting keeps its length, so only `start` is sent. The time has to be one the page offers for this booking: `listTimes` with `bookingId:` lists them, counting the current time of the booking as free. The page may be paused, and the cut-off that limits guests does not apply to the host.

The guest is told by email, sent from the address of the page with the updated invitation attached and with `reason` in it when one is given, and so is everyone the guest added. The host gets a copy at the same address unless `config.notifyHost` is false. A booking that waited for approval is confirmed at the new time, because the host chose it. The email goes out as `config.fromEmail`, so the call needs `emails:send` beside `calendar:write`.

Scopes: `calendar:write`, `emails:send`.

**Parameters**

- `id` (`string`, required): Booking id such as `bkg_2b8d4f6a1c3e5079b6d8f0a2`, as `list` returns it.
- `start` (`string|DateTimeInterface`, required): The new start, as a `DateTimeInterface` or an ISO 8601 string. It has to be one of the times `listTimes` returns for the page with this `bookingId:`.
- `reason` (`string|null`): A sentence for the guest, trimmed, at most 500 characters, included in the email about the new time. Left out or null, the email gives no reason.
- `apiKey` (`string`): Overrides the client API key for this call only.

**Returns**

An array, the booking as `get` returns it, at its new time: `start` and `end` moved, `status` `confirmed` and `rescheduleCount` one higher. `guestMailFailed` is true when the email to the guest could not be sent.

**Example**

```php
$booking = $client->bookings->get('bkg_2b8d4f6a1c3e5079b6d8f0a2');
$open = $client->bookings->listTimes($booking['pageId'], bookingId: $booking['id']);

$moved = $client->bookings->reschedule($booking['id'], [
    'start' => $open['times'][0],
    'reason' => 'Something came up, so I moved us to the next free time.',
]);

echo $moved['start'], ' ', $moved['rescheduleCount'], PHP_EOL;
```

**Notes**

- A time the page does not offer for this booking answers 409 `booking_time_unavailable`: it is taken, outside the hours, inside the notice or past how far ahead the page books. Read `listTimes` again and pick another.
- A booking that was declined or cancelled, or whose meeting has started, answers 409 `booking_ended`. A missing one is a 404 `booking_not_found`.
- Sending the time the meeting already has changes nothing and sends no email.
- The booking moves even when the email cannot be sent. `guestMailFailed` is then true, and `resendEmail` sends it again.
- An API key acts as the workspace owner and reaches the bookings of the owner, and a connected app reaches the bookings of the person who connected it.
- A key or an app limited to particular addresses or domains is refused with 422 `capability_unsupported`, because a booking page reads the whole calendar of its person.
- The SDK does not retry it, because a retry could email the guest twice. Read the booking with `get` after a lost response to see where it landed.

Also available in: API [`POST /bookings/{id}/reschedule`](https://nextmail.sh/docs/api/reference/booking-pages#post-bookings-id-reschedule); TypeScript [`nextmail.bookings.reschedule()`](https://nextmail.sh/docs/sdk/reference/bookings#reschedule); Python [`bookings.reschedule()`](https://nextmail.sh/docs/python/reference/bookings#reschedule); Ruby [`bookings.reschedule`](https://nextmail.sh/docs/ruby/reference/bookings#reschedule); Go [`Bookings.Reschedule`](https://nextmail.sh/docs/go/reference/bookings#reschedule); Java [`bookings().reschedule`](https://nextmail.sh/docs/java/reference/bookings#reschedule); C# [`Bookings.RescheduleAsync`](https://nextmail.sh/docs/csharp/reference/bookings#reschedule); CLI [`nextmail bookings reschedule`](https://nextmail.sh/docs/cli/reference/bookings#bookings-reschedule).
