# AllBooked API (Beta)

Base URL: `https://api.allbooked.com`

> **Beta.** This API is still evolving. Endpoints, parameters, and payloads may change without a version bump.
> Breaking changes and major updates are announced in the changelog at the end of this page.

## Getting started

The Skedda API gives you read-only access to a venue's assets and booking data through simple GET endpoints.
You cannot create, update or cancel bookings through this interface.

Authentication is done per venue. Your API key tells us what venue’s data you are pulling, so we don’t need a
venue identifier in the request url.

The API is built on two primary collections:

- **Assets**: The venue’s resources including bookable spaces, individual space assignments and optional add-ons.
- **Booking Occurrences**: Individual calendar entries for asset reservations. For recurring bookings, each
  individual date generates its own distinct occurrence record.

## Authentication

Send a venue API key as a bearer token:

```
Authorization: Bearer <your API key>
```

Venue administrators can generate API keys directly from the **Integrations** page within their venue.
As each key is tied to the specific venue that created it, the key itself determines which venue's data your
request accesses.

Treat these keys with the same security as passwords. Since API keys cannot be recovered once displayed, you
must store them safely upon creation; if lost, a key must be revoked and replaced.

A request with no key, an unknown key, or an expired or revoked one is answered with `401`. A key belonging
to a venue whose plan does not include API access is answered with `403`.

## Making requests

Every endpoint lives under `https://api.allbooked.com/api/v1` and answers
`application/json`.

```
curl "https://api.allbooked.com/api/v1/assets?filter[category]=bookable" \
  -H "Authorization: Bearer <your API key>"
```

Occurrences are read within a date range, and both bounds carry an offset — `Z` for UTC:

```
curl "https://api.allbooked.com/api/v1/booking-occurrences?filter[dateTimeFrom]=2026-09-01T00:00:00Z&filter[dateTimeTo]=2026-09-30T23:59:00Z" \
  -H "Authorization: Bearer <your API key>"
```

Successful responses are wrapped in a JSON envelope containing three main fields:

```json
{
  "metadata": { "timezone": "Australia/Melbourne" },
  "data": [ ... ],
  "paging": { "cursor": "us_ast_42", "pageSize": 100, "isLast": false }
}
```

- **`data`**: The array of requested records.
- **`paging`**: Instructions on how to fetch the next page of results.
- **`metadata.timezone`**: The venue's IANA time zone (e.g., "Australia/Melbourne"). All date-time values
  in the response are ISO-8601 strings pre-formatted with the venue's local offset for that specific
  instant, ensuring accuracy across daylight saving transitions.

### Strict Parameter Validation

Query parameters are strictly validated against accepted values. Any request with an unrecognized
parameter or a typo in a filter returns a `400 Bad Request` response instead of being silently ignored,
ensuring invalid filters are caught immediately.

## Pagination

Pagination uses a forward-only cursor system. You can set the number of records returned using the
`page[pageSize]` query parameter, which accepts values between 1 and
100 (defaulting to 100).

### Paging Response Structure

- **`paging.cursor`**: The identifier corresponding to the last record on the current page.
- **`paging.isLast`**: A boolean indicating whether you have reached the end of the results.

### Navigating Pages

To retrieve the next set of results, pass the previous `paging.cursor` value back in the `page[after]` query
parameter.

Maintain identical query parameters (such as filters or sorting options) across all paginated requests in a
single run. Changing parameters mid-stream alters the dataset layout, causing records to be skipped or
repeated.

For convenience, occurrence endpoints include a standard `Link` header containing a pre-formatted URL for the
next page that automatically preserves all existing parameters.

### Cursor Expiration & Errors

If the underlying dataset changes between page requests and the cursor becomes invalid, the API returns a
`400 Bad Request` status, requiring the pagination sequence to restart from the first page.

## Filtering and sorting

Filtering and sorting behavior follows specific structure and boundary rules:

Filters use bracketed query parameters: `filter[category]` for assets, and `filter[dateTimeFrom]`,
`filter[dateTimeTo]`, `filter[spaceIds]`, and `filter[holders]` for occurrences. ID-based filters can be
repeated up to 50 times in a single request to query multiple
values (e.g., `filter[spaceIds]=id1&filter[spaceIds]=id2`).

