# Quick book: sales

> Quote a sale, book it after payment, read its status and reprint the ticket.

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

> **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/quote

Quote a sale and get the cart line.

Checks the tour is sellable on this terminal and has the seats now, and returns the sale `reference` and the cart line. Holds nothing: seats are taken by `book` after payment (or by the optional `reserve`). Take payment for `cartLine.totalMinor` following the payment instructions in `cartLine.payment`, with `reference` as the payment's reference. Until `validUntil` the quoted price stands.

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

Request body (JSON):

- `activityId` (string): Panion activity id (a UUID), from GET /tours.
- `start` (object): The departure: local date and start time, as listed by GET /tours.
  - `date` (string, YYYY-MM-DD): Local date, YYYY-MM-DD.
  - `time` (string): Local wall-clock start time, HH:MM (24 hour).
- `guests` (array): Guests per type: at most 10 entries and 50 guests in all.
  - each item:
    - `type` (string): Guest type key from GET /tours/{activityId}/slots (`adult`, `child` and so on, from the operator's pricing categories).
    - `count` (integer)

Response fields (200):

- `reference` (string): The Panion sale reference, `PAN-` and 10 uppercase letters or digits. Pass it to the payment provider as the payment's reference.
- `validUntil` (string, ISO 8601 date-time): The quoted price stands until then. After it, `book` checks the live price.
- `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: Two adults and a child at 09:00

```sh
curl -X POST "https://www.panion.travel/api/quickbook/v1/quote" \
  -H "Authorization: Bearer $PANION_QUICKBOOK_KEY" \
  -H "Quickbook-Terminal: $TERMINAL_ID" \
  -H "Content-Type: application/json" \
  -d '{"activityId":"6f1e2d3c-4b5a-4c6d-8e7f-9a0b1c2d3e4f","start":{"date":"2026-11-14","time":"09:00"},"guests":[{"type":"adult","count":2},{"type":"child","count":1}]}'
```

```json
{
  "reference": "PAN-3F9A1C2B7D",
  "validUntil": "2026-11-13T10:15: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).
- 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.

### POST /api/quickbook/v1/book

Book a paid sale.

Call after the guest has paid. Panion verifies the payment with the payment provider (paid, amount, currency, merchant, reference and terminal), then books with the operator: without `holdRef` it reserves and confirms in one go, with `holdRef` it confirms that hold. Answers `confirmed` with the ticket, or `failed` with the reason, whether the money was returned and a `clerkMessage` for the person at the desk: a booking that fails after payment is refunded automatically. Idempotent on `payment.provider` and `payment.paymentId`: a repeat returns the stored result and never books or refunds twice. A second, different payment for the same `reference` is refused and refunded (`duplicate_payment`). After a timeout, call GET /sales/{reference} before trying again. A refusal before any money moved (key, terminal, scope, `payment_not_found`, `payment_mismatch`) answers with an error body; a failure after the payment was verified answers 200 `failed` with the same code as `reason`.

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

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.
- `holdRef` (string, optional): The hold from POST /reserve, when the client took one.
- `payment` (object): The payment that paid the sale.
  - `provider` (one of `sagapay`): The payment provider that took the payment; more may be added.
  - `orderId` (string): The paid order at the payment provider. For SagaPay: the SagaPay order id.
  - `paymentId` (string): The payment at the payment provider. For SagaPay: the SagaPay payment id. `book` is idempotent on `payment.provider` and `payment.paymentId`: a repeat returns the stored result.
- `customer` (object)
  - `firstName` (string)
  - `lastName` (string)
  - `email` (string)
  - `phone` (string, optional): International format (+47...). Required when `requiresPhone` is true.
- `language` (one of `en`, `nb`, optional): Language of the guest's email and the ticket. Default en.

Response fields (200):

Either:

- `status` (`confirmed`)
- `ticket` (object)
  - `lines32` (array): Ticket text, at most 32 characters a line.
  - `lines48` (array): Ticket text, at most 48 characters a line.
  - `qrPayload` (string): Print as a QR code on the ticket: the guest's booking page link.
  - `pdfUrl` (string, nullable): The ticket as a PDF, when available.
  - `bookingCode` (string): The booking code printed on the ticket.

Or:

- `status` (`failed`)
- `reason` (one of `sold_out`, `price_changed`, `provider_unavailable`, `hold_expired`, `hold_not_supported`, `terminal_not_registered`, `tour_not_sellable_here`, `phone_required`, `phone_rejected`, `payment_not_found`, `payment_mismatch`, `duplicate_payment`, `provider_refused`)
- `moneyReturned` (boolean): The payment was refunded or voided. When false, Panion is still returning it and the `clerkMessage` says so.
- `clerkMessage` (string): Show this to the clerk.

Example: Booked

```sh
curl -X POST "https://www.panion.travel/api/quickbook/v1/book" \
  -H "Authorization: Bearer $PANION_QUICKBOOK_KEY" \
  -H "Quickbook-Terminal: $TERMINAL_ID" \
  -H "Content-Type: application/json" \
  -d '{"reference":"PAN-3F9A1C2B7D","payment":{"provider":"sagapay","orderId":"84c1d2e3f4a5b6c7d8","paymentId":"84e9f8a7b6c5d4e3f2"},"customer":{"firstName":"Ingrid","lastName":"Hansen","email":"ingrid.hansen@example.com","phone":"+4791234567"},"language":"nb"}'
```

```json
{
  "status": "confirmed",
  "ticket": {
    "lines32": [
      "Destination Tromsø",
      "Fjord and Whale Safari",
      "14.11.2026 09:00",
      "2 x Adult, 1 x Child",
      "Meet: Stortorget 1, Tromsø",
      "Booking PAN-3F9A1C2B7D",
      "Paid NOK 4725.00",
      "Sold at Booking Point"
    ],
    "lines48": [
      "Destination Tromsø: Fjord and Whale Safari",
      "14.11.2026 09:00, 2 x Adult, 1 x Child",
      "Meet: Stortorget 1, Tromsø",
      "Booking PAN-3F9A1C2B7D, paid NOK 4725.00",
      "Sold at Booking Point"
    ],
    "qrPayload": "https://panion.travel/booking/BOOKING_TOKEN",
    "pdfUrl": null,
    "bookingCode": "PAN-3F9A1C2B7D"
  }
}
```

Example: Sold out after payment, refunded

```sh
curl -X POST "https://www.panion.travel/api/quickbook/v1/book" \
  -H "Authorization: Bearer $PANION_QUICKBOOK_KEY" \
  -H "Quickbook-Terminal: $TERMINAL_ID" \
  -H "Content-Type: application/json" \
  -d '{"reference":"PAN-3F9A1C2B7D","payment":{"provider":"sagapay","orderId":"84c1d2e3f4a5b6c7d8","paymentId":"84e9f8a7b6c5d4e3f2"},"customer":{"firstName":"Ingrid","lastName":"Hansen","email":"ingrid.hansen@example.com","phone":"+4791234567"}}'
```

```json
{
  "status": "failed",
  "reason": "sold_out",
  "moneyReturned": true,
  "clerkMessage": "Not booked: this departure is sold out. The payment has been refunded. Reference PAN-3F9A1C2B7D"
}
```

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: `payment_mismatch`: The payment does not match the sale (amount, currency, merchant, reference or terminal). Nothing was booked, nothing is refunded automatically, and Panion is alerted. `duplicate_payment`: The sale is already paid with a different payment. The second payment is refunded. `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). `provider_refused`: The tour operator refused the booking.
- 410: `hold_expired`: The seat hold ran out.
- 422: `payment_not_found`: Panion cannot find this payment at the payment provider. Nothing was booked. `phone_required`: This tour needs the guest's phone number. `phone_rejected`: The tour operator did not accept the phone number.
- 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.

### GET /api/quickbook/v1/sales/{reference}

Status of a sale.

The sale's status (`quoted`, `held`, `expired`, `booking`, `confirmed`, `failed`) and, when confirmed, the ticket. Use it after a timeout or a client restart to learn what happened before calling `book` again.

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

Path parameters:

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

Response fields (200):

- `reference` (string): The Panion sale reference, `PAN-` and 10 uppercase letters or digits. Pass it to the payment provider as the payment's reference.
- `status` (one of `quoted`, `held`, `expired`, `booking`, `confirmed`, `failed`)
- `ticket` (object, optional): Present when the sale is confirmed.
  - `lines32` (array): Ticket text, at most 32 characters a line.
  - `lines48` (array): Ticket text, at most 48 characters a line.
  - `qrPayload` (string): Print as a QR code on the ticket: the guest's booking page link.
  - `pdfUrl` (string, nullable): The ticket as a PDF, when available.
  - `bookingCode` (string): The booking code printed on the ticket.

Example: A confirmed sale

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

```json
{
  "reference": "PAN-3F9A1C2B7D",
  "status": "confirmed",
  "ticket": {
    "lines32": [
      "Destination Tromsø",
      "Fjord and Whale Safari",
      "14.11.2026 09:00",
      "2 x Adult, 1 x Child",
      "Meet: Stortorget 1, Tromsø",
      "Booking PAN-3F9A1C2B7D",
      "Paid NOK 4725.00",
      "Sold at Booking Point"
    ],
    "lines48": [
      "Destination Tromsø: Fjord and Whale Safari",
      "14.11.2026 09:00, 2 x Adult, 1 x Child",
      "Meet: Stortorget 1, Tromsø",
      "Booking PAN-3F9A1C2B7D, paid NOK 4725.00",
      "Sold at Booking Point"
    ],
    "qrPayload": "https://panion.travel/booking/BOOKING_TOKEN",
    "pdfUrl": null,
    "bookingCode": "PAN-3F9A1C2B7D"
  }
}
```

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.

> **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.

### GET /api/quickbook/v1/sales/{reference}/ticket

Reprint the ticket of a confirmed sale.

The ticket of a confirmed sale, to print again: text lines for 32 and 48 column printers, a QR payload, a PDF link and the booking code.

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

Path parameters:

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

Query parameters:

- `lang` (one of `en`, `nb`, optional, default "en"): Language of the ticket text. Default en.

Response fields (200):

- `lines32` (array): Ticket text, at most 32 characters a line.
- `lines48` (array): Ticket text, at most 48 characters a line.
- `qrPayload` (string): Print as a QR code on the ticket: the guest's booking page link.
- `pdfUrl` (string, nullable): The ticket as a PDF, when available.
- `bookingCode` (string): The booking code printed on the ticket.

Example: Reprint

```sh
curl "https://www.panion.travel/api/quickbook/v1/sales/PAN-3F9A1C2B7D/ticket?lang=en" \
  -H "Authorization: Bearer $PANION_QUICKBOOK_KEY" \
  -H "Quickbook-Terminal: $TERMINAL_ID"
```

```json
{
  "lines32": [
    "Destination Tromsø",
    "Fjord and Whale Safari",
    "14.11.2026 09:00",
    "2 x Adult, 1 x Child",
    "Meet: Stortorget 1, Tromsø",
    "Booking PAN-3F9A1C2B7D",
    "Paid NOK 4725.00",
    "Sold at Booking Point"
  ],
  "lines48": [
    "Destination Tromsø: Fjord and Whale Safari",
    "14.11.2026 09:00, 2 x Adult, 1 x Child",
    "Meet: Stortorget 1, Tromsø",
    "Booking PAN-3F9A1C2B7D, paid NOK 4725.00",
    "Sold at Booking Point"
  ],
  "qrPayload": "https://panion.travel/booking/BOOKING_TOKEN",
  "pdfUrl": null,
  "bookingCode": "PAN-3F9A1C2B7D"
}
```

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.
