# Making a booking

Three calls make a booking: check availability, get a quote, then book.

## 1. Availability

```
GET /rentals/{slug}/availability?from=2027-06-01&to=2027-06-04
```

One row per car type with how many cars are free over the span, and per day. The count already includes the rental's overbooking allowance, and drops to the physical fleet inside the rental's lead-time window. The span is 1 to 120 days.

## 2. Quote

```
POST /rentals/{slug}/quotes
```

Send the car type, the pickup and return, the locations, the extras and a coupon code if any. The answer is every line the customer will pay, with tax per line, and the total. A quote is a value: it is not stored, and nothing is held by it.

A quote can carry problems, in `problems`:

| Problem                 | What it means                                                             | Stops a booking |
| ----------------------- | ------------------------------------------------------------------------- | --------------- |
| `no_rate`               | Nothing prices that car type on those days.                               | Yes             |
| `too_short`             | Shorter than the rental's minimum stay (`minStayDays` on `GET /rentals`). | Yes             |
| `too_long`              | Kept for later. No quote gives it today.                                  | Yes             |
| `bad_dates`             | A date does not read, or the return is not after the pickup.              | Yes             |
| `coupon_unknown`        | No such coupon code.                                                      | No              |
| `coupon_expired`        | The pickup is outside the code's dates.                                   | No              |
| `coupon_not_open`       | The code cannot be redeemed on the day of booking.                        | No              |
| `coupon_not_applicable` | The code does not cover this car type, length or extras.                  | No              |

A coupon problem leaves the booking possible without the discount. Show it to the customer. When the coupon applied, the quote's `coupon` names it: `code`, and `percentBp` (1000 is 10%) or `amountMinor`, the other null. It is null otherwise. Each of `terms` has `discountMinor`, what the coupon took off that term's total, so `totalMinor` plus `discountMinor` is the price before the coupon.

The minimum stay is one number for the whole rental. A car type also shows `minRentalHours` and `maxRentalDays`, but no quote and no booking checks them today. A booking longer than `maxRentalDays` is priced and taken.

### The driver's age

Send `driverAge` when you know it: whole years, from 16 to 120. A rental's young driver fee goes on only with it. Without it, there is no fee. An age outside that range answers `422` with `invalid`.

A car type can have a minimum driver age, its `minDriverAge`. A driver under it answers `422` with the code `driver_too_young`, on a quote and on a booking alike. Nothing is booked. Every key gets this answer, the organization's own key too.

### Insurance, excess and deposit

A quote says what the customer pays at most if something happens to the car. `liability` names every peril, such as `collision`, `theft` and `gravel`, with its excess in minor units after every cover on the quote. 0 is covered in full. Null is not covered: the customer pays the whole cost, up to the car's value. `depositMinor` is what the rental holds on the card at pickup.

Ask a quote for each level or package the customer can pick, in one `POST /quotes` call, and show the excess and the deposit side by side. The booking made from the same order keeps the same excess and deposit.

An included cover that the customer may refuse has `includedByDefault` and `declinable` true on `GET /rentals/{slug}/products`. Send its id in `declined`, on the quote as on the booking. The quote then shows the price and the excess without it. Any other id answers `422` with `invalid`.

On the products, a cover also has `exclusions`, what it does not cover, in the asked `locale` when the rental translated them. `insurer` names the insurer when the cover is an insurer's policy, else it is null.

## Many rentals in one call

A group site that sells several rentals asks about all of them at once. A reseller has its own paths, with prices and holds: see [Sell as a reseller](/resellers).

```
GET  /availability?from=2027-06-01&to=2027-06-04
GET  /availability?from=2027-06-01&to=2027-06-04&rentals=generic-campers,generic-cars
POST /quotes
```

`/availability` gives the answer above for every rental the key reaches, one block per rental slug. `rentals=` limits it to some of them.

