Skip to main content
Create a booking when a customer books a court in your app, and cancel it when they cancel on your side. 1Club returns a bookingId you use to cancel or look the booking up later.

Create a booking

Requires the bookings:write scope.
Body fields:
  • areaId (required) - the area id from List areas
  • startTime, endTime (required) - ISO date-times, endTime after startTime
  • contactId - the customer, resolved through Contacts
  • customer - an inline customer matched to a contact by email, instead of contactId
  • instructorId (optional) - the coach leading the session, from Instructors
  • payment.status (required) - paid (you collected the money) or unpaid (outstanding)
  • payment.amount (optional) - amount collected; defaults to the computed price for the slot (max 1,000,000)
  • channel (optional) - a label stored for attribution
  • notes (optional)
  • notifyCustomer (optional) - send the customer a 1Club confirmation email; defaults to false
Send exactly one of contactId or customer. Sending both, or neither, is a 400. Response (201 Created):
Creating a booking accepts an 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

Pass instructorId 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.
The 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.
Physical constraints always apply and are never overridden: a genuine double-booking, the area’s concurrency capacity, a court the gym has blocked for maintenance or a private hire, a live event on the court, the coach’s own calendar, whether an instructor takes one-on-one bookings, and the slot alignment the area requires. A booking that breaks one of these is rejected - see Conflicts. There is no override parameter.

How contacts sync

Every booking belongs to a contact in the gym’s 1Club CRM. When you send an inline customer 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 from customer.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.com and alex@example.com are 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 customer fields (firstName, lastName, phone; name is 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.
For an existing contact, 1Club matches by email and does not overwrite the stored name or phone with what you send. Treat contact profile fields as create-time only - sending a new phone on a later booking for the same email will not change the contact. (To correct contact details, edit the contact in 1Club.)

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 with 400, 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.
Detect a taken slot on the presence of a populated details.conflicts, not on a list of codes. The list grows as new kinds of occupancy are modelled, and a code you do not recognise would otherwise read as a generic error - which at checkout means confirming a booking the gym rejected.
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 both contactId and customer (or neither).
  • 409 - an Idempotency-Key was reused with a different request body. Scheduling conflicts are 400.
  • 403 - the API key is missing the bookings:write scope.
  • 429 - rate limit exceeded (booking writes are throttled in addition to the standard per-org limit).
Opening hours, minimum notice, and how far ahead you may book are not enforced for this API - see Booking policy is not applied to this API.

Cancel a booking

Requires the bookings:write scope. Cancels the booking by its 1Club id. No refund is issued by 1Club.
Response:
Cancelling is idempotent: cancelling an already-cancelled booking succeeds and returns the same response.

Look up a booking

Requires the bookings:read scope. Returns the current state of a booking, including its payment status.
A 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 the bookings: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.
Query parameters:
  • 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 customer
  • status (optional) - for example confirmed, pending, cancelled
  • include (optional) - customer embeds each booking’s customer, see Embedding the customer
  • limit (optional) - 1 to 100, defaults to 50
  • offset (optional) - defaults to 0
Response:
Unlike look-up by id, this returns bookings from every channel, each tagged with 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 bare contactId. 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.
Each row in data then carries a customer alongside its contactId:
The object is exactly what 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.
If your key deliberately holds no contacts scope, attendee names are still available for class bookings: 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 as contactId; the others are players on it.

List players

Requires the bookings:read scope.

Add a player

Requires the bookings:write scope. The contact must already exist - create it through Contacts first.

Remove a player

Requires the bookings: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 returns 400. 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.