External reservations
Let non-members book exposed resources online with a temporary account and pay by card.
A reservation category can open some of its resources to people who are not club members.
On the public member site (/book/<clubId>), a visitor chooses a category, a resource, the
number of people (only for resources holding more than one) and a period, then either signs in
as a member or books without a membership. Booking without a
membership creates a temporary account that ends at the close of the reservation's last
day and pays the price and security deposit by card through Stripe Checkout.
Loading diagram...
Diagram source
sequenceDiagram
participant V as Visitor
participant A as Champagne API
participant S as Stripe
V->>A: externalReservationCatalog / externalReservationAvailability
V->>A: createExternalReservation
A-->>V: token + checkoutUrl (reservation booked)
V->>S: Pay price + deposit
S->>A: checkout.session.completed
A-->>A: reservation confirmed, deposit marked paid
Note over A: End of last day: temporary account deletedMembers who sign in on the same page book through createReservation
as usual: they pay with credits at their membership price, and still need an active membership.
Configuration
ReservationCategoryInput.externalBookingEnabled(defaultfalse) turns the module on. It cannot be combined withconversationRequiredorrequiresSession; the mutation returnsBAD_USER_INPUT(External booking cannot be combined with required conversations or sessions.) and a database check also refuses the combination.UpdateReservationResourceInput.externalBookableexposes one resource. Omitting it keeps the current value.UpdateReservationResourceInput.externalAmountis the non-member price in cents perpriceUnit, from 0 to 100,000,000. It is required whenexternalBookableis true (BAD_USER_INPUT,Enter the price for non-members.), because member prices are in credits. It is always written, so omitting it clears it, likedepositAmount.
ReservationCategory.externalBookingEnabled, ReservationResource.externalBookable and
ReservationResource.externalAmount return the stored values.
External bookings follow the category's
booking rules: opening hours, booking window
and minimum notice. When the category sets defaultDurationMinutes, exposed in the catalog, the end
is the start plus that length and the booking page hides the end fields. Slot booking cannot be
combined with external booking.
Clubs open to online booking
query ExternalBookingClubs {
externalBookingClubs {
id
name
}
}The query is public. It lists, by name then ID, the clubs with at least one category open to
external booking that has an externalBookable resource with an externalAmount. The member
site's Book online navigation tab uses it.
Catalog
query ExternalCatalog($clubId: ID!) {
externalReservationCatalog(clubId: $clubId) {
clubId
clubName
timezone
categories {
id
name
description
currency
defaultDurationMinutes
resources {
id
categoryId
name
amount
priceUnit
depositAmount
maxOccupants
cancellationNoticeDays
}
}
}
}The query is public. It returns null for an unknown club. Only categories with
externalBookingEnabled that have at least one externalBookable resource with an
externalAmount are listed, ordered by name then ID, and only those resources are included.
amount is the non-member price (externalAmount). depositAmount is the resource's security
deposit, set in credits, converted to euro cents at the club's creditsPerEuro rate.
Availability
query ExternalAvailability($clubId: ID!, $categoryId: ID!, $from: String!, $to: String!) {
externalReservationAvailability(clubId: $clubId, categoryId: $categoryId, from: $from, to: $to) {
busyPeriods {
resourceId
startsAt
endsAt
}
blocks {
resourceId
startsAt
endsAt
}
}
}The query is public. from and to are UTC ISO timestamps at most 62 days apart; other values
return BAD_USER_INPUT. A category that is not open to external booking returns empty lists.
Busy periods carry no customer information. A block with a null resourceId covers the whole
category.
Book without a membership
mutation CreateExternalReservation($input: CreateExternalReservationInput!) {
createExternalReservation(input: $input) {
token
personId
reservationId
checkoutUrl
error
}
}{
"input": {
"clubId": "51000000-0000-4000-8000-000000000051",
"categoryId": "73000000-0000-4000-8000-000000000001",
"resourceId": "79000000-0000-4000-8000-000000000001",
"startsAt": "2026-11-02T14:00:00.000Z",
"endsAt": "2026-11-04T10:00:00.000Z",
"firstName": "Iris",
"lastName": "Visitor",
"email": "iris@example.com",
"phoneNumber": "+33 6 12 34 56 78",
"password": "a long password",
"guests": 2
}
}| Argument | Type | Description |
|---|---|---|
clubId | ID! | Club of the category. |
categoryId | ID! | Category with externalBookingEnabled. |
resourceId | ID! | An externalBookable resource of that category. |
startsAt | String! | UTC ISO start, in the future. |
endsAt | String! | UTC ISO end, after the start and at most 31 days later. |
firstName | String! | 1-100 characters after trimming. |
lastName | String! | 1-100 characters after trimming. |
email | String! | Trimmed and compared case-insensitively. |
phoneNumber | String! | 6-30 digits, spaces, +, (, ), . or -. |
password | String! | 12-200 characters with a lowercase letter, an uppercase letter, a digit and a symbol. |
guests | Int! | Number of people including the booker, default 1. |
The mutation is public. Malformed IDs or timestamps return BAD_USER_INPUT. Business refusals
return a result with every other field null and a user-facing error, without creating anything:
- the category is not open to online booking, or the resource is not exposed;
- the period is in the past, inverted, longer than 31 days, blocked or already reserved;
guestsis above the resource'smaxOccupants(Too many people for this resource.); values outside 1-500 returnBAD_USER_INPUT;- the email already has an account in this club (
This email already has an account in this club. Sign in instead.). An expired temporary account with that email is deleted and replaced; - the club has no Stripe merchant configured for payments.
On success, one transaction creates the person, its credential, a guest_access grant of kind
external, a session and the reservation, then Champagne opens a Stripe Checkout session for
amount + depositAmount. The reservation is booked until the payment completes, or
confirmed straight away when nothing is due. token signs the visitor in as the temporary
account. checkoutUrl is the Stripe page; it is null when nothing is due, or when Stripe could
not be reached, in which case error asks the visitor to pay from their account.
The reservation price uses the resource's non-member price and price unit. The deposit is the
resource's depositAmount, set in credits and converted to euro cents at the club's
creditsPerEuro rate when the booking is made. The booking snapshots the contact details in
Reservation.externalCustomer and sets Reservation.source to external.
Card payment
createReservationCheckout(clubId, reservationId, requestId) returns the open Stripe Checkout
session of a booked reservation for the signed-in temporary account that owns it, or creates a
new one. Regular members receive an error because they pay with credits. When the last checkout is
already paid but its checkout.session.completed webhook has not been processed yet, no new session
is created: the reservation is confirmed from the paid session, as the webhook would, and the
mutation returns the error "Your payment was received. Your reservation is being confirmed.". A checkout expires after
30 minutes. When Stripe reports checkout.session.expired, the still-unpaid reservation is
canceled and its period released. checkout.session.completed confirms it and marks the deposit
as paid. Canceling an unpaid reservation with cancelReservation, updateReservation or
cancelMyReservation expires
its open checkout; the cancellation still succeeds if Stripe cannot be reached. If the reservation
was canceled before the payment completed anyway, the webhook refunds the
price and deposit in full instead of confirming it, and records the payment and refund on the
canceled reservation.
Refunds go back to the card:
cancelMyReservationby the temporary account refunds the price and deposit, or only the deposit once the cancellation notice has passed. It is refused once the reservation has started.- Staff canceling with
cancelReservationorupdateReservationrefund the price and deposit. Once the cancellation notice has passed, only the deposit is refunded unless they passrefundLateCancellation: true. - After departure, staff refunding the deposit, or the automatic refund, return all or part of it (see Departure and deposit refunds).
Temporary accounts
memberViewer.external is true for temporary accounts and guestExpiresAt is the end of their
access: the next club-local midnight after the reservation ends. PersonDetail.guestAccess.invitedBy.kind
is external, with a null inviter. Like guests, temporary accounts cannot buy memberships or
add-ons. They also cannot buy or spend credits, create or change reservations, join waitlists,
invite people or open the payment-method portal (UNAUTHENTICATED). They can read their account
and reservations, download tickets, chat, cancel and pay by card.
Every five minutes a worker deletes temporary accounts whose access has ended. Their sessions,
credential, access grant and other personal records are deleted with the person. Unpaid
reservations are canceled first. Reservations keep guestName, source and the
externalCustomer contact details as the booking record. Three years after a reservation
ends, the worker anonymises it: externalCustomer becomes null and guestName becomes
External customer.