### Date Range Requirements for Occurrences

Reading occurrences requires specifying both date-range boundaries:

- **Strict Offsets**: Both `filter[dateTimeFrom]` and `filter[dateTimeTo]` are required ISO-8601 strings and
  must include an explicit UTC offset (e.g., `+02:00` or `Z`). Values missing an offset are rejected to
  prevent unintended time shifts.
- **Range Limit**: The total window between the start and end dates cannot exceed
  31 days.

### Sorting

Control result ordering using `sort[field]` and `sort[direction]`. If omitted, requests revert to the default
field and direction defined for each endpoint in the API reference.

## Expanding related records

By default, when a record references another entity, it contains only that entity's id:

```json
"holder": { "id": "us_vur_1" }
```

To receive full objects instead of plain IDs, specify the relationship path in the `expand` query parameter.
This inlines the target record directly into the response.

- **Multiple & Nested Paths**: Separate paths with commas, and use dot-notation for nested relationships
  (e.g., `expand=holder,addOns.asset`).
- **Expansion Limit**: A single request path can name a maximum of
  3 expanded fields.

### Performance & Validation

- **Optimization**: Expanding records adds database overhead. Request only the specific relationships your
  application requires and leave unused references as IDs.
- **Validation**: If you attempt to expand a field that does not support expansion, the API returns a
  `400 Bad Request` error containing a list of all valid expandable fields.

## Rate limiting

Rate limits are tracked per API key using a fixed one-minute window. Each key is allowed 100 requests per
minute; the `X-RateLimit-Limit` header reports the limit that actually applies to
your venue. Every metered response includes headers reflecting your current usage and status within that
window:

| Header | Meaning |
| --- | --- |
| `X-RateLimit-Limit` | The maximum number of requests allowed per minute. |
| `X-RateLimit-Remaining` | The number of requests remaining in the current window. |
| `X-RateLimit-Reset` | The Unix time, in seconds, at which the current window resets. |

### Handling Rate Limits

Exceeding the threshold returns a `429 Too Many Requests` status, and the response body or `Retry-After`
header specifies how many seconds you must wait before retrying.

- **Pacing**: Monitor the `X-RateLimit-Remaining` header to throttle outgoing
  requests proactively and avoid hitting the cap.
- **Retries**: Never retry a rejected request immediately. Retrying before the cooldown period ends will
  continue to fail with additional `429` responses.

## Errors and responses

All error responses share a consistent envelope structure, allowing a single error handler to process every
failure across the API:

```json
{
  "metadata": { "timezone": "UTC" },
  "errors": [
    {
      "type": "invalid_param",
      "title": "filter[dateTimeFrom] must be before filter[dateTimeTo].",
      "status": 400,
      "requestId": "0HN7A9BJKMQ1V:00000003"
    }
  ]
}
```

### Handling Errors

- **Branch on type**: Base your programmatic error-handling logic on the `type` field (a stable identifier)
  rather than `title`, which is human-readable text subject to wording changes.
- **Support Inquiries**: Always include the `requestId` when contacting support about a failed API call, as
  this value uniquely identifies the transaction within the internal logs.

| `type` | Status | What happened |
| --- | --- | --- |
| `invalid_param` | 400 | A parameter was missing, malformed, out of range, or not one this endpoint accepts |
| `invalid_expand` | 400 | An `expand` path named a field that cannot be expanded there, or nested too deeply |
| `auth_missing_token` | 401 | No `Authorization: Bearer` header, or one that could not be read |
| `auth_invalid_token` | 401 | The key is not a key of ours |
| `auth_expired_token` | 401 | The key has passed its expiry date |
| `auth_revoked_token` | 401 | The key was revoked by a venue admin |
| `forbidden_feature_not_enabled` | 403 | The venue's plan does not include API access |
| `rate_limit_exceeded` | 429 | Too many requests in the current window |

## Changelog

Changes to this API are recorded here, newest first.

### Version 1.0 Release Notes

