# Tours

> The tours your site sells, to build your own tour pages.

Source: https://www.panion.travel/docs/api/reference/tours

Read the tours your site sells and render your own pages, then let the Panion checkout take the booking. Your own tours on sale and the tours you resell for other operators are in the catalogue; draft and archived tours never are, and a tour your site does not sell answers 404.

## Keys

- A **publishable key** (`pk_widget_...`): the `data-panion-key` already in your checkout script tag. Pass it as `?key=` with no Authorization header. It reads only public catalogue data for its own site, so it is safe in a browser and in a static build. Browsers can read the response from the key's allowed domains only (CORS).
- A **secret key** (`pnn_live_...`) with the `tours:read` scope, sent as `Authorization: Bearer`. It reads the catalogue of every operator on the key; add `partner_id` for one. Keep it on your server.

## Quickstart

1. List your tours:

```sh
curl "https://www.panion.travel/api/v1/tours?key=pk_widget_...&locale=en"
```

2. Read one tour for its page (title, description HTML, photos, inclusions, meeting point, departure times, From price, season, who operates it):

```sh
curl "https://www.panion.travel/api/v1/tours/<tour id>?key=pk_widget_...&locale=nb"
```

3. Drop in the checkout with the tour's `checkout.activity_id`:

```html
<script src="https://www.panion.travel/sdk/core.js" data-panion-key="pk_widget_..." defer></script>
<div data-panion-checkout data-activity-id="<tour id>"></div>
```

The checkout quotes each date, takes payment and books in the operator's booking system. The API itself never reserves or books.

## Rules for your page

- Show `from_price` as the From price (titles, cards, JSON-LD offers). It is the lowest regular adult price of the season and ignores last-minute reductions in the next 7 days. Date prices are the checkout's job.
- When `operator.is_host` is false, someone else runs the tour: say "Operated by" with `operator.name`, and do not show your own ratings or brand as theirs. `seller` is the seller of record.
- `rating` is the booking system's rating for the tour, when it has one.
- `season.known: false` means the booking system did not answer in time; keep the tour bookable rather than showing it closed.
- Translations: pass `locale`. Translated fields replace English ones; the rest stay English. `locale` in the response says which translation was used.

## Availability

GET /api/v1/tours/{id}/availability?from=YYYY-MM-DD&to=YYYY-MM-DD&pax=2 answers whether each day can be booked (up to 366 days) and, for up to 14 days, each departure with seats left, `fits_party` and the booking system's prices. Use it for a search by date or a calendar on your own page.

## Caching and limits

Publishable-key responses are public and cacheable; cache them in your build or CDN too:

- GET /api/v1/tours: s-maxage=300, stale-while-revalidate=3600.
- GET /api/v1/tours/{id}: s-maxage=600, stale-while-revalidate=86400.
- GET /api/v1/tours/{id}/availability: s-maxage=60, stale-while-revalidate=300.

Uncached publishable-key requests are limited to 1200 per minute per key (429 with `Retry-After`). Secret keys share the key's normal per-minute limit and show in its usage.

### GET /api/v1/tours

List the tours your site sells.

Every tour your site sells: your own tours on sale and the ones you resell for other operators. With a publishable key that is the key's site; with a secret key, the sites of every operator on the key (or the one in `partner_id`). Draft and archived tours never appear. Each carries the advertised From price, the season and who operates it. Stable order, 50 per page by default.

Scope: `tours:read`.

Query parameters:

- `key` (string, optional): Your site's publishable widget key (`pk_widget_...`, the `data-panion-key` of your checkout script tag). Use it instead of a secret key in browsers and at build time. Omit it when you send `Authorization: Bearer pnn_live_...`.
- `locale` (string, optional): Locale code (`nb`, `de`, `fr`). Translated fields replace the English ones; untranslated fields stay English. Omit for English.
- `cursor` (string, optional): `next_cursor` of the last page.
- `limit` (integer, optional, default 50)
- `partner_id` (string, optional): Secret keys only: the catalogue of this one operator on the key. Any other id answers 404.

Response fields (200):