`/quotes` takes up to 50 quotes as `{"quotes": [...]}`. Each one is the body of a single quote plus `rental`, the slug. The answers come in the order asked. An answer has `data` with the quote, or `error`: `not_found` when the rental or car type is not there or nothing prices it, `driver_too_young` for a driver under the car type's minimum age. One failed quote leaves the others alone.

Car type and location ids belong to one rental. Read them per rental from `/rentals/{slug}/car-types` and `/rentals/{slug}/locations`, and keep them.

## 3. Book

```
POST /rentals/{slug}/bookings
Idempotency-Key: 2f5c-…
```

Send the same fields as the quote plus the customer. The booking is priced again at that moment and the price frozen on it, so a rate change later never rewrites it. The answer is `201` with the booking.

Always send an `Idempotency-Key`. A retry with the same key and the same body gets the first answer again, so a retry after a timeout never makes two bookings. A `500` is the exception: the key then stays in progress for 24 hours. [Errors](/errors) says what to do. A `403` or a `404` is not kept under the key, so the same key works again once the cause is gone. The same key with a different body is refused with `409` and the code `idempotency_mismatch`. A retry while the first call still runs gets `409` with `in_progress`: wait a moment and try again.

When the car type is full, the answer is `409` with the code `none_free`. Nothing is booked.

When the booking cannot be priced, the answer is `422`. The code is `quote` when the quote had a problem that stops a booking, and the message names it. A driver under the car type's minimum age answers `422` with `driver_too_young`.

The booking keeps the driver's age you sent. A change later prices the booking again with that age, so a young driver fee stays on the booking.

### The customer

`customer` takes `firstName` and `lastName`, and `email`, `phone` and `country` if you have them. `country` is the country the customer lives in, as two letters, such as `DE`. It counts for a customer the booking adds, and a customer the rental already has keeps the country on file. The booking answers `customer.country`, or null when the rental has none. A customer the rental already knows by that email and the same first and last name is the same customer. The email alone makes a new customer.

Send `marketingOptIn: true` only when the customer ticked a box to get offers from the rental. The rental keeps the time and that it came through the API. It counts for a customer the booking adds. A customer the rental already has keeps the answer on file. `false`, or no field, changes nothing on file.

### Custom fields

A rental can add its own fields to bookings and to customers, such as a voucher number or a member number. The rental picks which of them go through the API, with **Send through the API** on the field. Only those fields are read and written here, and only by the rental's own keys. A reseller's key and the reseller routes do not carry them.

A booking carries `customFields`, and its `customer` carries `customFields` too. Each is an object by the field's key. A value nobody gave reads as the field's default, or `null`.

```json
{
  "customFields": { "voucher": "A-1042" },
  "customer": {
    "firstName": "Anna",
    "lastName": "Berg",
    "customFields": { "member_number": "M-7" }
  }
}
```

Send the same two objects when you make a booking. A value has the JSON type of its field:

| Field type        | Value                                 |
| ----------------- | ------------------------------------- |
| Text, long text   | A string.                             |
| Link              | A string that starts with `http`.     |
| Number            | A number.                             |
| Yes or no         | `true` or `false`.                    |
| One of a list     | The value of one choice, as a string. |
| Several of a list | A list of choice values.              |
| Date              | A string, as `2026-06-01`.            |

These answer `422` with `invalid`, and no booking is made. The `message` names each field:

- A key that is no field of the rental, or a field the rental does not send through the API.
- A value of the wrong type, or one that is not among the choices.
- A required field with no default that is left out, on the booking or on the customer.

The customer's fields count only for a customer the booking adds. A customer the rental already has keeps the values on file. A required customer field must come with every booking all the same: the rental finds the customer only after it holds the car.

While the rental's plan has no booking and customer fields, both objects are empty and every key is refused.

### Who referred the customer

A customer who came through an affiliate's link lands on your site with `?mref=` in the address. Keep it for the session and send it back as `mref` on the booking, as it came. An unknown or expired one is ignored, never refused.

