Skip to main content
Create a booking when a customer books a court in your app, update it when they move it, and cancel it when they cancel on your side. 1Club returns a bookingId you use to update, 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) - the price the customer paid, before any fee you kept; defaults to the computed price for the slot (max 1,000,000)
  • payment.reference (optional) - your own id for the payment you collected, up to 128 characters. Requires payment.status: "paid".
  • payment.fee (optional) - what you kept out of amount, your commission. It does not reduce the sale; the gym reconciles it against your payout statement. Requires payment.status: "paid" and an explicit payment.amount it cannot exceed.
  • channel (optional) - a label stored for attribution, up to 120 characters
  • notes (optional) - up to 2,000 characters
  • notifyCustomer (optional) - tell the customer their booking is confirmed: email plus the in-app, WhatsApp, and SMS notification, and the payment receipt for a paid booking. Defaults to false for an API key, since your system normally sends its own confirmation. The gym’s own staff alerts are sent either way.
Send exactly one of contactId or customer. Sending both, or neither, is a 400. payment.reference and payment.fee cannot accompany an amount of 0, which records no payment. Response (201 Created):
  • bookingUuid - a stable, unguessable identifier. Prefer it over bookingId when you store a reference.
  • maxPlayers - how many players the booking takes, the organizer included, when it differs from the area type’s default (for example 2 on a court booked for singles). null means the area type’s own limit.
  • payment.paidAmount - what has been collected, before any refund. payment.refundedAmount - what has been returned. paidAmount - refundedAmount is what the gym kept.
  • payment.reference and payment.fee - what you reported on create, or null.
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 be active in your organization. Their bookability setting does not block an API booking, because your key acts for the gym, but 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. When the instructor has priceIncludesArea: true, their hourly rate is an all-in price and the area price is not added on top. Send payment.amount explicitly whenever your side is the authority on what the customer paid. payment.amount is stated the way the gym states its prices: where the gym’s tax rate is exclusive, tax is added on top, exactly as for a price entered in the admin.

How payment is recorded

  • paid - 1Club records the booking’s charge as paid (a Charge in the 1Club admin, a transaction in the Platform API), settled against a payment method named after your API key (manual channel, no card charged). Each key gets its own method, so the gym can tell your revenue apart from every other partner’s. Counts as revenue and paid.
  • unpaid - 1Club records the booking’s charge as outstanding. Counts as accrued revenue with nothing paid.
The payment.status you get back can also be no_charge, which means there is nothing to collect - a free area, an explicit amount of 0, or a membership or external program the customer holds that covers the booking. There is no charge 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, a block on slots that have already ended, and the coach’s working hours. 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, 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.
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 booking’s charge (paid → settled, unpaid → outstanding) and return 201 with the booking.
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 carry the counterpart’s id and name (the booking and its customer’s name, or the class or event and its name). They are null unless your key can read that counterpart through this API anyway: bookings:read for a booking, classes:read for a class or an event, and areas:read for a block. A bookings:write-only key is told that the hour is taken, and not by whom. 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 was not found in your organization, the instructor was not found or is inactive, the contact is archived, you sent both contactId and customer (or neither), or payment.reference or payment.fee was sent without a collected payment.
  • 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.

Update a booking

Requires the bookings:write scope. Reschedules a booking, moves it to another area, attaches or removes a coach, reassigns the customer, or changes its notes or price. Send only the fields that change.
Body fields, all optional, at least one required:
  • startTime, endTime - ISO date-times with offset. The end must be after the start, whether stored or sent.
  • areaId - move the booking to this area. It must be an area this API can sell, as on create.
  • instructorId - attach this coach, or null to take the coach off. The coach must be free at the time.
  • contactId - reassign the booking to this contact. An outstanding charge moves with it.
  • notes - up to 2,000 characters
  • amount - the booking’s new price, before tax, in the gym’s currency (max 1,000,000). Omit it to keep the price you recorded.
  • notifyCustomer - tell the customer their booking changed. Sent only when the edit names a time, area, or instructor. Defaults to false for an API key. A body with nothing but notifyCustomer is refused.