- `data` (array)
  - each item:
    - `id` (string): The tour id. Use it as `data-activity-id` on the checkout tag.
    - `slug` (string, nullable)
    - `title` (string)
    - `excerpt` (string, nullable)
    - `photos` (array): Image URLs, cover first.
    - `duration_minutes` (integer, nullable)
    - `priced_per_booking` (boolean): True when the price is for the whole group, not per person.
    - `languages` (array): Guide languages as ISO 639-1 codes (`en`, `de`).
    - `from_price` (object, nullable): The advertised From price: the lowest regular adult price over the bookable season, without date-based checkout discounts and ignoring the next 7 days (where booking systems put last-minute reductions). Use it for titles, cards and JSON-LD offers. The checkout quotes each date itself.
      - `amount_minor` (integer): Integer minor units (øre, cents).
      - `currency` (string): ISO 4217 code.
    - `season` (object)
      - `known` (boolean): False when the booking system did not answer in time. Unknown is never closed: keep showing the tour.
      - `next_available_date` (string, YYYY-MM-DD, nullable): First day from today with a bookable departure in the next 12 months, or null when nothing is on sale (with `known` true) or unknown.
    - `rating` (object, nullable): The rating the tour's booking system publishes for it, when it has any reviews. Not Panion's own verified reviews.
      - `average` (number): Average rating out of 5.
      - `count` (integer)
    - `operator` (object, nullable): Who runs the tour ("Operated by"): the named operator, else the seller.
      - `name` (string)
      - `legal_name` (string, nullable)
      - `org_number` (string, nullable)
      - `is_seller` (boolean): True when the seller of record (`seller`) runs the tour itself.
      - `is_host` (boolean): True only when the business behind the asking site (the publishable key's operator, or one of the secret key's operators) sells AND runs the tour. When false, your page must say who operates the tour and must not show your own ratings or brand on it.
    - `seller` (object, nullable): The seller of record: the Panion operator the booking goes through.
      - `name` (string): Public brand.
      - `legal_name` (string, nullable): Registered legal name, when known.
      - `org_number` (string, nullable): Organisation number, when known.
    - `locale` (string): The translation applied (`nb`, `de`), or `en` when the tour has none for the requested locale. Fields without a translation stay English.
- `next_cursor` (string, nullable): Pass as `cursor` for the next page.

Example: The first page of a site's catalogue

```sh
curl "https://www.panion.travel/api/v1/tours?key=pk_widget_3xAmpLeKeyF0rD0cs&limit=50"
```

```json
{
  "data": [
    {
      "id": "3a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
      "slug": "northern-lights-chase",
      "title": "Northern Lights Chase",
      "excerpt": "Follow the clear skies away from the city lights with a small group.",
      "photos": [
        "https://images.example.com/aurora-1.jpg",
        "https://images.example.com/aurora-2.jpg"
      ],
      "duration_minutes": 420,
      "priced_per_booking": false,
      "languages": [
        "en",
        "de"
      ],
      "from_price": {
        "amount_minor": 179900,
        "currency": "NOK"
      },
      "season": {
        "known": true,
        "next_available_date": "2026-10-15"
      },
      "rating": {
        "average": 4.8,
        "count": 312
      },
      "operator": {
        "name": "Arctic Tours",
        "legal_name": "Arctic Tours AS",
        "org_number": "912345678",
        "is_seller": true,
        "is_host": true
      },
      "seller": {
        "name": "Arctic Tours",
        "legal_name": "Arctic Tours AS",
        "org_number": "912345678"
      },
      "locale": "en"
    }
  ],
  "next_cursor": null
}
```

Responses:

- 200: OK
- 400: Invalid request
- 401: Missing, invalid, expired or revoked key
- 403: The key lacks the scope
- 429: Rate limited (300 requests per minute per secret key, or 1200 uncached requests per minute per publishable key)

### GET /api/v1/tours/{id}

Get one tour for its page.

Everything a tour page shows: title, description HTML, photos, inclusions, highlights, know-before lines, meeting point, duration, cancellation terms, languages, departure times, pricing categories and options, the advertised From price, the season and next available date, who operates and who sells it, and the checkout's `activity_id`. Translations are merged for `locale`. A tour your site does not sell answers 404.

Scope: `tours:read`.

Path parameters:

- `id` (string): Tour id from GET /api/v1/tours.

Query parameters:

- `key` (string, optional): Your site's publishable widget key (`pk_widget_...`, the `data-panion-key` of your checkout script tag). Use it instead of a secret key in browsers and at build time. Omit it when you send `Authorization: Bearer pnn_live_...`.
- `locale` (string, optional): Locale code (`nb`, `de`, `fr`). Translated fields replace the English ones; untranslated fields stay English. Omit for English.
- `partner_id` (string, optional): Secret keys only: the catalogue of this one operator on the key. Any other id answers 404.

Response fields (200):

- `id` (string): The tour id. Use it as `data-activity-id` on the checkout tag.
- `slug` (string, nullable)
- `title` (string)
- `excerpt` (string, nullable)
- `photos` (array): Image URLs, cover first.
- `duration_minutes` (integer, nullable)
- `priced_per_booking` (boolean): True when the price is for the whole group, not per person.
- `languages` (array): Guide languages as ISO 639-1 codes (`en`, `de`).
- `from_price` (object, nullable): The advertised From price: the lowest regular adult price over the bookable season, without date-based checkout discounts and ignoring the next 7 days (where booking systems put last-minute reductions). Use it for titles, cards and JSON-LD offers. The checkout quotes each date itself.
  - `amount_minor` (integer): Integer minor units (øre, cents).
  - `currency` (string): ISO 4217 code.
