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.
https://app.bifrostorbit.com/travel/api/cdn/graphqlGraphQL 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>orx-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:
queryvariablesoperationName| 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 |
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:
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:
{ "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.
organisationlocaleOrganisation!. Brand, organiser, travel guarantee, languages and whether online payment works right now.tripslocale, activity, country, category, ids: [String!][Trip!]!. Published, public trips with their departures. ids asks for up to 100 chosen trips, unlisted ones too.tripslug: String!, localeTripDetail. One trip with its itinerary, packages, extras, payment plan, questions, guides and places to stay.guideslocale[Guide!]!. Your guides who lead trips or have a bio.guideslug: String!, localeGuideDetail. One guide with the trips they lead and their pick.destinationslocale[Continent!]!. Continents and countries your public trips visit, and countries you have published without trips.continentkey: String!, localeContinent. A continent, such as europe, with its countries and your text once published.countrycontinent: String!, country: String!, localeCountry. Facts, your text, trips, local time, weather and the exchange rate. country is a slug or two-letter code.quoteinput: QuoteInput! with departureId, travellers (each with an optional packageId), addOns (id, quantity), discountCode, currency, localeQuote!. 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.| 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 for what is inside.
Organisationslug, brandName, logoUrl, colour, contactEmail, payments { online publishableKey }, organiser { legalName registration address country }, guarantee { provider text }, termsUrl, locales, defaultLocale, locale, fallbackTripid, 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, fallbackTripDetailTrip, and description, highlights, included, notIncluded, itinerary, packages, addOns, paymentPlan, paymentPlanByCurrency, cancellation, questions, travellerFields, travellers, guides, stays, terms, brochureUrl, invitationForDepartureid, startDate, endDate, flexible, guaranteed, status, seatsLeft, capacity, prices, priceFrom, pricesByCurrency, priceFromByCurrency, bookingDeadlineImageid, url, mimeType, width, height, alt, title, focalPointGuideslug, name, role, bio, image, languages, profile, tripCount, seo, locale, fallback. GuideDetail adds trips and pick.Continentkey, name, published, tagline, cover, countries, content, locale, fallbackCountrycode, slug, name, flag, continent { key name }, facts, labels, content, trips, live, locale, fallback| 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
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 } } }}{ "slug": "lofoten-ridge-walk", "locale": "nb" }Trips in a country with the destinations menu
{ 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:
{ "data": { "trip": null }, "errors": [ { "message": "Trip was not found", "locations": [{ "line": 1, "column": 3 }], "path": ["trip"], "extensions": { "code": "not_found" } } ]}400query, 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?organisation=, or a key that is wrong, revoked, expired or for another organisation (unauthorized)403quote from a website that isn't allowed (forbidden)404not_found)413query_too_large)422quote with details that don't add up, such as no travellers (invalid)429rate_limited). Wait a minute.500server_error). Try again.200| 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,continentorcountryfails, it isnulland the other fields still answer. organisation,trips,guidesanddestinationsnever answernull, so when one of them fails the wholedataisnull.- 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 is429. - 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?