# Quick book: holds (optional)

> Optional seat holds for scarce departures while the guest pays, and releasing them.

Source: https://www.panion.travel/docs/api/quickbook/holds

> **Restricted access:** Needs a Panion-issued Quick book key. Not available with partner API keys. Contact [hello@panion.travel](mailto:hello@panion.travel) for access.

### POST /api/quickbook/v1/reserve

Optional: hold the seats of a quoted sale while the guest pays.

Optional. The main path books after payment without a hold. Use `reserve` for scarce departures (few seats left, popular evening departures), where a seat could go to the web while the guest pays. Takes a hold in the operator's booking system and returns `holdRef` (the same value as `reference`), `expiresAt` and the cart line checked again. A tour whose booking system cannot hold answers `hold_not_supported`: carry on with the main path.

Headers: `Authorization: Bearer pqb_live_...` and `Quickbook-Terminal: <terminal id>`. Rate limit: writes.

Request body (JSON):

- `reference` (string): The Panion sale reference, `PAN-` and 10 uppercase letters or digits. Pass it to the payment provider as the payment's reference.
- `idempotencyKey` (string): A new value (a UUID) per hold. A retry with the same value returns the same hold.

Response fields (200):

- `holdRef` (string): The hold, the same value as the sale `reference`.
- `expiresAt` (string, ISO 8601 date-time): The hold is released then. Show a timer.
- `cartLine` (object)
  - `operatorName` (string)
  - `tourName` (string)
  - `start` (object)
    - `date` (string, YYYY-MM-DD): Local date, YYYY-MM-DD.
    - `time` (string): Local wall-clock start time, HH:MM (24 hour).
    - `timeZone` (string): IANA time zone, e.g. Europe/Oslo.
  - `guests` (array)
    - each item:
      - `type` (string): Guest type key from GET /tours/{activityId}/slots (`adult`, `child` and so on, from the operator's pricing categories).
      - `label` (string)
      - `count` (integer)
      - `unitMinor` (integer): Price per guest, in minor units.
      - `lineMinor` (integer): unitMinor times count, in minor units.
  - `totalMinor` (integer): The amount to charge, VAT included, in minor units.
  - `currency` (string): ISO 4217 currency code.
  - `payment` (object): Payment instructions for the provider named in `provider`. For a SagaPay payment, create the order on payment.merchantId for `totalMinor` with these tax lines and service providers.
    - `provider` (one of `sagapay`): The payment provider these instructions are for; more may be added. Today only `sagapay`.
    - `merchantId` (string): For a SagaPay payment: create the order on payment.merchantId, the selling operator's SagaPay merchant, and set the order's `referenceId` to the sale `reference`.
    - `taxLines` (array): For a SagaPay payment: the order's tax array, the VAT included in the total, per rate. Pass it unchanged.
      - each item:
        - `amount` (integer): VAT included in the total at this rate, in minor units.
        - `percentage` (number): VAT rate in percent, e.g. 12.
        - `type` (`VAT`)
    - `serviceProviders` (array): For a SagaPay payment: Panion's fee as a service-provider split. Pass it unchanged in the order's `controlFunctions.serviceProviders`.
      - each item:
        - `serviceProviderId` (string): Panion's SagaPay service provider id.
        - `amount` (object)
  - `requiresPhone` (boolean)
  - `orderLineText` (string): Short label for the receipt and the order: tour name, date and time, at most 40 characters.

Example: Hold a quoted sale

```sh
curl -X POST "https://www.panion.travel/api/quickbook/v1/reserve" \
  -H "Authorization: Bearer $PANION_QUICKBOOK_KEY" \
  -H "Quickbook-Terminal: $TERMINAL_ID" \
  -H "Content-Type: application/json" \
  -d '{"reference":"PAN-3F9A1C2B7D","idempotencyKey":"2d1c6f0e-8a4b-4e3c-9b7a-5f6e4d3c2b1a"}'
```

```json
{
  "holdRef": "PAN-3F9A1C2B7D",
  "expiresAt": "2026-11-13T10:20:00+01:00",
  "cartLine": {
    "operatorName": "Destination Tromsø",
    "tourName": "Fjord and Whale Safari",
    "start": {
      "date": "2026-11-14",
      "time": "09:00",
      "timeZone": "Europe/Oslo"
    },
    "guests": [
      {
        "type": "adult",
        "label": "Adult",
        "count": 2,
        "unitMinor": 189000,
        "lineMinor": 378000
      },
      {
        "type": "child",
        "label": "Child",
        "count": 1,
        "unitMinor": 94500,
        "lineMinor": 94500
      }
    ],
    "totalMinor": 472500,
    "currency": "NOK",
    "requiresPhone": true,
    "orderLineText": "Fjord and Whale Safari 2026-11-14 09:00",
    "payment": {
      "provider": "sagapay",
      "merchantId": "MERCHANT_ID",
      "taxLines": [
        {
          "amount": 50625,
          "percentage": 12,
          "type": "VAT"
        }
      ],
      "serviceProviders": [
        {
          "serviceProviderId": "PANION_SERVICE_PROVIDER_ID",
          "amount": {
            "percentage": 12.5
          }
        }
      ]
    }
  }
}
```

Errors:

- 400: `terminal_not_registered`: the `Quickbook-Terminal` header is missing or malformed. `invalid_request`: The body or query does not validate (a missing field, a bad date or number).
- 401: `unauthorized`: The Quick book key is missing, not valid, expired or revoked, or was sent in the URL. Answers with `WWW-Authenticate: Bearer realm="panion-quickbook"`.
- 403: `terminal_not_registered`: The terminal in the `Quickbook-Terminal` header is not registered, not active or not bound to this key. A missing or malformed header answers 400 with this code. `tour_not_sellable_here`: This terminal cannot charge for this tour's operator, so it cannot sell it.
- 409: `sold_out`: Not enough seats left on this departure. `price_changed`: The live price differs from the quote, and the quote is past `validUntil`. From `quote`: a guest type the tour does not sell (read the slots again).
- 410: `hold_expired`: The seat hold ran out.
- 422: `hold_not_supported`: This tour's booking system cannot hold seats. Carry on without `reserve`: take payment and call `book`.
- 429: `rate_limited`: Too many requests from this terminal: 60 writes, 60 books and 300 reads per minute. `Retry-After` says when to try again.
- 503: `provider_unavailable`: The tour operator's booking system did not answer. Try again in a moment.

> **Restricted access:** Needs a Panion-issued Quick book key. Not available with partner API keys. Contact [hello@panion.travel](mailto:hello@panion.travel) for access.

### DELETE /api/quickbook/v1/reserve/{holdRef}

Optional: release a hold.

Optional, only after `reserve`. Releases the seats when the guest does not pay. Idempotent: releasing a hold that is already released or expired answers the same.

Headers: `Authorization: Bearer pqb_live_...` and `Quickbook-Terminal: <terminal id>`. Rate limit: writes.

Path parameters:

- `holdRef` (string): The `holdRef` from POST /reserve.

Response fields (200):

- `holdRef` (string): The Panion sale reference, `PAN-` and 10 uppercase letters or digits. Pass it to the payment provider as the payment's reference.
- `released` (`true`)

Example: Release a hold

```sh
curl -X DELETE "https://www.panion.travel/api/quickbook/v1/reserve/PAN-3F9A1C2B7D" \
  -H "Authorization: Bearer $PANION_QUICKBOOK_KEY" \
  -H "Quickbook-Terminal: $TERMINAL_ID"
```

```json
{
  "holdRef": "PAN-3F9A1C2B7D",
  "released": true
}
```

Errors:

- 400: `terminal_not_registered`: the `Quickbook-Terminal` header is missing or malformed. `invalid_request`: The body or query does not validate (a missing field, a bad date or number).
- 401: `unauthorized`: The Quick book key is missing, not valid, expired or revoked, or was sent in the URL. Answers with `WWW-Authenticate: Bearer realm="panion-quickbook"`.
- 403: `terminal_not_registered`: The terminal in the `Quickbook-Terminal` header is not registered, not active or not bound to this key. A missing or malformed header answers 400 with this code.
- 404: `not_found`: No such sale or hold for this terminal: the reference is unknown or belongs to another terminal.
- 429: `rate_limited`: Too many requests from this terminal: 60 writes, 60 books and 300 reads per minute. `Retry-After` says when to try again.
- 503: `provider_unavailable`: The tour operator's booking system did not answer. Try again in a moment.
