---
title: Booking
description: Sell an organization's bookable resources on its own website, as requests that an operator approves.
---

The booking API lets an organization sell its rooms and other bookable resources on its own website. A guest picks a time on the website, and your backend sends Hydda One a booking request. The request waits until an operator at the organization approves or rejects it. The API never confirms a booking by itself.

A company pays for each request. You name the company by its Swedish organisation number. Your Application never sees or names a customer contract. Hydda One decides which contract the booking is billed to.

## What the token needs

The caller is the organization's own Application, with a token from the `client_credentials` grant. See [Authentication](/guides/authentication).

An administrator of the organization grants the Application the `Booking Integration` role. The role is granted at each business whose resources the Application may sell. Only an organization's own Application can hold this role. An installed connector cannot. See [Roles](/guides/roles).

Every route under `/v1/booking` answers `403` to any other token, such as a person's token or an installed connector's token.

Call the API from your website's backend. The client secret must never reach a visitor's browser.

## Which resources you can sell

A resource is listed only when all three of these hold:

- It is live.
- An operator marked it as bookable from outside.
- It belongs to a business where your Application holds the role.

Any other resource answers `404`, as if it did not exist.

A resource carries its name, type, description, capacity and time zone. It also carries its booking rules, the opening hours in effect, its default price, VAT and pricing unit. `roomLayouts` lists the layouts a request may name. It is empty unless the room is furnished. `imageUrls` come in display order. `purchaseTermsUrl` links to the resource's purchase terms.

A request from the API always costs the resource's default price. No other price applies.

## The flow

```mermaid
sequenceDiagram
  participant App as Your backend
  participant H as Hydda One
  App->>H: GET /v1/booking/resources
  H-->>App: resources and nextCursor
  App->>H: GET /v1/booking/resources/{resourceId}/availability
  H-->>App: free time
  App->>H: GET /v1/booking/resources/{resourceId}/add-on-products
  H-->>App: add-on products
  App->>H: POST /v1/booking/company-lookups
  H-->>App: exists and invoicingDetailsRequired
  App->>H: POST /v1/booking/booking-requests/preview
  H-->>App: times and price
  App->>H: POST /v1/booking/booking-requests
  H-->>App: the request, pending_approval
  loop Until an operator decides
    App->>H: GET /v1/booking/booking-requests/{bookingId}
    H-->>App: the request and its status
  end
```

Each call is described in the [booking reference](/reference/booking).

1. **List the resources.** Call `GET /v1/booking/resources`. Pass `operationalContextId` to list one business only. Read one resource with `GET /v1/booking/resources/{resourceId}`.
2. **Read the free time.** Call `GET /v1/booking/resources/{resourceId}/availability` with `fromDate` and `toDate`. Both are local dates in the resource's `timeZone`, and both are included. The range can be at most 31 days.
3. **List the add-on products.** Call `GET /v1/booking/resources/{resourceId}/add-on-products`. A guest can order them with the booking, through `addOnLines`. This step is optional.
4. **Look up the paying company.** Call `POST /v1/booking/company-lookups` with `organizationNumber`. Any written form of the number works. The number travels in the body so that it stays out of URLs and access logs.
5. **Preview the request.** Call `POST /v1/booking/booking-requests/preview` with `bookableResourceId`, `timeSpec`, and optionally `roomLayout` and `addOnLines`. The answer has the exact `startsAt` and `endsAt`, the `quote`, and the price of the add-ons. A preview writes nothing.
6. **Send the request.** Call `POST /v1/booking/booking-requests` with the preview's fields plus `bookingId`, `guest` and `company`. The answer is the request with `status` set to `pending_approval`. Hydda One emails the guest that the request was received.
7. **Follow the request.** Call `GET /v1/booking/booking-requests/{bookingId}` to see its status. Hydda One does not call your backend when the status changes.

### Free time by booking model

The resource's `bookingRules.model` decides the shape of the free time. It also decides which `timeSpec` a request sends.

| `model` | Free time | What a request sends |
| --- | --- | --- |
| `time_interval` | `windows`: periods where a unit is free the whole time. Each start sits on the rules' increment. | A `startsAt` and `endsAt` inside one window, within the rules' durations. |
| `time_slots` | `slots`: each free slot, with its `timeSpec`. | That slot's `timeSpec`, unchanged. |
| `date_interval` | `freeNights`: each date whose night nobody holds. | A `fromDate` and `toDate` over a run of free nights, within the rules' minimum and maximum nights. |
| `date_span` | `days`: each date with units left, the count in `remaining`, and its `timeSpec`. | That day's `timeSpec`, unchanged. |

