Skip to main content

Bookings API

This page covers the booking checkout flow (hold, add-ons, offers, codes, confirmation) and the endpoints for changing a booking afterwards. The JavaScript widget uses these same endpoints, so you only need them if you build your own checkout.

Every request needs the x-tenant-id header. Checkout endpoints are public: a booking in hold status can be read and changed by anyone who has its ID. Once a booking is confirmed, reading, cancelling or rescheduling it (including the cancel and reschedule previews and the reschedule history) needs one of:

  • a staff JWT (Authorization: Bearer ...),
  • a customer JWT for the customer who owns it (Authorization: Bearer ... or x-customer-token),
  • the guestAccessToken returned when the booking was confirmed, sent as x-guest-token or ?token=. Links in confirmation emails carry this token.

Staff-only endpoints also check permissions. See Authorization.

Checkout flow​

  1. Create a hold. POST /api/v1/bookings/hold reserves the slot for 15 minutes and returns the price and a pricing hash.

  2. Set add-ons (optional). PUT /api/v1/bookings/{bookingId}/addons reprices the hold.

  3. Show checkout offers (optional). GET /api/v1/checkout-offers?bookingId=... returns up to three discounted add-ons. An accepted offer is sent back through the add-ons endpoint.

  4. Apply a code (optional). POST /api/v1/bookings/{bookingId}/apply-promotion or POST /api/v1/bookings/{bookingId}/apply-voucher.

  5. Pay or confirm.

    • Card (Stripe): create a PaymentIntent, confirm it in the browser, then call POST /api/v1/payments/{bookingId}/confirm.
    • Bank (Paytrail): create a payment and redirect the customer.
    • Pay on arrival or a zero total: call POST /api/v1/bookings/{bookingId}/confirm.

    See Payments API for the payment calls.

Every call that changes the price returns the new totals and pricing hash. Keep the latest pricingHash and send it when you confirm.

Endpoints​

Checkout (public)​

MethodPathPurpose
POST/api/v1/bookings/holdCreate a hold
PUT/api/v1/bookings/{bookingId}/addonsReplace the hold's add-ons and reprice
GET/api/v1/checkout-offersList checkout offers for a hold
POST/api/v1/bookings/{bookingId}/apply-promotionApply a promotion code
POST/api/v1/bookings/{bookingId}/apply-voucherApply a voucher or gift card code
DELETE/api/v1/bookings/{bookingId}/remove-promotionRemove the applied code
POST/api/v1/bookings/{id}/confirmConfirm a hold without an online card payment
POST/api/v1/bookings/{id}/resolve-conflictResolve resource add-on conflicts found at confirm
POST/api/v1/bookings/{bookingId}/participantsAdd participants
PUT/api/v1/bookings/{bookingId}/participantsReplace participants
GET/api/v1/bookings/{bookingId}/participantsList participants

After confirmation​

MethodPathWhoPurpose
GET/api/v1/bookings/{bookingId}Owner or staffGet a booking
GET/api/v1/bookings/{bookingId}/cancel-previewOwner or staffShow the refund the customer would get
POST/api/v1/bookings/{bookingId}/cancelOwner or staffCancel (also releases a hold)
GET/api/v1/bookings/{bookingId}/reschedule-previewOwner or staffShow the reschedule policy and fee
POST/api/v1/bookings/{bookingId}/reschedule/previewOwner or staffCheck a specific new slot without moving
POST/api/v1/bookings/{bookingId}/reschedule/customerOwner or staffMove to a new slot under the policy
GET/api/v1/bookings/{bookingId}/reschedule-historyOwner or staffPast reschedules
POST/api/v1/bookings/{bookingId}/party-size/previewOwner or staffPrice a party size change
PATCH/api/v1/bookings/{bookingId}/party-sizeOwner or staffChange the party size
GET/api/v1/bookings/{bookingId}/calendar.icsPublicDownload a calendar file

Staff actions​

These need a staff JWT and the listed permission.