- `season` (object)
  - `known` (boolean): False when the booking system did not answer in time. Unknown is never closed: keep showing the tour.
  - `next_available_date` (string, YYYY-MM-DD, nullable): First day from today with a bookable departure in the next 12 months, or null when nothing is on sale (with `known` true) or unknown.
- `rating` (object, nullable): The rating the tour's booking system publishes for it, when it has any reviews. Not Panion's own verified reviews.
  - `average` (number): Average rating out of 5.
  - `count` (integer)
- `operator` (object, nullable): Who runs the tour ("Operated by"): the named operator, else the seller.
  - `name` (string)
  - `legal_name` (string, nullable)
  - `org_number` (string, nullable)
  - `is_seller` (boolean): True when the seller of record (`seller`) runs the tour itself.
  - `is_host` (boolean): True only when the business behind the asking site (the publishable key's operator, or one of the secret key's operators) sells AND runs the tour. When false, your page must say who operates the tour and must not show your own ratings or brand on it.
- `seller` (object, nullable): The seller of record: the Panion operator the booking goes through.
  - `name` (string): Public brand.
  - `legal_name` (string, nullable): Registered legal name, when known.
  - `org_number` (string, nullable): Organisation number, when known.
- `locale` (string): The translation applied (`nb`, `de`), or `en` when the tour has none for the requested locale. Fields without a translation stay English.
- `description_html` (string): Description as clean HTML: p, br, strong, b, em, i, u, ul, ol, li, h2-h4, span and a (href, target, rel only). No inline styles or scripts.
- `included_html` (string)
- `excluded_html` (string)
- `inclusions` (array)
- `exclusions` (array)
- `highlights` (array)
- `know_before` (array)
- `meeting_point` (object, nullable)
  - `name` (string)
  - `address` (string, nullable)
  - `lat` (number, nullable)
  - `lng` (number, nullable)
- `cancellation` (object)
  - `title` (string, nullable)
  - `free_hours` (integer, nullable): Hours before departure until which cancelling is free.
- `min_age` (integer, nullable)
- `difficulty` (string, nullable)
- `departure_times` (array): Usual local start times (HH:MM), sorted, without duplicates.
- `pricing_categories` (array)
  - each item:
    - `id` (string): The booking system's category id.
    - `is_default` (boolean): The default (normally adult) category.
- `rates` (array)
  - each item:
    - `id` (string)
    - `title` (string)
    - `description` (string, nullable)
- `linked_products` (array)
  - each item:
    - `label` (string): How the checkout labels this option card, e.g. a language.
    - `language` (string, nullable): ISO 639-1 code when the option is a guide language.
- `checkout` (object): Drop in the Panion checkout: load https://www.panion.travel/sdk/core.js with data-panion-key set to your publishable key, then add <div data-panion-checkout data-activity-id="<activity_id>"></div>.
  - `activity_id` (string)

Example: One tour for its page, in Norwegian

```sh
curl "https://www.panion.travel/api/v1/tours/3a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d?key=pk_widget_3xAmpLeKeyF0rD0cs&locale=nb"
```

```json
{
  "id": "3a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
  "slug": "northern-lights-chase",
  "title": "Nordlysjakt",
  "excerpt": "Follow the clear skies away from the city lights with a small group.",
  "photos": [
    "https://images.example.com/aurora-1.jpg",
    "https://images.example.com/aurora-2.jpg"
  ],
  "duration_minutes": 420,
  "priced_per_booking": false,
  "languages": [
    "en",
    "de"
  ],
  "from_price": {
    "amount_minor": 179900,
    "currency": "NOK"
  },
  "season": {
    "known": true,
    "next_available_date": "2026-10-15"
  },
  "rating": {
    "average": 4.8,
    "count": 312
  },
  "operator": {
    "name": "Arctic Tours",
    "legal_name": "Arctic Tours AS",
    "org_number": "912345678",
    "is_seller": true,
    "is_host": true
  },
  "seller": {
    "name": "Arctic Tours",
    "legal_name": "Arctic Tours AS",
    "org_number": "912345678"
  },
  "locale": "nb",
  "description_html": "<p>Vi kjører dit himmelen er klar.</p><ul><li>Varme dresser</li></ul>",
  "included_html": "<ul><li>Transport</li><li>Varm drikke</li></ul>",
  "excluded_html": "",
  "inclusions": [],
  "exclusions": [],
  "highlights": [
    "Small group, at most 15 guests",
    "Photos of you under the aurora"
  ],
  "know_before": [
    "Dress for minus 20 degrees"
  ],
  "meeting_point": {
    "name": "Radisson Blu Hotel",
    "address": "Sjøgata 7, 9008 Tromsø",
    "lat": 69.6496,
    "lng": 18.9553
  },
  "cancellation": {
    "title": "Free cancellation up to 24 hours before",
    "free_hours": 24
  },
  "min_age": 6,
  "difficulty": "EASY",
  "departure_times": [
    "18:00",
    "19:00"
  ],
  "pricing_categories": [
    {
      "id": "1179264",
      "is_default": true
    },
    {
      "id": "1179265",
      "is_default": false
    }
  ],
  "rates": [
    {
      "id": "TG1",
      "title": "English",
      "description": null
    },
    {
      "id": "TG4",
      "title": "German",
      "description": null
    }
  ],
  "linked_products": [],
  "checkout": {
    "activity_id": "3a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d"
  }
}
```