When your site knows the affiliate but has no `mref`, send their code as `affiliate`. A code outside the rental's program is ignored. The API never tells you whom a booking was attributed to: that is the rental's business.

### Warnings

A booking carries `warnings`: what the rental should know before it hands over a car. The same customer already has a car over these dates, an earlier booking ended owing money, a chargeback, a habit of cancelling. None of them stops anything. A key of one channel sees only what the bookings of that channel show. The warnings are for the people who sell and hand over the car. Never show them to the customer.

## Payment

```
POST /rentals/{slug}/bookings/{id}/payments
```

Starts a payment at the rental's own processor. No card data passes through this API. It needs a key with the `payment.take` scope.

- `mode: "link"` answers with a `url`. Send the customer there. They land back on `returnUrl`, or on the rental's own return address.
- `mode: "embedded"` answers with `session`, the values that processor's own browser code takes. Draw the form on your own site.
- `kind: "hold"` places a deposit instead of taking the money, where the processor allows it.
- `amount` left out: a sale asks for `payNowMinor`, the balance less the fees paid on arrival. When only those fees are left, it asks for the balance. A hold asks for the deposit. When nothing is due, the answer is `422` with `invalid`.

The payment is only started here. It counts on the booking when the processor tells us it happened. Read the booking again, or listen for the `booking.payment` webhook.

A rental with no online processor answers `409` with `no_processor`. A link with no return address answers `409` with `no_return_url`. A processor that refuses answers `409` with `processor`, and the message is the processor's.

## Only while reserved

A change or a cancel through the API works only while the booking is reserved. The rule is the same for every key: a website's, an own reseller's, the organization's and a reseller on Mellow's. Once the car is out, or the booking is returned, closed, cancelled or a no-show, the API answers `409` with the code `wrong_status`.

A later change, such as a longer rental, goes through the rental. The customer's manage booking page then shows how to reach the rental.

## Cancelling

```
POST /rentals/{slug}/bookings/{id}/cancel
```

Send a `reason` if you have one. Only a reserved booking can be cancelled. Any other status answers `409` with the code `wrong_status`.

## Changing

```
PATCH /rentals/{slug}/bookings/{id}
```

Send only what changes: `pickupAt`, `returnAt`, `pickupLocationId`, `returnLocationId`, `carTypeId`, `customFields`, and a `reason` if you have one. The booking is priced again over its new span by the same rules as a new one, and the extras it already has come along. The answer is the booking as it now stands.

A change can refuse:

- `409` with `none_free`: no car of that car type is free for the new span. The booking is unchanged.
- `409` with `wrong_status`: the booking is no longer reserved, so the API cannot change it.
- `409` with `manual_price`: the rental priced the booking by hand. Only the rental changes it.
- `409` with `below_paid`: the new total is below what is paid. A refund is the rental's decision. The new total counts the charges the rental added by hand, which a change keeps.
- `422` with `driver_too_young`: the new car type needs an older driver than the age sent with the booking.
- `422`: the new span cannot be priced, and the `message` says why.

`customFields` on a change holds only the fields that change. A key left out keeps its value. `null` takes a value off, but not from a required field. A change of custom fields alone prices nothing. The customer's own fields do not change here. See [Custom fields](#custom-fields).

## A paused channel

The rental can pause a reseller's channel on the reseller's page. Its key still reads its bookings. Availability and a quote answer `403` with the code `paused`, and so do a hold, a new booking, a change, a cancel and a check-in mark. Across rentals, `/availability` and `/quotes` leave a paused rental out, or answer `paused` for it.

## Reading

`GET /rentals/{slug}/bookings` lists the bookings on your channel: 100, or up to 200 with `limit`. They come newest pickup first, then newest made. `from` and `to` pick the bookings that touch those days. `GET /rentals/{slug}/bookings/{id}` reads one. A key never sees another channel's bookings: those answer `404`.