Returns the booking after the edit, in the same shape as create. As on create, the gym’s booking rules are not applied, but an occupied or blocked court, or a coach who is already booked, is refused with 400 and details.conflicts - see Conflicts.
  • A cancelled booking cannot be edited (BOOKING_CANCELLED), and a booking on a class takes its time and price from the class (BOOKING_ON_CLASS_NOT_RESCHEDULABLE).
  • Payment status cannot be changed here. A new amount on a paid booking leaves any increase outstanding. A price below what was already paid is refused with 409 (BOOKING_CHARGE_OVERPAID), because it needs a refund rather than an edit.
  • A booking an API key created cannot be made to recur (RECURRENCE_NOT_AVAILABLE_TO_MANAGING_KEY), so each session you sell stays its own booking.

Cancel a booking

Requires the bookings:write scope. Cancels the booking by its 1Club id. 1Club moves no money: you collected the payment, so you return it.
The body is optional. Fields:
  • reason (optional) - why the booking is cancelled, up to 500 characters. Shown to the gym’s staff on the cancellation. Defaults to a note naming the API.
  • refund.amount (optional) - what you returned to the customer. 0 records no refund, for example when you kept the whole payment as a cancellation fee. It cannot exceed what was collected (400 with code: "AMOUNT_EXCEEDS_REFUNDABLE").
  • refund.reference (optional) - your own id for the refund, up to 128 characters
  • notifyCustomer (optional) - tell the customer the booking was cancelled. Defaults to false for a booking this API created.
Response:
Cancelling with an API key records a refund of everything collected unless refund.amount says otherwise, so the gym’s revenue matches yours. Send the amount you actually returned to keep a cancellation fee, or 0 if you kept it all. refundedAmount is what this call recorded, and 0 when nothing was. An unpaid booking records nothing. Cancelling is idempotent: cancelling an already-cancelled booking succeeds with status: "cancelled". A repeat records a refund only when it carries an explicit refund, so a retry can record a refund that failed the first time without ever recording one twice. A booking that has already been checked in cannot be cancelled (409, BOOKING_ALREADY_CHECKED_IN). One occurrence of a recurring booking is cancelled, not the series.

Look up a booking

Requires the bookings:read scope. Returns the current state of a booking, including its payment status.
Returns the same object as one row of List bookings. A 404 means no booking with that id exists in your organization. Pass include to embed more, as on List bookings.
Reads reach any booking in your organization, whichever channel created it. Writes do not: update, cancel, and adding or removing players reach only the bookings an API key created (source: "api"), and any other booking is reported as not found. That is deliberate, so a key cannot change or cancel a booking a member made in the gym’s own app. A booking another API key manages is refused with 409 and code: "BOOKING_EXTERNALLY_MANAGED", naming that integration in details.managedBy.

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, or a bare YYYY-MM-DD read as midnight UTC, bounding the booking start time. from is inclusive and to exclusive. 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
  • classId, eventId (optional) - only the attendees of one class occurrence, or the bookings for one event
  • status (optional) - for example confirmed, pending, cancelled
  • include (optional) - comma-separated expansions: customer, money, cancellation. See Embedding the customer.
  • limit (optional) - 1 to 100, defaults to 50
  • offset (optional) - defaults to 0
Response:
This returns bookings from every channel, each tagged with source (api for yours, plus admin, portal, app, kiosk, network, website, and integration). 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.

Money and cancellation

Two more expansions sit alongside customer. Combine them with commas, for example include=customer,money,cancellation.
  • money - what the booking billed, what was paid, and what is still outstanding, with the charges, cancellation credits, payments, and refunds behind those figures. Requires transactions:read.
  • cancellation - the cancellation terms stamped on the booking when it was accepted, with the deadlines they give and until when it cancels for free, and, once cancelled, who cancelled it, when, why, with how much notice, and at what refund. Needs no scope beyond bookings:read.
An expansion whose scope your key lacks is refused with 403 rather than left out. 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. Works on any booking in your organization. Adding and removing players reaches only the bookings an API key created, as with cancel.

Add a player

Requires the bookings:write scope. The contact must already exist - create it through Contacts first.
Returns 201 with { "bookingId": 48213, "contactId": 301 }.

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.