Booking groups API
A booking group ties several bookings together into one event with one price and one payment. For example, a company team day might include mini golf at 14:00 and laser tag at 15:30. Each activity is a normal booking; the group coordinates them.
Groups are usually created from an event package. Staff can also build a custom group activity by activity.
For how staff work with groups in the admin UI, see Booking groups.
Customer endpoints
These endpoints need a signed-in customer (Authorization: Bearer <token>) and the x-tenant-id header.
| Method | Path | Description |
|---|---|---|
POST | /api/v1/booking-groups/hold | Create a group and hold every activity at once. |
GET | /api/v1/booking-groups/{id} | Get a group with its bookings and totals. |
POST | /api/v1/booking-groups/{id}/confirm | Start payment for a held group. |
Create a group hold
POST /api/v1/booking-groups/hold
{
"packageId": "pkg_team_day",
"groupType": "PACKAGE",
"partySize": 20,
"customerName": "Example Oy",
"customerEmail": "events@example.com",
"customerPhone": "+358401234567",
"slotSelections": [
{ "productId": "p_golf", "siteId": "s_center", "slotId": "sl_1400" },
{ "productId": "p_laser", "siteId": "s_center", "slotId": "sl_1530" }
]
}
| Field | Required | Description |
|---|---|---|
groupType | Yes | PACKAGE or CUSTOM. |
partySize | Yes | Group size, 1–500. |
packageId | No | The event package the group is built from. |
customerName | No | Customer or company name. |
customerEmail | No | Where confirmations are sent. |
customerPhone | No | Customer phone number. |
slotSelections | No | One entry per activity: productId, siteId, slotId, optional partySize. |
All activities are held together with the same expiry. If any slot cannot be held, nothing is held.
The response is the group, with status: "HOLD", holdExpiresAt and a bookings list:
{
"id": "grp_123",
"reference": "GRP-20261015-001",
"status": "HOLD",
"groupType": "PACKAGE",
"partySize": 20,
"currency": "EUR",
"holdExpiresAt": "2026-10-15T11:15:00.000Z",
"bookings": [
{
"bookingId": "bk_001",
"status": "hold",
"slotId": "sl_1400",
"startsAt": "2026-10-15T11:00:00.000Z",
"endsAt": "2026-10-15T12:00:00.000Z"
}
]
}
Confirm a group
POST /api/v1/booking-groups/{id}/confirm
{ "paymentMethod": "STRIPE" }
The group must be in HOLD and the hold must not have expired. The response is the group plus a stripeClientSecret. Complete the payment with Stripe on your page. The bookings stay on hold until Stripe reports the payment, then they are all confirmed together.
This endpoint accepts only STRIPE, with no customTotal or customPricingReason. Anything else gets 403 with code GROUP_CONFIRM_METHOD_NOT_ALLOWED and the bookings stay on hold. Invoiced, complimentary and custom-priced groups are confirmed by staff through the admin endpoint below.
Admin endpoints
Admin endpoints need a staff token with the manage_bookings permission.
| Method | Path | Description |
|---|---|---|
GET | /api/v1/admin/booking-groups | List groups. Filters: status, customerEmail. |
POST | /api/v1/admin/booking-groups | Create a group in DRAFT. Same body as the customer hold. |
GET | /api/v1/admin/booking-groups/{id} | Get a group. |
PATCH | /api/v1/admin/booking-groups/{id} | Update customer details and internalNotes. |
DELETE | /api/v1/admin/booking-groups/{id} | Delete a draft group. |
POST | /api/v1/admin/booking-groups/{id}/bookings | Add an activity to a draft group. |
DELETE | /api/v1/admin/booking-groups/{id}/bookings/{bookingId} | Remove an activity from a draft group. |
POST | /api/v1/admin/booking-groups/{id}/calculate-pricing | Price the group and split the total across its bookings. |
POST | /api/v1/admin/booking-groups/{id}/hold | Hold every activity at once, with one expiry. |
POST | /api/v1/admin/booking-groups/{id}/confirm | Confirm the group. |
POST | /api/v1/admin/booking-groups/{id}/cancel | Cancel every active booking in the group. Body: { "reason" }. |
Add an activity
{
"productId": "p_golf",
"siteId": "s_center",
"slotId": "sl_1400",
"partySize": 20
}
The product must belong to the site, and the slot must belong to the product.
Calculate pricing
Returns the group total and how it is split:
{
"grandTotal": 1100.0,
"subtotal": 893.5,
"taxTotal": 206.5,
"currency": "EUR",
"pricingMethod": "BUNDLE_PER_PERSON",
"allocations": [
{
"bookingId": "bk_001",
"allocatedSubtotal": 446.75,
"taxAmount": 103.25,
"allocatedTotal": 550.0
}
]
}
pricingMethod is FIXED_PRICE, BUNDLE_PER_PERSON or INDIVIDUAL_SUM, depending on the package's pricing.
Confirm (staff)
{
"paymentMethod": "INVOICE",
"customTotal": 900.0,
"customPricingReason": "Returning customer discount"
}
| Field | Required | Description |
|---|---|---|
paymentMethod | Yes | STRIPE, PAYTRAIL, INVOICE or COMP. |
customTotal | No | Override the total. |
customPricingReason | With customTotal | Why the total was changed. Saved in the group notes. |
INVOICE: all bookings are confirmed now; the payment is recorded as pending.COMP: all bookings are confirmed at no charge.STRIPE: returns astripeClientSecret, as in the customer flow.
Group status
| Status | Meaning |
|---|---|
DRAFT | Created; activities are still being added. |
HOLD | Every activity is held. |
CONFIRMED | Paid or approved; every booking is confirmed. |
PARTIALLY_CANCELED | Some bookings are cancelled, others are still active. |
CANCELED | Every booking is cancelled. |
COMPLETED | Every booking has finished. |
EXPIRED | The hold ran out before the group was confirmed. |
The group status follows its bookings and updates when any of them changes.