Responses:

- 200: OK
- 400: Invalid request
- 401: Missing, invalid, expired or revoked key
- 403: The key lacks the scope
- 404: Not found, or not yours
- 429: Rate limited (300 requests per minute per secret key, or 1200 uncached requests per minute per publishable key)

### GET /api/v1/tours/{id}/availability

Bookable days and live departures for a tour.

Read from the tour's booking system through Panion. `days` covers the whole range (up to 366 days): whether each day has a bookable departure and its lowest regular price. For ranges of up to 14 days `departures` lists each departure with seats left, whether `pax` people fit and the booking system's prices. Never reserves anything: send guests to the checkout to book.

Scope: `tours:read`.

Path parameters:

- `id` (string): Tour id from GET /api/v1/tours.

Query parameters:

- `key` (string, optional): Your site's publishable widget key (`pk_widget_...`, the `data-panion-key` of your checkout script tag). Use it instead of a secret key in browsers and at build time. Omit it when you send `Authorization: Bearer pnn_live_...`.
- `from` (string, YYYY-MM-DD): First day (YYYY-MM-DD), the tour's local date.
- `to` (string, YYYY-MM-DD): Last day, inclusive (YYYY-MM-DD).
- `pax` (integer, optional, default 1): Party size, for `fits_party`.
- `partner_id` (string, optional): Secret keys only: the catalogue of this one operator on the key. Any other id answers 404.

Response fields (200):

- `tour_id` (string)
- `currency` (string): ISO 4217 code.
- `from` (string, YYYY-MM-DD)
- `to` (string, YYYY-MM-DD)
- `pax` (integer)
- `known` (boolean): False when the booking system did not answer in time. Unknown is never sold out.
- `days` (array): Every day in the range: whether it has a bookable departure.
  - each item:
    - `date` (string, YYYY-MM-DD)
    - `available` (boolean)
    - `from_price_minor` (integer, nullable): That day's lowest regular adult price, when the booking system has one.
- `departures` (array): Live departures with seats and prices, for ranges of up to 14 days. Prices are the booking system's own before checkout discounts; the checkout quotes the final price.
  - each item:
    - `date` (string, YYYY-MM-DD)
    - `start_time` (string, nullable): Local start time (HH:MM), or null for all-day or flexible.
    - `label` (string, nullable)
    - `seats_remaining` (integer)
    - `fits_party` (boolean): True when `pax` people fit (seats and the departure's minimum party).
    - `rates` (array)
      - each item:
        - `id` (string)
        - `title` (string)
        - `prices` (array): Per-person prices by category.
        - `price_per_booking_minor` (integer, nullable): A flat price for the whole party when the option is priced per booking; `prices` is then empty.

Example: Two days for a party of two

```sh
curl "https://www.panion.travel/api/v1/tours/3a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d/availability?key=pk_widget_3xAmpLeKeyF0rD0cs&from=2026-10-15&to=2026-10-16&pax=2"
```

```json
{
  "tour_id": "3a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
  "currency": "NOK",
  "from": "2026-10-15",
  "to": "2026-10-16",
  "pax": 2,
  "known": true,
  "days": [
    {
      "date": "2026-10-15",
      "available": true,
      "from_price_minor": 179900
    },
    {
      "date": "2026-10-16",
      "available": false,
      "from_price_minor": null
    }
  ],
  "departures": [
    {
      "date": "2026-10-15",
      "start_time": "19:00",
      "label": null,
      "seats_remaining": 9,
      "fits_party": true,
      "rates": [
        {
          "id": "1234567",
          "title": "English",
          "prices": [
            {
              "category": "1179264",
              "amount_minor": 179900
            },
            {
              "category": "1179265",
              "amount_minor": 99800
            }
          ],
          "price_per_booking_minor": null
        }
      ]
    }
  ]
}
```

Responses:

- 200: OK
- 400: Invalid request
- 401: Missing, invalid, expired or revoked key
- 403: The key lacks the scope
- 404: Not found, or not yours
- 429: Rate limited (300 requests per minute per secret key, or 1200 uncached requests per minute per publishable key)
