Everything in the local database is queryable at /graphql. Open
$API/graphql in a browser for GraphiQL, which lists every
field with its type and autocompletes as you type — the fastest way to find out
what exists.
The page loads without a key and arrives with the key already filled into its
header editor, so you can run a query immediately. Everything else, including
queries sent from your own code, needs x-api-key:
curl -H "x-api-key: $KEY" -H 'content-type: application/json' \
-d '{"query":"{ bookingsCount csatCount membersCount }"}' \
$API/graphqlThe schema is generated from the database tables when the server starts, so when the organiser adds a dataset it appears here without a new release.
The datasets
| Query | What | Notable fields |
|---|---|---|
bookings |
Holiday bookings, one row per reservation | reservation_id, member_id, hotel_code, selling_price, margin, start_date, duration, pax, departure_airport_code, board_code, agent_method |
csat |
Post-holiday survey responses | osat, nps, value_for_money, hotel_rating, comment, hotel_code, hotel_name, link_type |
members |
One row per customer, aggregated | member_id, booking_count, survey_count, total_selling_price, first_booking_date, last_booking_date, post_area |
destinations |
189 countries, regions and resorts | code, name, type, parent_code, country_code |
destination_airports |
170 links between a destination and an airport | destination_code, airport_code |
hotels |
Content for the 93 hotels that appear in bookings | hotel_code, name, star_rating, rating, tripadvisor_id, latitude, longitude, resort_name, description |
hotel_images |
2,685 hotel photos | hotel_code, position, small, medium, large |
hotel_facility_items |
2,013 facilities, one row per facility | hotel_code, group_name, name, facility_code, distance |
Bookings carry around 180 fields covering flights, accommodation, bags, transfers, margins and promotions. Browse them in GraphiQL rather than guessing.
Every query takes the same four arguments
{
bookings(
where: { departure_airport_code: "MAN", selling_price_gte: 1000 }
orderBy: "selling_price desc"
limit: 20
offset: 0
) {
reservation_id
hotel_code
selling_price
}
}limitdefaults to 50 and caps at 500.orderByis a field name, optionally followed byascordesc.- Each query has a matching
Countfield —bookingsCount,csatCount,membersCount— taking the samewhereand ignoring pagination.
Filter operators
Every field supports these suffixes:
| Suffix | Meaning | Example |
|---|---|---|
| none | equals | board_code: "AI" |
_in |
one of | departure_airport_code_in: ["MAN", "LGW"] |
_gte |
at least | selling_price_gte: 1000 |
_lte |
at most | osat_lte: 3 |
_contains |
substring, text fields only | comment_contains: "transfer" |
Combine them freely; conditions are ANDed. Dates are stored as YYYY-MM-DD text
and compare correctly, so start_date_gte and start_date_lte give you a window:
{
bookingsCount(where: { start_date_gte: "2026-11-01", start_date_lte: "2026-11-30" })
}Passing null matches rows where the field is empty — where: { member_id: null }
finds bookings made through an agent or by a customer who wasn’t logged in.
Joins
Bookings and surveys both link to a member, and a member exposes both back:
{
members(orderBy: "total_selling_price desc", limit: 5) {
member_id
booking_count
total_selling_price
bookings { reservation_id hotel_code selling_price start_date }
surveys { osat nps comment link_type }
}
}Or from a survey outward:
{
csat(where: { osat_lte: 3 }, limit: 10) {
osat
comment
hotel_name
member { member_id total_selling_price booking_count }
}
}A booking also reaches its hotel, and a hotel reaches its images, facilities and bookings. This is how you ask what the low scorers actually stayed in:
{
bookings(where: { selling_price_gte: 2000 }, limit: 10) {
reservation_id
selling_price
hotel { name star_rating rating resort_name tripadvisor_id }
}
}hotels.tripadvisor_id is on every one of the 93 hotels, so it is the way across
to /api/tripadvisor/<id> for reviews and ratings.
Destinations
The same geography the search API uses, flattened into rows. 30 countries, 133 regions, 16 resorts, plus 10 virtual groupings like “South of France”.
type is one of Country, Region, Resort, VirtualCountry, VirtualRegion.
parent_code is the destination one level up; country_code is the country it
ultimately sits in, so you can filter a whole tree in one hop:
{
spanishRegions: destinations(where: { country_code: "ES", type: "Region" }) {
code
name
}
resortCount: destinationsCount(where: { type: "Resort" })
}Walk up or down the hierarchy through parent, children and country:
{
destinations(where: { code: "GBCIJE" }) {
name # Jersey
airports { airport_code } # JER
parent { name type } # Channel Islands, Region
country { code name } # GB, United Kingdom
}
}Go the other way to find where an airport flies:
{
destination_airports(where: { airport_code: "MAN" }) {
destination { code name type country { name } }
}
}The code values are exactly what /api/search/packages wants for its
geography parameter, so this is how you populate a destination picker and then
search with it.
Read this before you trust a join
The practical consequence: on a synthetic row, the survey’s hotel and the joined
booking’s hotel are different hotels.
- For hotel-level satisfaction, use the survey’s own
hotel_codeandhotel_name. - Do not use
member { bookings { hotel_code } }to decide which hotel a survey is about.
# Correct: satisfaction by hotel, using the survey's own hotel
{
csat(where: { hotel_code: "ESTF0008" }) { osat hotel_name comment }
}Anything that joins satisfaction to booking value, margin or lead time is measuring noise until the extracts are aligned. It will start being true without any change to your query once they are.
Anonymisation, and what it means for you
Identifiers are replaced with sequential surrogates: member_id reads M0001,
reservation_id reads R0001. Consistency is preserved, so a member’s bookings
and surveys still group correctly. Free-text comments have emails, phone numbers,
URLs and booking references stripped. Postcodes are reduced to their area letters
(JE2 becomes JE).
Asking for more data
The schema comes from the database, so a new dataset needs no code — the organiser imports it and it appears in GraphiQL. If you need a field that isn’t there, or a join that isn’t wired up, ask: adding a relation is a two-line change.