MethodPathPermissionPurpose
POST/api/v1/bookings/{bookingId}/mark-arrivedbookings:checkin and bookings:writeCheck the customer in
POST/api/v1/bookings/{bookingId}/undo-arrivalbookings:checkin and bookings:writeUndo a check-in
POST/api/v1/bookings/{bookingId}/mark-no-showbookings:checkin and bookings:writeMark as no-show
POST/api/v1/bookings/{bookingId}/mark-paidbookings:write and payments:recordRecord a payment taken outside Sessiq
POST/api/v1/bookings/{bookingId}/completebookings:writeMark as completed
POST/api/v1/bookings/{bookingId}/confirm-holdbookings:writeConfirm a hold on the customer's behalf
POST/api/v1/bookings/{bookingId}/reschedule/adminadmin:bookings:writeReschedule with optional policy, fee or capacity overrides
GET/api/v1/bookings/{bookingId}/transitionsbookings:readStatus history and allowed next statuses
GET/api/v1/bookings/{bookingId}/available-actionsbookings:readActions staff can take now
GET/api/v1/bookings/{bookingId}/paymentsbookings:readPayment transactions
GET/api/v1/bookingsbookings:readList bookings (site_id, product_id, status, date_from, date_to, customer_email, q)
GET/api/v1/bookings/holdsbookings:readActive holds for a site and date

Create a hold​

POST /api/v1/bookings/hold

A hold reserves capacity for 15 minutes. If it is not confirmed in time it becomes expired and the capacity goes back on sale. Bookings that span several slots reserve all of them together, or fail.

curl -X POST https://api.sessiq.com/api/v1/bookings/hold \
-H "Content-Type: application/json" \
-H "x-tenant-id: {tenantId}" \
-d '{
"slotId": "slot_123",
"partySize": 4,
"participantTypes": [
{ "participantTypeId": "pt_adult", "count": 2 },
{ "participantTypeId": "pt_child", "count": 2 }
]
}'
FieldRequiredDescription
slotIdYesSlot from the availability API
partySizeYes1 to 50
variantIdNoProduct variant. Resolved from the slot when omitted
participantTypesNo[{ participantTypeId, count }] for per-type pricing
participantAgesNoAges, for age-based pricing
participantsNoNamed participants (firstName, lastName, ...)
addonsNo[{ id, qty }]. Most clients set add-ons later with the add-ons endpoint
resourceAddonsNoTimed resource add-ons such as a party room: [{ configId, startsAt, endsAt, partySize? }]
roomSelectionsNoThe chosen room for each "choose one" block: [{ blockOrder, resourceId }]. Omitted blocks are auto-picked
durationMinsFor variable-duration productsMust match one of the product's duration options
customerName, customerEmail, customerPhoneNoContact details. Can also be sent at confirmation
consentNoConsent snapshot at booking time
gaClientId, gaSessionId, attributionNoAnalytics identifiers. See Analytics

To link the booking to a logged-in customer, send their token in x-customer-token.

Response:

{
"booking_id": "bk_abc123",
"lifecycleStatus": "hold",
"paymentStatus": "pending",
"expires_at": "2026-10-15T09:15:00.000Z",
"pricing": {
"items": [{ "name": "Adult", "qty": 2, "unit": 25, "subtotal": 50 }],
"discounts": [],
"tax": { "total": 8.76, "inclusive": true },
"subtotal": 71.24,
"total": 80,
"currency": "EUR",
"hash": "3f2a..."
}
}

Money values are in euros. If an automatic promotion applies, it already shows in pricing.discounts and pricing.total (see Promotions).

A full slot returns 422. Invalid input returns 400.

Set add-ons​

PUT /api/v1/bookings/{bookingId}/addons replaces the booking's add-ons and reprices it. Send an empty array to remove them all.

{
"addons": [
{ "id": "addon_equipment", "qty": 2 },
{ "id": "addon_gloves", "qty": 1, "offerId": "off_12345" }
],
"finalizeGroups": true
}
FieldDescription
addons[].idAdd-on ID
addons[].qtyQuantity, at least 1
addons[].offerIdThe checkout offer this line was accepted from. See below
finalizeGroupsSend true on the customer's last add-on save. It also enforces add-on group minimums ("choose at least N"). Group maximums are always enforced

The response has booking.subtotal, booking.taxTotal, booking.grandTotal, booking.pricingHash and the priced booking.addons.

Add-ons can be changed while the booking is hold, confirmed or arrived. If a change raises the total of a paid booking, its payment status becomes part_paid. A booking in another status, an expired hold, or too little add-on stock returns 409.

Checkout offers​

Checkout offers are discounted add-ons shown before payment ("Add gloves for €3"). Staff set them up in the admin under add-ons.

GET /api/v1/checkout-offers?bookingId={bookingId}&locale=fi

