Skip to content

GraphQL

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

Reference7 min read

GraphQL reads the same trips, departures, guides and destinations as the 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.

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

Requests

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

  • send an API key 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 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:

query
Type
string
Means
The GraphQL document. Required.
variables
Type
object
Means
Values for the query's variables
operationName
Type
string
Means
Which operation to run when the document has several
Shell
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:

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

organisation
Arguments
locale
Returns
Organisation!. Brand, organiser, travel guarantee, languages and whether online payment works right now.
trips
Arguments
locale, activity, country, category, ids: [String!]
Returns
[Trip!]!. Published, public trips with their departures. ids asks for up to 100 chosen trips, unlisted ones too.
trip
Arguments
slug: String!, locale
Returns
TripDetail. One trip with its itinerary, packages, extras, payment plan, questions, guides and places to stay.
guides
Arguments
locale
Returns
[Guide!]!. Your guides who lead trips or have a bio.
guide
Arguments
slug: String!, locale
Returns
GuideDetail. One guide with the trips they lead and their pick.
destinations
Arguments
locale
Returns
[Continent!]!. Continents and countries your public trips visit, and countries you have published without trips.
continent
Arguments
key: String!, locale
Returns
Continent. A continent, such as europe, with its countries and your text once published.
country
Arguments
continent: String!, country: String!, locale
Returns
Country. Facts, your text, trips, local time, weather and the exchange rate. country is a slug or two-letter code.
quote
Arguments
input: QuoteInput! with departureId, travellers (each with an optional packageId), addOns (id, quantity), discountCode, currency, locale
Returns
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 for what is inside.

Organisation
Fields
slug, brandName, logoUrl, colour, contactEmail, payments { online publishableKey }, organiser { legalName registration address country }, guarantee { provider text }, termsUrl, locales, defaultLocale, locale, fallback
Trip
Fields
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
Fields
Everything in Trip, and description, highlights, included, notIncluded, itinerary, packages, addOns, paymentPlan, paymentPlanByCurrency, cancellation, questions, travellerFields, travellers, guides, stays, terms, brochureUrl, invitationFor
Departure
Fields
id, startDate, endDate, flexible, guaranteed, status, seatsLeft, capacity, prices, priceFrom, pricesByCurrency, priceFromByCurrency, bookingDeadline
Image
Fields
id, url, mimeType, width, height, alt, title, focalPoint
Guide
Fields
slug, name, role, bio, image, languages, profile, tripCount, seo, locale, fallback. GuideDetail adds trips and pick.
Continent
Fields
key, name, published, tagline, cover, countries, content, locale, fallback
Country
Fields
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" }    }  ]}
400
When
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
When
Neither a key nor ?organisation=, or a key that is wrong, revoked, expired or for another organisation (unauthorized)
403
When
A key that reads drafts, sent from a browser on another website, or quote from a website that isn't allowed (forbidden)
404
When
No organisation with that booking name, or no published trip, guide or destination with that address (not_found)
413
When
A query longer than 20,000 characters (query_too_large)
422
When
A quote with details that don't add up, such as no travellers (invalid)
429
When
Too many requests in a short time (rate_limited). Wait a minute.
500
When
Something failed on our side (server_error). Try again.
200
When
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). For pages a CDN should cache, the REST endpoints carry cache headers.

Was this page helpful?

Read next