bookingId you use to cancel or look the booking up later.
Create a booking
Requires thebookings:write scope.
areaId(required) - the area id from List areasstartTime,endTime(required) - ISO date-times,endTimeafterstartTimecontactId- the customer, resolved through Contactscustomer- an inline customer matched to a contact by email, instead ofcontactIdinstructorId(optional) - the coach leading the session, from Instructorspayment.status(required) -paid(you collected the money) orunpaid(outstanding)payment.amount(optional) - amount collected; defaults to the computed price for the slot (max 1,000,000)channel(optional) - a label stored for attributionnotes(optional)notifyCustomer(optional) - send the customer a 1Club confirmation email; defaults tofalse
contactId or customer. Sending both, or neither, is a 400.
Response (201 Created):
Idempotency-Key, so a timeout cannot become two bookings. See Idempotency.
Identifying the customer
contactId is the recommended path: resolve the customer with Search contacts, create them if new, then book against the id. It is explicit about which person the booking is for, and it works for customers you only know by name.
The inline customer object remains supported for integrations that never hold 1Club ids. It is matched to a contact by email, and a contact is created when there is no match - see How contacts sync.
Adding a coach
PassinstructorId when the session is coached rather than a bare court hire. The coach must take one-on-one bookings (isBookable), and their own calendar is respected, so a booking that would double-book them is rejected. Attaching a coach also feeds the gym’s instructor pay records automatically.
How the amount is calculated
payment.amount is optional. When you omit it, 1Club prices the slot itself: the area price per hour plus the instructor hourly rate, times the duration. A one hour court at 24 with a coach at 30 is priced at 54. Without an instructor it is just the area price. Send payment.amount explicitly whenever your side is the authority on what the customer paid.
How payment is recorded
paid- 1Club records a paid booking transaction, settled against a dedicated External / API payment method (manual channel, no card charged). Counts as revenue and paid.unpaid- 1Club records the transaction as outstanding. Counts as accrued revenue with nothing paid.
payment.status you get back can also be no_charge, which means nothing was ever owed - a free area, or an explicit amount of 0. There is no transaction to settle in that case, so it is reported as no_charge rather than as whichever of paid or unpaid you sent.
You keep ownership of collection and refunds. 1Club never charges a card for these bookings.
Booking policy is not applied to this API
1Club has member-facing booking rules a gym can configure: a minimum notice period, a maximum number of days in advance, an opening-hours envelope, and a block on slots that have already ended. None of them are enforced for this API, because the system calling it runs its own booking rules. That is what lets you record a schedule as it stands, including sessions in the past when you are migrating an existing calendar into 1Club.How contacts sync
Every booking belongs to a contact in the gym’s 1Club CRM. When you send an inlinecustomer object instead of a contactId, that customer is resolved to a
contact automatically, as a by-product of booking sync. Sync is one-way:
partner to 1Club. 1Club never pushes contact changes back to you.
This section describes the inline
customer path. To resolve a customer yourself
before booking - by name, email, or phone - use
Contacts and pass the resulting
contactId instead.Matching and creation
1Club resolves the contact fromcustomer.email:
- Email is the identity key. It’s normalized (trimmed and lower-cased) and matched within the club’s organization. Matching is case-insensitive -
Alex@Example.comandalex@example.comare the same contact. - Scoped per organization. Contacts belong to one organization. The same email booking at two different clubs (organizations) is two separate contacts - there is no global customer identity across clubs.
- Match wins over create. If a contact with that email already exists, the booking links to it. If not, a new contact is created from the
customerfields (firstName,lastName,phone;nameis derived from first + last, falling back to the email when no name is given). - The booking stores the resolved
contactId. From then on the booking, its revenue, and its history all roll up to that one contact in the CRM.
Guarantees
- No duplicates. A unique constraint on
(organization, lower(email))plus a create-race retry means two bookings racing with the same new email still produce exactly one contact. - Consistent identity. Because matching is by normalized email, repeat customers always land on the same contact - their bookings accumulate rather than fragmenting.
- Searchable immediately. A newly created contact is available in the CRM (searchable by name and email) as soon as the booking succeeds.
The full create flow
1
Validate the area
The
areaId must be bookable through this API (active, public, bookable type).
If not → 400.2
Resolve the contact
Normalize
customer.email (trim + lower-case), then match an existing
contact in the organization or create one from the customer fields. An
existing contact is reused as-is (name/phone not overwritten). When you send a
contactId instead, it is checked against your organization and must not be
archived.3
Validate the instructor
When
instructorId is present, the coach must be active in your organization
and must take one-on-one bookings.4
Create the booking
Link the resolved
contactId and write the booking, re-checking availability
atomically. If the slot was taken in the meantime → 400 with a populated
details.conflicts - see Conflicts.5
Record revenue & respond
Record the transaction (
paid → settled, unpaid → outstanding) and return
201 with { bookingId, status, payment }.Contact resolution runs before the booking write. So if the slot turns out to
be taken (
400) or the request is otherwise rejected, a contact just created for a
first-time customer is kept - it isn’t rolled back. That’s harmless: it carries
no booking, stays matched by email, and is reused on retry. Only the booking write
re-checks availability atomically and rejects conflicts.Conflicts
A slot that cannot be sold is refused with400, not 409. The body carries a
machine-readable code and a details.conflicts array naming what is in the way:
subject says what holds the slot, in the same vocabulary the
availability response
uses for busy[].subject. A court conflict is member_booking, class, event,
or area_block; a coach conflict adds time_off and shift_break. The counterpart’s own startTime/endTime may be wider
than the slot you asked for.
id and label are always null on this API. They carry the counterpart’s row id
and the member’s name for the gym’s own staff calendar, where someone is entitled to
see who they would be displacing - an integration is not, so it is told that the hour
is taken and no more.
The codes you can see today are AREA_AT_CAPACITY, AREA_BLOCKED,
AREA_RUNNING_EVENT, INSTRUCTOR_AT_CAPACITY, INSTRUCTOR_TEACHING_CLASS, and
INSTRUCTOR_RUNNING_EVENT.
Retrying a conflict does not help, and there is no parameter that forces one
through. Re-read availability for the window and offer another slot.
Common errors
400- the slot cannot be sold (see Conflicts), the area is not bookable through this API, the contact or instructor was not found in your organization, the contact is archived, the instructor does not take one-on-one bookings, or you sent bothcontactIdandcustomer(or neither).409- anIdempotency-Keywas reused with a different request body. Scheduling conflicts are400.403- the API key is missing thebookings:writescope.429- rate limit exceeded (booking writes are throttled in addition to the standard per-org limit).
Cancel a booking
Requires thebookings:write scope. Cancels the booking by its 1Club id. No refund is issued by 1Club.
Look up a booking
Requires thebookings:read scope. Returns the current state of a booking, including its payment status.
404 means no booking with that id exists in your organization. Pass
include=customer to embed the contact, as on
List bookings.
Look-up and cancel only reach bookings this API created. That is deliberate:
cancelling issues no refund, so a key must not be able to cancel a booking a
member made in the gym’s own app. To see bookings from every channel, use
List bookings.
List bookings
Requires thebookings:read scope. Returns bookings whose start time falls in a window, so you can verify what you created and reconcile against what else is on the calendar.
from,to(optional) - ISO date-times bounding the booking start time. The window may span at most 93 days. Omit both for the next 7 days; pass one and the other is derived from it.clubId,areaId,instructorId,contactId(optional) - narrow to one gym, court, coach, or customerstatus(optional) - for exampleconfirmed,pending,cancelledinclude(optional) -customerembeds each booking’s customer, see Embedding the customerlimit(optional) - 1 to 100, defaults to 50offset(optional) - defaults to 0
source (api for yours, plus admin, portal, app, and others). That is what makes reconciliation possible: a caller that cannot see member and staff bookings cannot tell that a slot it thinks is free was taken in the gym’s own tools.
Embedding the customer
A booking carries the customer as a barecontactId. Pass include=customer on
either read to get the whole contact inline, so importing a window of bookings
costs one request instead of a contact look-up
per row - which matters against the 100 requests per minute budget.
data then carries a customer alongside its contactId:
GET /contacts/{id}
returns, so nothing is lost by skipping that call. customer is null when the
booking has no contact.
Requires the
contacts:read scope in addition to bookings:read. A key
without it gets a 403 rather than a page with the field quietly missing -
which would read as “these bookings have no customer”. phone can be null:
1Club requires an email or a phone on a contact, not both.GET /v1/platform/classes/{id}/bookings returns
contactName under bookings:read alone, and gates only the email on
contacts:read. A name identifies who is on a roster; the whole contact record is
CRM data.
Players on a booking
A doubles court is booked by one person but played by several. The customer the booking is for stays on the booking itself ascontactId; the others are players on it.
List players
Requires thebookings:read scope.
Add a player
Requires thebookings:write scope. The contact must already exist - create it through Contacts first.
Remove a player
Requires thebookings:write scope. Returns 204 and does not affect the person the booking is for.
Limits
The total, counting the person the booking is for, is capped by the area type’s maximum players. Adding beyond it returns400. On an area that allows only one player, no additional players can be added at all.
Adding someone who is already the booking’s customer, or already a player, returns 409. Archived contacts cannot be added.