API
Quick book: sales
Quote a sale, book it after payment, read its status and reprint the ticket.
Restricted.
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,childand so on, from the operator's pricing categories).count(integer)
- each item:
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,bookchecks 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,childand 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.
- each item:
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 inprovider. For a SagaPay payment, create the order on payment.merchantId fortotalMinorwith these tax lines and service providers.provider(one ofsagapay): The payment provider these instructions are for; more may be added. Today onlysagapay.merchantId(string): For a SagaPay payment: create the order on payment.merchantId, the selling operator's SagaPay merchant, and set the order'sreferenceIdto the salereference.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)
- each item:
serviceProviders(array): For a SagaPay payment: Panion's fee as a service-provider split. Pass it unchanged in the order'scontrolFunctions.serviceProviders.- each item:
serviceProviderId(string): Panion's SagaPay service provider id.amount(object)
- each item:
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: theQuickbook-Terminalheader 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 withWWW-Authenticate: Bearer realm="panion-quickbook". - 403:
terminal_not_registered: The terminal in theQuickbook-Terminalheader 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 pastvalidUntil. Fromquote: 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-Aftersays when to try again. - 503:
provider_unavailable: The tour operator's booking system did not answer. Try again in a moment.
Restricted.
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 ofsagapay): 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.bookis idempotent onpayment.providerandpayment.paymentId: a repeat returns the stored result.
customer(object)firstName(string)lastName(string)email(string)phone(string, optional): International format (+47...). Required whenrequiresPhoneis true.
language(one ofen,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 ofsold_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 theclerkMessagesays 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: theQuickbook-Terminalheader 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 withWWW-Authenticate: Bearer realm="panion-quickbook". - 403:
terminal_not_registered: The terminal in theQuickbook-Terminalheader 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 pastvalidUntil. Fromquote: 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-Aftersays when to try again. - 503:
provider_unavailable: The tour operator's booking system did not answer. Try again in a moment.
Restricted.
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 ofquoted,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: theQuickbook-Terminalheader 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 withWWW-Authenticate: Bearer realm="panion-quickbook". - 403:
terminal_not_registered: The terminal in theQuickbook-Terminalheader 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-Aftersays when to try again.
Restricted.
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 ofen,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: theQuickbook-Terminalheader 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 withWWW-Authenticate: Bearer realm="panion-quickbook". - 403:
terminal_not_registered: The terminal in theQuickbook-Terminalheader 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-Aftersays when to try again.