The free time already accounts for the opening hours, the minimum notice, the buffer between bookings and the two-year booking horizon. It counts every booking that holds time, including requests still waiting for approval.

Treat the free time as a guide. A request checks every rule again when Hydda One writes it. On a resource with several units, a long request or a stay of several nights can still be refused.

### The paying company

The company lookup answers two fields:

- `exists` is `true` when the organization already holds the company as a customer.
- `invoicingDetailsRequired` is `true` when Hydda One cannot invoice the company yet. The request must then carry `company.name` and `company.billingAccount`.

Send `company.name` and `company.billingAccount` together, or leave both out. When the lookup says they are not required, Hydda One does not use them.

The lookup never returns the company's name, id or contracts.

## Request statuses

| `status` | Meaning |
| --- | --- |
| `pending_approval` | The request waits for an operator. It holds its time, so nobody else can book it. |
| `confirmed` | An operator approved the request. Hydda One emails the guest a confirmation. |
| `rejected` | An operator refused the request. Its time is free again. |
| `expired` | Nobody decided in time. Its time is free again. |
| `cancelled` | The booking was cancelled. Its time is free again. |

A request expires after `bookingRules.approvalExpiryMinutes`, counted from when you sent it. When the field is absent, the request waits until an operator decides.

## Rules for callers

### Send each request once, with your own id

You mint `bookingId`, a UUID, before you send the request. Mint one id per guest request and keep it.

If a call times out, send the same request again with the same `bookingId`. Hydda One answers with the first request and writes nothing new. It does not check the body again, so a changed body with the same id is ignored.

If another booking already holds your `bookingId`, the answer is `409` with `BOOKING_REQUEST_ID_ALREADY_USED`. Mint a new id and send the request again.

The resource reads, the company lookup and the preview write nothing. You can repeat them freely.

### Read the status yourself

Hydda One never calls your backend. Read the request with `GET /v1/booking/booking-requests/{bookingId}` to learn its status. You can read only requests your own Application made. Any other id answers `404`.

### Limits

| What | Limit |
| --- | --- |
| `limit` on the resource list | 1 to 100. Defaults to 50. |
| Availability range | At most 31 days. |
| `guest.name`, `company.name` | 1 to 200 characters. |
| Requests under `/v1/booking` | 600 a minute from each IP address. |

Page through the resource list with `nextCursor`. Pass it as `cursor`, with the same `operationalContextId` as the first page. A cursor sent with a different filter answers `400`.

All your website's visitors reach Hydda One through your one backend. They therefore share the rate limit. A request over the limit gets `429`.

## Errors to handle

| Code | Status | When |
| --- | --- | --- |
| `VALIDATION_FAILED` | 400 | A body, query or path parameter does not match its schema. |
| `BOOKING_EXTERNAL_INVOICING_DETAILS_REQUIRED` | 400 | The company needs `company.name` and `company.billingAccount`, and the request has neither. |
| `BOOKING_EXTERNAL_BILLING_ACCOUNT_INCOMPLETE` | 400 | `company.billingAccount` lacks something an invoice needs, such as a contact name. `detail` names what is missing. |
| `BOOKING_REQUEST_ID_ALREADY_USED` | 409 | Another booking holds your `bookingId`. |
| `BOOKING_SLOT_TAKEN` | 409 | The time is no longer free. |
| `BOOKING_BUFFER_CONFLICT` | 409 | The time is too close to another booking. |
| `BOOKING_NOTICE_TOO_SHORT` | 400 | The booking starts too soon. |
| `BOOKING_OUTSIDE_OPEN_HOURS` | 400 | The time falls outside the opening hours. |
| `BOOKING_TIME_SPEC_MODEL_MISMATCH` | 400 | The `timeSpec` does not match the resource's booking model. |
| `BOOKING_ROOM_LAYOUT_NOT_OFFERED` | 400 | The `roomLayout` is not one of the resource's `roomLayouts`. |

The other booking rules have codes of their own, such as `BOOKING_DURATION_TOO_SHORT` or `BOOKING_STAY_TOO_LONG`. A request that breaks a rule gets the same code an operator's booking would get. Show the guest that the time cannot be booked, and read the free time again.

A `404` without a `code` means the resource or request does not exist, or your Application cannot see it.

[Errors](/guides/errors) lists every booking code and its status.
