GraphQL API

Endpoint, authentication, generated CRUD, and every custom Tour Guide query and mutation.

GraphQL is served at /api/graphql. The checked-in schema.graphql in the repo is the reference for a revision. The custom operations return a JSON scalar, so read their shapes from the source in features/keystone/mutations/tour-*.ts.

Authentication

Staff sessions use the keystonejs-session cookie. Sign in with authenticateUserWithPassword, sign out with endSession. MCP clients can also use OAuth bearer tokens. Operator operations check the caller's Role flags. See Roles and permissions.

Guests have no session. The guest operations use proof:

  • Hold token. createDepartureHold takes a token of 32 to 256 characters that the caller makes up. Only its SHA-256 hash is stored. checkoutHold, startBookingCheckout, and completeBookingCheckout need the same token.
  • Booking code and email. lookupBooking and cancelMyBooking need both. A wrong pair returns "Booking not found".
  • Link tokens. Waiver and rebook tokens are derived on the server. waiverForToken, signWaiver, and rebookWithCredit take one.

Queries

QueryArgumentsAccessPurpose
toursOnSalenonePublicActive tours with an upcoming on-sale departure, with ticket types, add-ons, meeting point, and policy
tourAvailabilitytourId, from, toPublicBookable departures with seats left. At most 92 days
checkoutHoldholdId, tokenHold tokenThe held quote, expiry, and booking code if any
lookupBookingbookingRef, emailCode and emailBooking, lines, guests, meeting point, waiver links, refund if cancelled now
waiverForTokentokenLink tokenWaiver text for one participant
operatorNamenonePublicOperator name from settings
tourTotalnoneSigned in (returns 0 without a session)Number of tours, used by the setup card
departuresInRangefrom, tocanCheckInDepartures with booked and held seats, guides, waiver gaps, staffing. At most 31 days
departureManifestdepartureIdcanCheckInBookings and participants for a departure
redirectToInitnonePublicTrue while there are no users

Mutations

Every mutation that changes data takes an idempotencyKey of up to 200 characters. Reusing a key with the same input returns the first result. Reusing it with different input fails.

MutationAccessWhat it does
createDepartureHold(input, idempotencyKey)PublicHolds seats for 10 minutes and returns a quote. Optional promo code
startBookingCheckout(input, idempotencyKey)Hold tokenCreates a pending_payment booking and a Stripe payment intent. Returns the client secret
completeBookingCheckout(input, idempotencyKey)Hold tokenConfirms the payment with Stripe and the seats
cancelMyBooking(bookingRef, email, idempotencyKey)Code and emailCancels by policy and refunds
signWaiver(input, idempotencyKey)Link tokenRecords a waiver signature
rebookWithCredit(input, idempotencyKey)Link tokenBooks a departure of the same tour with a credit
createChannelBooking(input, idempotencyKey)canManageBookingsPhone, walk-up, or reseller booking
generateDepartures(input, idempotencyKey)canManageDeparturesCreates departures from schedules
sweepDepartures(idempotencyKey)canManageDeparturesFlags short departures, closes past the cutoff
resizeDeparture(input, idempotencyKey)canManageDeparturesChanges capacity
cancelDeparture(input, idempotencyKey)canManageDeparturesCancels and refunds, or offers credit
closeDeparture(input, idempotencyKey)canManageDeparturesCompletes an ended departure and drafts guide payouts
assignGuide(input, idempotencyKey)canManageGuidesOffers a departure to a guide
respondToAssignment(input, idempotencyKey)Guide or canManageGuidesAccepts or declines
assignResource(input, idempotencyKey)canManageResourcesAssigns a vehicle or pool
checkInParticipant(input, idempotencyKey)canCheckInChecks one guest in
markNoShow(input, idempotencyKey)canCheckInMarks a booking no-show
generateChannelStatement(input, idempotencyKey)canManageChannelsBuilds a reseller statement
requestRefundApproval(bookingId, reason, amount)canIssueRefundsOpens a refund approval
approveRefund(approvalId)canIssueRefundsApproves it, as a different user
refundBooking(input, idempotencyKey)canIssueRefundsRefunds. Returns the booking code and refund id
ensureTourOnboardingRecord(entity, data)canManageOnboardingSetup helper, allow-listed entities only
seedTourActivity(template, seed)canManageOnboardingSetup helper for departures and bookings
updateActiveUser(data)Signed inUpdates the user's name or onboarding status only

Input shapes

input DepartureHoldInput { departureId: ID!, tickets: [TourTicketChoice!]!, addOns: [TourTicketChoice!], promoCode: String, token: String! }
input StartBookingCheckoutInput { holdId: ID!, token: String!, lead: BookingLeadInput!, participants: [BookingParticipantInput!]! }
input RefundBookingInput { bookingId: ID!, amountCents: Int!, reason: String!, approvalId: ID, cancelBooking: Boolean }
input CancelDepartureInput { departureId: ID!, reason: String!, offerCredit: Boolean }

requestRefundApproval and approveRefund take no idempotencyKey.

Example: hold seats

mutation Hold($input: DepartureHoldInput!) {
  createDepartureHold(input: $input, idempotencyKey: "hold-123")
}
{
  "input": {
    "departureId": "…",
    "tickets": [{ "id": "…", "qty": 2 }],
    "token": "a-random-string-of-at-least-32-characters"
  }
}

Capacity, in short

Seats left for a departure are its capacity minus booked seats, minus seats in unexpired holds, minus unsold channel allocation. Every operation that changes seats takes a transaction lock on the departure and counts again before it writes.

Generated CRUD

Lists keep generated queries. Generated create and update are blocked for command-only lists: Booking, BookingLine, InventoryHold, Departure, Participant, WaiverSignature, GuideAssignment, ResourceAssignment, GuidePayout, ChannelStatement, OperationalTask, PromoRedemption, and AuditEvent. No list allows generated delete. Status select fields can be set at creation but not changed by update.

Webhook and assistant

  • POST /api/webhooks/payment/[provider] for Stripe events.
  • /api/completion is the dashboard assistant. /api/mcp-transport/[transport] is its MCP endpoint. It has generic tools (listModels, queryData, searchModels, getFieldsForType, lookupInputType, createData, updateData, deleteData, modelSpecificSearch) and runs as the caller, so list access still applies. It adds no tour-specific tools. A request without a session or token gets 401.

On this page