This initial release introduces core read access to venue resources and booking data:

- **Venue Data Access**: Retrieve venue assets and individual booking occurrences.
- **Cursor Pagination**: Sequential, forward-only navigation across large result sets.
- **Record Expansion**: Inline related entity data directly into primary response payloads.
- **Per-Key Rate Limiting**: Request quotas managed and tracked per API key over fixed one-minute windows.

## Security scheme: ApiKey

HTTP, scheme `bearer`.

A venue API key, sent as `Authorization: Bearer <key>`. Keys are created by a venue admin on the Integrations page and are scoped to the venue that issued them, which is how a request selects its venue.

## Endpoints

### Assets

The spaces and add-ons at a venue: bookable spaces and add-ons that can be booked, and assigned spaces that are permanently allocated to one user. An asset's category determines which of its optional properties are populated.

#### GET /api/v1/assets

**List assets**

Returns the assets available at a venue, including bookable spaces, assigned spaces, and add-ons. Assets are ordered by ID for consistency; the order itself has no additional meaning.

Parameters:

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `filter[category]` | Query | No | `bookable` \| `assigned` \| `addOn` | Returns only the assets of one category. An asset belongs to exactly one. |
| `sort[field]` | Query | No | `assetId` \| `name` \| `category` | Orders the assets by this field. Defaults to `assetId`. |
| `sort[direction]` | Query | No | `asc` \| `desc` | Direction of the ordering. Defaults to `asc`. |
| `page[after]` | Query | No | string | Returns the assets that follow this cursor. Pass back the `paging.cursor` of the previous response, and keep every other parameter of the request unchanged — a filter or sort changed mid-run re-orders the list the cursor is resolved against. |
| `page[pageSize]` | Query | No | string | How many assets to return, from 1 to 100. Defaults to 100. |
| `expand` | Query | No | string | Comma-separated paths naming which referenced records to return in full rather than as an id — for example `assignedTo`. Nest with dots, up to 3 expanded fields in one path. |

`filter[category]` values:

| Value | Description |
| --- | --- |
| `bookable` | A space venue users book for a period of time. |
| `assigned` | A space held permanently by one venue user, who is named in `assignedTo`. |
| `addOn` | An extra booked alongside a space, such as equipment or catering; how much of it exists is in `addOnConfig`. |

Responses:

