Panion Docs

API

Quick book: sales

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

Restricted.

Needs a Panion-issued Quick book key. Not available with partner API keys. Contact 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

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}]}'
{
  "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.

Needs a Panion-issued Quick book key. Not available with partner API keys. Contact 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

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"}'
{
  "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

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"}}'
{
  "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.

Needs a Panion-issued Quick book key. Not available with partner API keys. Contact 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

curl "https://www.panion.travel/api/quickbook/v1/sales/PAN-3F9A1C2B7D" \
  -H "Authorization: Bearer $PANION_QUICKBOOK_KEY" \
  -H "Quickbook-Terminal: $TERMINAL_ID"
{
  "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.

Needs a Panion-issued Quick book key. Not available with partner API keys. Contact 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

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"
{
  "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.

On this page