QueryRequiredDescription
bookingIdYesThe hold to find offers for
localeNoFor translated titles, for example fi. Defaults to en
{
"offers": [
{
"offerId": "off_12345",
"addonId": "addon_gloves",
"title": "Add grip gloves",
"description": "Recommended for climbing",
"imageUrl": null,
"imageAlt": null,
"mode": "free_qty",
"unitPrice": 3,
"compareAtUnitPrice": 4,
"currency": "EUR",
"vatRate": 0.255,
"suggestedQuantity": 1,
"maxQuantity": null
}
]
}

The endpoint returns at most three offers, in the order staff set. An empty list means there is nothing to show, so skip the step. It returns 404 if the booking is not hold, confirmed or arrived, or the hold has expired.

FieldMeaning
modeyesno (take it or not), free_qty (any quantity), or per_participant
unitPriceThe price charged if the customer accepts
compareAtUnitPriceThe normal add-on price. Only present when it is higher than the offer and staff chose to show it
suggestedQuantityThe party size for per_participant offers, otherwise 1. Do not pre-select it
maxQuantityUpper limit, or null

Accepting an offer. Add the offer's add-on to the PUT .../addons request with its offerId, together with the customer's other add-ons:

const path = '/api/v1/bookings/{bookingId}/addons'.replace(
'{bookingId}',
bookingId,
);

await fetch(apiBase + path, {
method: 'PUT',
headers: { 'Content-Type': 'application/json', 'x-tenant-id': tenantId },
body: JSON.stringify({
addons: [
{ id: 'addon_equipment', qty: 2 },
{ id: offer.addonId, qty: 1, offerId: offer.offerId },
],
}),
});

The server checks that the offer is valid for this booking and prices the line at the offer price. Keep sending the same offerId on every later add-on update, or the line goes back to the normal price. Once accepted, the offer price is locked for that booking even if staff later change or turn off the offer. An unknown or ineligible offerId returns 400 with CHECKOUT_OFFER_NOT_ELIGIBLE.

Promotion codes and vouchers​

Codes can only be applied while the booking is hold. Otherwise the API returns 409.

Promotion code. POST /api/v1/bookings/{bookingId}/apply-promotion

{ "promotionCode": "SUMMER25" }
{
"booking": {
"id": "bk_abc123",
"status": "hold",
"promotionCode": "SUMMER25",
"discountAmount": 20,
"subtotal": 71.24,
"taxTotal": 8.76,
"grandTotal": 60,
"pricingHash": "3f2a...",
"updatedAt": "2026-10-15T09:03:10.000Z"
},
"discount": {
"code": "SUMMER25",
"type": "percentage",
"value": 20,
"appliedAmount": 20
},
"message": "Promotion code applied successfully"
}

A booking carries one promotion. If an automatic promotion already gives a bigger discount, it stays, and message says so. An invalid, expired or out-of-scope code returns 400 and leaves the booking unchanged.

Voucher or gift card. POST /api/v1/bookings/{bookingId}/apply-voucher

{ "voucherCode": "GIFT-ABCD-1234" }

The response has the same booking totals plus a voucher object with code, appliedAmount, balanceBefore, balanceAfter and isFullyUsed. The voucher covers up to the booking total; any leftover balance stays on the code.

Remove. DELETE /api/v1/bookings/{bookingId}/remove-promotion removes the applied code, returns voucher balance to the code, and brings back any automatic promotion that applies.

Confirm a booking​

POST /api/v1/bookings/{id}/confirm confirms a hold that is not paid by card in the browser.

{
"expected_pricing_hash": "3f2a...",
"payment_mode": "poa",
"guest_email": "customer@example.com",
"guest_name": "Maija Meikäläinen",
"guest_phone": "+358401234567"
}
FieldRequiredDescription
expected_pricing_hashYesThe latest pricingHash you received
payment_modeNopoa (pay on arrival) or stripe (default). Any other value returns 400 PAYMENT_MODE_NOT_SUPPORTED
payment_intent_idWith stripeA PaymentIntent the customer already paid. Most clients use POST /api/v1/payments/{bookingId}/confirm instead
guest_email, guest_name, guest_phoneNoContact details for a guest checkout

What happens depends on the mode and the total:

  • Zero total (fully covered by a discount or voucher): the booking becomes confirmed and paid, whatever mode you send.
  • poa: confirmed with payment pending. Only allowed when the product has pay on arrival turned on, has no deposit, and its booking policy does not require payment up front. Otherwise 409 with POA_NOT_ALLOWED, DEPOSIT_REQUIRED or PAYMENT_REQUIRED.