| Status | Body | Description |
| --- | --- | --- |
| 200 | [AssetListResponse](#assetlistresponse) | OK |
| 400 | [ErrorResponse](#errorresponse) | Bad Request |
| 401 | [ErrorResponse](#errorresponse) | Unauthorized |
| 403 | [ErrorResponse](#errorresponse) | Forbidden |
| 429 | [ErrorResponse](#errorresponse) | Too Many Requests |

`200` headers:

| Header | Type | Description |
| --- | --- | --- |
| `X-RateLimit-Limit` | integer | How many requests this API key may make in a one-minute window. |
| `X-RateLimit-Remaining` | integer | How many requests are left in the current window. |
| `X-RateLimit-Reset` | integer | When the current window resets, as a Unix time in seconds. |

`400` headers:

| Header | Type | Description |
| --- | --- | --- |
| `X-RateLimit-Limit` | integer | How many requests this API key may make in a one-minute window. |
| `X-RateLimit-Remaining` | integer | How many requests are left in the current window. |
| `X-RateLimit-Reset` | integer | When the current window resets, as a Unix time in seconds. |

`429` headers:

| Header | Type | Description |
| --- | --- | --- |
| `X-RateLimit-Limit` | integer | How many requests this API key may make in a one-minute window. |
| `X-RateLimit-Remaining` | integer | How many requests are left in the current window. |
| `X-RateLimit-Reset` | integer | When the current window resets, as a Unix time in seconds. |
| `Retry-After` | integer | How many seconds to wait before retrying. The same number is in the error's title. |

```bash
curl -X GET "https://api.allbooked.com/api/v1/assets" \
  -H "Authorization: Bearer $SKEDDA_API_KEY"
```

### Booking occurrences

Individual calendar entries for asset reservations. Each date of a recurring booking is its own occurrence, so a series is read one occurrence at a time.

#### GET /api/v1/booking-occurrences

**List booking occurrences**

Returns booking occurrences within a specified date range of up to 31 days. Each occurrence of a recurring booking is returned separately.

Parameters:

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `filter[dateTimeFrom]` | Query | Yes | string (date-time) | Start of the range to read, as an ISO-8601 date-time carrying a UTC offset — for example `2026-05-01T09:00:00+02:00`. A value without an offset is rejected rather than assumed to be in the venue's zone. |
| `filter[dateTimeTo]` | Query | Yes | string (date-time) | End of the range to read, in the same form as the start of it. The window between the two cannot exceed 31 days. |
| `filter[spaceIds]` | Query | No | array of string | Returns only the occurrences held in these spaces. Repeat the parameter once per space global id, up to 50 of them. |
| `filter[holders]` | Query | No | array of string | Returns only the occurrences whose booking is held by these venue users. Repeat the parameter once per venue-user global id, up to 50 of them. |
| `sort[field]` | Query | No | `start` \| `holder` \| `lastModifiedTime` | Orders the occurrences by this field. Defaults to `start`. |
| `sort[direction]` | Query | No | `asc` \| `desc` | Direction of the ordering. Defaults to `asc`. |
| `page[after]` | Query | No | string | Returns the occurrences that follow this cursor. Pass back the `paging.cursor` of the previous response, and keep every other parameter of the request unchanged — a filter or sort changed mid-run re-orders the list the cursor is resolved against. |
| `page[pageSize]` | Query | No | string | How many occurrences to return, from 1 to 100. Defaults to 100. |
| `expand` | Query | No | string | Comma-separated paths naming which referenced records to return in full rather than as an id — for example `holder.tags`. Nest with dots, up to 3 expanded fields in one path. |

Responses:

| Status | Body | Description |
| --- | --- | --- |
| 200 | [BookingOccurrenceListResponse](#bookingoccurrencelistresponse) | OK |
| 400 | [ErrorResponse](#errorresponse) | Bad Request |
| 401 | [ErrorResponse](#errorresponse) | Unauthorized |
| 403 | [ErrorResponse](#errorresponse) | Forbidden |
| 429 | [ErrorResponse](#errorresponse) | Too Many Requests |

`200` headers:

| Header | Type | Description |
| --- | --- | --- |
| `X-RateLimit-Limit` | integer | How many requests this API key may make in a one-minute window. |
| `X-RateLimit-Remaining` | integer | How many requests are left in the current window. |
| `X-RateLimit-Reset` | integer | When the current window resets, as a Unix time in seconds. |
| `Link` | string | A ready-made URL for the next page, in the RFC 8288 form `<url>; rel="next"`. It repeats the request's own filters and sort, so following it cannot change the run the cursor is resolved against. Absent on the last page. |

`400` headers:

| Header | Type | Description |
| --- | --- | --- |
| `X-RateLimit-Limit` | integer | How many requests this API key may make in a one-minute window. |
| `X-RateLimit-Remaining` | integer | How many requests are left in the current window. |
| `X-RateLimit-Reset` | integer | When the current window resets, as a Unix time in seconds. |

`429` headers:

| Header | Type | Description |
| --- | --- | --- |
| `X-RateLimit-Limit` | integer | How many requests this API key may make in a one-minute window. |
| `X-RateLimit-Remaining` | integer | How many requests are left in the current window. |
| `X-RateLimit-Reset` | integer | When the current window resets, as a Unix time in seconds. |
| `Retry-After` | integer | How many seconds to wait before retrying. The same number is in the error's title. |

```bash
curl -X GET "https://api.allbooked.com/api/v1/booking-occurrences" \
  -H "Authorization: Bearer $SKEDDA_API_KEY"
```

### API reference

This reference in other formats: the OpenAPI document for generating clients or importing into tools, and plain Markdown or HTML for reading without JavaScript. No API key is needed.

#### GET /api/v1/openapi/v1.json

**OpenAPI document (JSON)**

Responses:

| Status | Body | Description |
| --- | --- | --- |
| 200 | &mdash; | OK |

```bash
curl -X GET "https://api.allbooked.com/api/v1/openapi/v1.json"
```

#### GET /api/v1/openapi/v1.yaml

**OpenAPI document (YAML)**

Responses:

| Status | Body | Description |
| --- | --- | --- |
| 200 | &mdash; | OK |

```bash
curl -X GET "https://api.allbooked.com/api/v1/openapi/v1.yaml"
```

#### GET /api/v1/docs.md

**API reference (Markdown)**

Responses:

| Status | Body | Description |
| --- | --- | --- |
| 200 | &mdash; | OK |

```bash
curl -X GET "https://api.allbooked.com/api/v1/docs.md"
```

#### GET /api/v1/docs/html

**API reference (HTML)**

Responses:

| Status | Body | Description |
| --- | --- | --- |
| 200 | &mdash; | OK |

```bash
curl -X GET "https://api.allbooked.com/api/v1/docs/html"
```

## Models

### Asset

An asset is exactly one of these types: a bookable space, an assigned space, or an add-on. Category determines the type,
and therefore which optional properties are populated.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | Yes | Global id of the asset. |
| `name` | string | Yes |  |
| `category` | [AssetCategory](#assetcategory) | Yes | What kind of asset this is, which determines how a venue books it. |
| `description` | string, or null | Yes |  |
| `images` | array of string | Yes | Absolute URLs of the asset's images, ready to fetch as-is. Empty when the asset has no images. |
| `assignedTo` | `{ id }` or [VenueUser](#venueuser), or null | Yes | Who the asset is permanently assigned to, for assigned spaces: the venue user's id, or the full venue user when `expand=assignedTo` is requested. Null for every other category. |
| `addOnConfig` | [AssetAddOnConfig](#assetaddonconfig), or null | Yes |  |
| `attributes` | [AssetAttributes](#assetattributes) | Yes | The venue's own classification of an asset: its type, its tags, and how many people it seats. |

### AssetAddOnConfig

How much of an add-on exists and how much of it one booking may take.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `quantity` | integer (int32) | Yes | Total units the venue holds. |
| `maxQuantityPerBooking` | integer (int32), or null | Yes | Per-booking ceiling, or null when a booking may take the whole quantity. |
| `isArchived` | boolean | Yes |  |

### AssetAttribute

A venue-defined type or tag, as its id plus the venue's label for it.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | Yes | Identifier of the type or tag. Unlike the other ids here it is not a global id: types and tags are defined per venue, so the value is the venue-local one assigned when the type or tag was created. |
| `name` | string | Yes | The venue's label, which they may rename at any time. |

### AssetAttributes

The venue's own classification of an asset: its type, its tags, and how many people it seats.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `type` | [AssetAttribute](#assetattribute), or null | Yes |  |
| `tags` | array of [AssetAttribute](#assetattribute) | Yes |  |
| `capacity` | integer (int32), or null | Yes | People the asset seats, or null when the venue has not set one. |

### AssetCategory

What kind of asset this is, which determines how a venue books it.

| Value | Description |
| --- | --- |
| `bookable` | A space venue users book for a period of time. |
| `assigned` | A space held permanently by one venue user, who is named in `assignedTo`. |
| `addOn` | An extra booked alongside a space, such as equipment or catering; how much of it exists is in `addOnConfig`. |

### AssetListResponse

The envelope every successful list response is wrapped in. The items live under `data`; everything a client
needs to interpret or continue the request sits beside them rather than inside each item.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `metadata` | [ResponseMetadata](#responsemetadata) | Yes | Request-level context that applies to the whole response rather than to any one item. |
| `data` | array of [Asset](#asset) | Yes |  |
| `paging` | [Paging](#paging) | Yes | Where the returned page sits in the full result set, and how to ask for the next one. |

### BookingOccurrence

One occurrence of one booking — the unit this endpoint returns. Recurring bookings contribute one item per
occurrence, so the booking-level properties repeat across the items of a series.

Every date-time is an ISO-8601 string with the venue's UTC offset for that occurrence, so the offset can differ
across a daylight-saving change. The venue's time zone is given in `metadata.timezone`.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `bookingId` | string | Yes |  |
| `venueId` | string | Yes |  |
| `holder` | `{ id }` or [VenueUser](#venueuser), or null | Yes | A reference to a VenueUser: only its id unless `expand` names this position, in which case the full VenueUser is inlined. |
| `spaces` | array of (`{ id }` or [Asset](#asset)) | Yes | The spaces the booking occupies, ordered by the underlying space id. The order carries no meaning — it exists so the same request always renders the array the same way. |
| `title` | string, or null | Yes |  |
| `type` | [BookingType](#bookingtype) | Yes |  |
| `createdBy` | `{ id }` or [VenueUser](#venueuser), or null | Yes | A reference to a VenueUser: only its id unless `expand` names this position, in which case the full VenueUser is inlined. |
| `lastModifiedBy` | `{ id }` or [VenueUser](#venueuser), or null | Yes | A reference to a VenueUser: only its id unless `expand` names this position, in which case the full VenueUser is inlined. |
| `createdTime` | string, or null | Yes |  |
| `lastModifiedTime` | string, or null | Yes |  |
| `customFields` | array of [BookingOccurrenceCustomField](#bookingoccurrencecustomfield) | Yes |  |
| `addOns` | array of [BookingOccurrenceAddOn](#bookingoccurrenceaddon) | Yes |  |
| `conference` | [BookingOccurrenceConference](#bookingoccurrenceconference), or null | Yes |  |
| `start` | string | Yes |  |
| `end` | string | Yes |  |
| `checkedIn` | boolean | Yes |  |
| `paymentStatus` | [BookingPaymentStatus](#bookingpaymentstatus) | No | Omitted entirely when the venue has no payment gateway set up. |
| `price` | number (double) | No | If the venue doesn't use pricing rules, this field is left out entirely. When it's missing, it just means the venue doesn't manage pricing. If a booking is free or fully covered by a coupon, this will show 0. |
| `chargeTransactionId` | string | No | Omitted when the venue has no payment gateway set up, and also when nothing has been charged yet. Presence of PaymentStatus is what distinguishes the two. |

### BookingOccurrenceAddOn

An add-on booked with this booking, as a reference to the add-on space plus the quantity taken.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `asset` | `{ id }` or [Asset](#asset) | Yes | A reference to a Asset: only its id unless `expand` names this position, in which case the full Asset is inlined. |
| `quantity` | integer (int32) | Yes |  |

### BookingOccurrenceConference

The conference attached to this booking. Deliberately narrow: join URL and provider only. The meeting id,
password, access code, host email and per-occurrence ids are not exposed.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `joinUrl` | string, or null | Yes |  |
| `type` | [ConferenceType](#conferencetype) | Yes |  |

### BookingOccurrenceCustomField

A filled-in custom field. Name is the venue's label for the field; the value is emitted as
stored, so its JSON type follows the field's type: a string, a number, a boolean, or an array of strings for a
multi-select field.

Id is the durable key: it is assigned once when the field is created and is immutable
thereafter, whereas the label can be renamed at any time and is not unique across a venue's fields. Unlike every
other id in this payload it is not a global id — the global-id stack is numeric-only and a field id is unique
within its venue rather than globally, so it is emitted as stored (e.g. `cfPurposeOfVisit`).

Fields the venue marked admin-only are deliberately included. The key authenticating this endpoint is created by
an admin and scoped to the venue, so it carries admin visibility — the same reason the iCal feed renders
admin-only values for a venue-level token but omits them for a per-user one.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | Yes |  |
| `name` | string | Yes |  |
| `value` | boolean or number or string or array of string, or null | Yes |  |

### BookingOccurrenceListResponse

The envelope every successful list response is wrapped in. The items live under `data`; everything a client
needs to interpret or continue the request sits beside them rather than inside each item.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `metadata` | [ResponseMetadata](#responsemetadata) | Yes | Request-level context that applies to the whole response rather than to any one item. |
| `data` | array of [BookingOccurrence](#bookingoccurrence) | Yes |  |
| `paging` | [Paging](#paging) | Yes | Where the returned page sits in the full result set, and how to ask for the next one. |

### BookingPaymentStatus

| Value | Description |
| --- | --- |
| `notApplicable` | No payment is tracked, because the booking's price is 0 — free, or fully covered by a discount code. The app shows this as "N/A". |
| `default` | The booking has a price, but no payment status is being tracked for it — the app shows this as "No status". It is where every priced event booking starts, and where a venue leaves a booking it would rather an admin mark paid or unpaid later. Nothing resolves it on its own: it stays until an admin changes it, or a payment or an invoice moves it on. |
| `unpaid` | The booking has a price and no payment has been received. A booking also falls back to this after a full refund, or after its invoice is voided. |
| `paid` | Payment was received in full — by card charge, by a paid invoice, or marked by an admin. A paid booking can no longer have its price or discount changed, and cannot be ended early. |
| `invoiced` | An invoice has been issued and is not yet paid — the app shows this as "Unpaid - Invoiced". It becomes paid once the invoice is paid, and unpaid if the invoice is voided. |

### BookingType

| Value | Description |
| --- | --- |
| `internal` |  |
| `user` |  |
| `unavailable` |  |
| `event` |  |

### ConferenceType

| Value | Description |
| --- | --- |
| `unknown` |  |
| `zoom` |  |
| `webex` |  |

### Error

One broken rule. A response reports every error it found, so expect several of these.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `type` | string | Yes | Stable machine-readable identifier for the kind of failure — the value to branch on in client code, since Title is prose and may be reworded. |
| `title` | string | Yes | Human-readable explanation, intended for a developer reading a log rather than an end user. |
| `status` | integer (int32) | Yes | The HTTP status this error maps to. |
| `requestId` | string | Yes | Identifier for this request, worth quoting in a support conversation: it is what ties the response back to our logs. |

### ErrorResponse

The envelope every failed request is answered with.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `metadata` | [ResponseMetadata](#responsemetadata) | Yes | Request-level context that applies to the whole response rather than to any one item. |
| `errors` | array of [Error](#error) | Yes |  |

### Paging

Where the returned page sits in the full result set, and how to ask for the next one.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `cursor` | string, or null | Yes | Cursor of the last item, to be echoed back as `page[after]`. |
| `pageSize` | integer (int32) | Yes | How many items this page actually contains, which may be fewer than requested. |
| `isLast` | boolean | Yes | Whether this is the final page of the result set. |

### ResponseMetadata

Request-level context that applies to the whole response rather than to any one item.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `timezone` | string | Yes | The IANA zone the response's date-times belong to. Those are pre-formatted strings carrying an offset, and the offset alone cannot be turned back into a zone, so it is named here. |

### VenueUser

A venue's member together with the person's identity. One flat object because neither half is useful alone:
the venue-user row says nothing about who the person is, and the profile carries no venue context.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | Yes |  |
| `venueId` | string | Yes |  |
| `email` | string, or null | Yes |  |
| `firstName` | string, or null | Yes |  |
| `lastName` | string, or null | Yes |  |
| `organisation` | string, or null | Yes |  |
| `phoneNumber` | string, or null | Yes |  |
| `notes` | string, or null | Yes |  |
| `paymentGatewayCustomerId` | string, or null | Yes |  |
| `createdTime` | string, or null | Yes |  |
| `lastModifiedTime` | string, or null | Yes |  |
| `createdBy` | `{ id }`, or null | Yes | A reference to a VenueUser: only its id unless `expand` names this position, in which case the full VenueUser — the same shape as this model — is inlined. |
| `lastModifiedBy` | `{ id }`, or null | Yes | A reference to a VenueUser: only its id unless `expand` names this position, in which case the full VenueUser — the same shape as this model — is inlined. |
| `tags` | array of (`{ id }` or [VenueUserTag](#venueusertag)) | Yes |  |

### VenueUserTag

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | Yes |  |
| `name` | string | Yes |  |
| `type` | `custom` \| `bookingAdmin` \| `owner` \| `systemAdmin` \| `customAdmin` | Yes |  |

