# GraphQL

> Read the organisation, trips with their departures, one trip, guides and destinations in one request, with the schema, examples, errors and limits.

Source: https://docs.bifrostorbit.com/travel/developers/graphql/

GraphQL reads the same trips, departures, guides and destinations as the [booking API](https://docs.bifrostorbit.com/travel/developers/booking-api/). Use it when one page needs several things at once, such as the organisation, a trip and the destinations menu, in a single request.

```
https://app.bifrostorbit.com/travel/api/cdn/graphql
```

GraphQL only reads. It can price a booking with `quote`, which saves and holds nothing; making bookings and the traveller account stay in the [booking API](https://docs.bifrostorbit.com/travel/developers/booking-api/).

## Requests

GraphQL has one address for every organisation, so it needs to know whose trips to read. Either:

- send an [API key](https://docs.bifrostorbit.com/travel/developers/api-keys/) as `Authorization: Bearer <key>` or `x-api-key: <key>`. The key says which organisation it belongs to, and a key that reads drafts (`trv_sk_`) adds draft trips, as in REST.
- or add `?organisation=` and your booking name from [Booking settings](https://docs.bifrostorbit.com/travel/everyone/booking-settings/) to the address, such as `?organisation=northwilds`. Like the booking API, this needs no key.

With both, the key must belong to that organisation.

`POST` a JSON body:

| Property | Type | Means |
| --- | --- | --- |
| `query` | string | The GraphQL document. Required. |
| `variables` | object | Values for the query's variables |
| `operationName` | string | Which operation to run when the document has several |

```bash
curl 'https://app.bifrostorbit.com/travel/api/cdn/graphql' \
  -H 'Authorization: Bearer trv_pk_…' \
  -H 'Content-Type: application/json' \
  -d '{"query":"query ($slug: String!) { trip(slug: $slug, locale: \"nb\") { name priceFrom currency } }","variables":{"slug":"lofoten-ridge-walk"}}'
```

`GET` works too, with `query`, `variables` (as JSON text) and `operationName` in the query string. URL-encode them:

```bash
curl -G 'https://app.bifrostorbit.com/travel/api/cdn/graphql' \
  --data-urlencode 'organisation=northwilds' \
  --data-urlencode 'query={ trips(country: "NO") { slug name nextDeparture } }'
```

Answers are JSON with `data`, and `errors` when something went wrong:

```json
{ "data": { "trip": { "name": "Eggevandring i Lofoten", "priceFrom": 124000, "currency": "EUR" } } }
```

Any website may call it from a browser, as it may read the booking API. A key that reads drafts works only from a server; from another website it gets `403`.

## Queries

Every query takes `locale`, such as `nb`, which works as in the booking API. Leave it out for your main language. Each answer has its own `locale` and `fallback`.

| Query | Arguments | Returns |
| --- | --- | --- |
| `organisation` | `locale` | `Organisation!`. Brand, organiser, travel guarantee, languages and whether online payment works right now. |
| `trips` | `locale`, `activity`, `country`, `category`, `ids: [String!]` | `[Trip!]!`. Published, public trips with their departures. `ids` asks for up to 100 chosen trips, unlisted ones too. |
| `trip` | `slug: String!`, `locale` | `TripDetail`. One trip with its itinerary, packages, extras, payment plan, questions, guides and places to stay. |
| `guides` | `locale` | `[Guide!]!`. Your guides who lead trips or have a bio. |
| `guide` | `slug: String!`, `locale` | `GuideDetail`. One guide with the trips they lead and their pick. |
| `destinations` | `locale` | `[Continent!]!`. Continents and countries your public trips visit, and countries you have published without trips. |
| `continent` | `key: String!`, `locale` | `Continent`. A continent, such as `europe`, with its countries and your text once published. |
| `country` | `continent: String!`, `country: String!`, `locale` | `Country`. Facts, your text, trips, local time, weather and the exchange rate. `country` is a slug or two-letter code. |
| `quote` | `input: QuoteInput!` with `departureId`, `travellers` (each with an optional `packageId`), `addOns` (`id`, `quantity`), `discountCode`, `currency`, `locale` | `Quote!`. What the booking would cost: `lines`, `subtotal`, `discount`, `total`, the payment `schedule`, `dueNow` and `seatsLeft`, the same as the REST quote. Nothing is saved. From a browser, only your own and allowed websites may call it. |

## Types

`JSON` holds data that keeps the booking API's shape, such as a trip's `itinerary`, `packages` and `addOns`, prices by currency and a country's `facts`. It cannot be selected into. See the [booking API](https://docs.bifrostorbit.com/travel/developers/booking-api/) for what is inside.

| Type | Fields |
| --- | --- |
| `Organisation` | `slug`, `brandName`, `logoUrl`, `colour`, `contactEmail`, `payments { online publishableKey }`, `organiser { legalName registration address country }`, `guarantee { provider text }`, `termsUrl`, `locales`, `defaultLocale`, `locale`, `fallback` |
| `Trip` | `id`, `slug`, `name`, `draft`, `category`, `tags`, `activities`, `countries`, `areas`, `area`, `summary`, `badge`, `days`, `difficulty`, `groupSize { min max }`, `images`, `seo`, `currency`, `priceFrom`, `currencies`, `pricesFrom`, `rating { average count }`, `bookingMode`, `nextDeparture`, `departures`, `cmsEntryId`, `locale`, `fallback` |
| `TripDetail` | Everything in `Trip`, and `description`, `highlights`, `included`, `notIncluded`, `itinerary`, `packages`, `addOns`, `paymentPlan`, `paymentPlanByCurrency`, `cancellation`, `questions`, `travellerFields`, `travellers`, `guides`, `stays`, `terms`, `brochureUrl`, `invitationFor` |
| `Departure` | `id`, `startDate`, `endDate`, `flexible`, `guaranteed`, `status`, `seatsLeft`, `capacity`, `prices`, `priceFrom`, `pricesByCurrency`, `priceFromByCurrency`, `bookingDeadline` |
| `Image` | `id`, `url`, `mimeType`, `width`, `height`, `alt`, `title`, `focalPoint` |
| `Guide` | `slug`, `name`, `role`, `bio`, `image`, `languages`, `profile`, `tripCount`, `seo`, `locale`, `fallback`. `GuideDetail` adds `trips` and `pick`. |
| `Continent` | `key`, `name`, `published`, `tagline`, `cover`, `countries`, `content`, `locale`, `fallback` |
| `Country` | `code`, `slug`, `name`, `flag`, `continent { key name }`, `facts`, `labels`, `content`, `trips`, `live`, `locale`, `fallback` |

Amounts are whole minor units, as in REST: `124000` with `EUR` is €1,240.00. `seatsLeft` and `capacity` are `null` for no limit.

## Examples

### A trip page

```graphql
query TripPage($slug: String!, $locale: String) {
  organisation(locale: $locale) {
    brandName
    payments {
      online
      publishableKey
    }
  }
  trip(slug: $slug, locale: $locale) {
    name
    description
    images {
      url
      alt
      width
      height
    }
    currency
    priceFrom
    departures {
      id
      startDate
      endDate
      status
      seatsLeft
      priceFrom
    }
    itinerary
    packages
    guides {
      name
      role
      image {
        url
      }
    }
  }
}
```

```json
{ "slug": "lofoten-ridge-walk", "locale": "nb" }
```

### Trips in a country with the destinations menu

```graphql
{
  trips(country: "NO") {
    slug
    name
    nextDeparture
    pricesFrom
  }
  destinations {
    key
    name
    countries {
      slug
      name
      tripCount
    }
  }
}
```

## Errors

Errors come in `errors`, each with a `message`, a `code` under `extensions`, and the `path` and `locations` of the field that failed when there is one. The codes are the same as in REST:

```json
{
  "data": { "trip": null },
  "errors": [
    {
      "message": "Trip was not found",
      "locations": [{ "line": 1, "column": 3 }],
      "path": ["trip"],
      "extensions": { "code": "not_found" }
    }
  ]
}
```

| Status | When |
| --- | --- |
| `400` | No `query`, `variables` that are not a JSON object, a query that does not parse or does not match the schema (`bad_request`), or one over the limits (`query_too_deep`, `query_too_complex`) |
| `401` | Neither a key nor `?organisation=`, or a key that is wrong, revoked, expired or for another organisation (`unauthorized`) |
| `403` | A key that reads drafts, sent from a browser on another website, or `quote` from a website that isn't allowed (`forbidden`) |
| `404` | No organisation with that booking name, or no published trip, guide or destination with that address (`not_found`) |
| `413` | A query longer than 20,000 characters (`query_too_large`) |
| `422` | A `quote` with details that don't add up, such as no travellers (`invalid`) |
| `429` | Too many requests in a short time (`rate_limited`). Wait a minute. |
| `500` | Something failed on our side (`server_error`). Try again. |
| `200` | Success |

- When `trip`, `guide`, `continent` or `country` fails, it is `null` and the other fields still answer.
- `organisation`, `trips`, `guides` and `destinations` never answer `null`, so when one of them fails the whole `data` is `null`.
- When several fields fail, the HTTP status comes from the first error.

## Limits

- A query may be at most 20,000 characters long, nest at most 10 levels and ask for at most 300 fields. Each alias and each use of a fragment counts.
- Introspection is on and needs no key, so GraphQL tools can read the schema from the address. Introspection fields do not count towards the limits.
- Each calling address may make 120 requests a minute per organisation, and 30 quotes a minute. With `?organisation=`, the 120 are shared with the booking API's reads; with a key, they are counted per key. Over that, the answer is `429`.
- Persisted queries are not supported. Send the full query each time.

## Caching

Every GraphQL answer has `Cache-Control: no-store` and `Vary: Origin, Authorization`, so nothing between your website and the API keeps it. Cache in your website instead, for as long as prices and places may be out of date. Travel Orbit doesn't send webhooks to your website (see [Events](https://docs.bifrostorbit.com/travel/developers/webhooks-and-events/)). For pages a CDN should cache, the REST endpoints carry cache headers.