A product with a deposit must be paid online, at least the deposit: pay with paymentAmount through the payments API (see Payments API). The former deposit_later mode, which confirmed without any payment, was removed and now returns 400.

Products that need staff approval return 409 with APPROVAL_REQUIRED.

{
"success": true,
"booking": {
"id": "bk_abc123",
"lifecycleStatus": "confirmed",
"paymentStatus": "pending",
"pricing_validated": true,
"confirmed_at": "2026-10-15T09:05:00.000Z"
},
"guestAccessToken": "eyJhbGciOi...",
"message": "Booking confirmed successfully"
}

Store guestAccessToken if you want to let a guest view or cancel the booking later.

If the booking has timed resource add-ons (for example a party room) that are no longer free, the response has success: false and a conflict object. Send the customer's choice to POST /api/v1/bookings/{id}/resolve-conflict.

Confirm errors​

StatusError codeMeaning
400PAYMENT_MODE_NOT_SUPPORTEDpayment_mode is not stripe or poa (for example the removed deposit_later)
409PRICING_CHANGEDThe hash does not match the current price. Fetch the booking and show the new price
409—The booking is not hold, or the hold has expired. Start a new hold
409POA_NOT_ALLOWED, PAYMENT_REQUIRED, APPROVAL_REQUIREDThe product's policy does not allow this mode
409DEPOSIT_REQUIREDPay on arrival was requested for a product with a deposit. Pay the deposit online

Read a booking​

GET /api/v1/bookings/{bookingId} returns booking, capacityInfo, isExpired and timeUntilExpiry. A hold can be read with just the tenant header. A confirmed booking needs one of the tokens listed at the top of this page, or it returns 403.

Cancel​

Call GET /api/v1/bookings/{bookingId}/cancel-preview first to show the customer the policy and the refund they would get. Then:

POST /api/v1/bookings/{bookingId}/cancel

{ "reason": "Change of plans" }

Cancelling a hold releases it at once. Cancelling a confirmed booking applies the product's cancellation policy and starts any refund due. The response includes booking and, when money is involved, refund_summary.

Staff can cancel a paid booking without a refund by sending "refundOptedOut": true with a refundOptOutReason. The reason is required and goes to the audit log.

Reschedule​

  • GET /api/v1/bookings/{bookingId}/reschedule-preview shows whether the booking can be moved and what it costs.
  • POST /api/v1/bookings/{bookingId}/reschedule/preview with { "newSlotId": "slot_456" } checks one target slot: canReschedule, policyViolations, fee, newSlot and any pricingChanges.
  • POST /api/v1/bookings/{bookingId}/reschedule/customer with { "newSlotId": "slot_456", "reason": "..." } moves the booking if the policy allows it. The response has success, the updated booking, feeCharged and rescheduleEventId.

Staff use POST /api/v1/bookings/{bookingId}/reschedule/admin, which also accepts waiveFee, skipPolicyChecks and forceCapacity.

Change the party size​

POST /api/v1/bookings/{bookingId}/party-size/preview with { "partySize": 6 } returns the new price without changing anything. PATCH /api/v1/bookings/{bookingId}/party-size with the same body makes the change.

Staff actions​

  • Mark arrived accepts optional arrivalTime, notes, acknowledgeLateness and, to take payment at check-in, collectPayment with paymentMethod (stripe_card_present, pos_integration or manual_cash).
  • Mark no-show accepts optional reason, notes and noShowFee. To charge a no-show fee, see No-show fees.
  • Mark paid accepts paymentMethod, transactionId, amountPaid and notes.
  • Complete accepts actualEndTime, notes and serviceFeedback.

Booking status​

A booking has two independent statuses.

Lifecycle (lifecycleStatus):

ValueMeaning
holdReserved for 15 minutes, not yet confirmed
confirmedConfirmed
arrivedChecked in
completedFinished
canceledCancelled by the customer, staff or the system
no_showThe customer did not come
expiredThe hold ran out before confirmation

Payment (paymentStatus):

ValueMeaning
pendingNothing paid yet
part_paidPartly paid, for example a deposit
paidFully paid
failedThe payment failed
refundedRefunded

A pay-on-arrival booking, for example, is confirmed + pending until the customer pays at the venue.

The pricing hash​

Every hold carries a hash of its current price. You send the latest hash when you confirm, and the server rejects the confirm with 409 PRICING_CHANGED if the price has moved. This stops a customer being charged a price they did not see. Always use the pricingHash from the most recent response that returned one.