Booking API
The Booking API lets you read booking availability, manage booking products, list and check in bookings, and manage door-lock pin codes from your own systems. It is part of the Periode Merchant API.
See the Getting started guide for the server URL, your
x-api-key, and the shared conventions (JSON bodies, minor units,:id= your merchant ID, error semantics). All paths below are relative to the base URL; most endpoints require thex-api-keyheader.
Conventions (booking-specific)
- Dates are ISO
YYYY-MM-DDstrings. - Times in responses are
HH:mmstrings. - Lead times are in hours.
minTimeBeforeOrder,minTimeBeforeOrderNonEmptyandminTimeBeforeCancelare decimal hours, so24means 24 hours, not 24 minutes. enabled: falsedoes not retract sessions that are already published. Disabling a product stops the nightly job from publishing new days, but slots already generated stay bookable. To take a product off sale immediately, setclosedDates— those dates are filtered out of the availability responses at read time, so they stop selling straight away. Clearing the schedule and republishing withpublishHoursis not equivalent: the rebuild only drops slots that are completely unbooked and not frozen, so a slot with even one booking survives and keeps selling its remaining seats, and dates beyond thedaysyou republish are untouched.- Two products only block each other if they are linked. Booking a session in
product A does not automatically consume capacity in product B at the same
time — each product has its own generated slots. Sharing happens in two ways:
a product with
parentProductset reads and writes the parent’s slot grid, so parent and all its children share one capacity pool; and products that shareresourceentries compete for those resources. Two independent products with their own schedules are not mutually exclusive, so a shared session and a private session on the same physical room must be linked one of those two ways — otherwise both can be booked for the same hour.parentProduct.idmust be one of your own booking products; another merchant’s id is rejected as not found.
Availability
Get events for a booking group
Returns every bookable slot across all products in a booking group, within a date range.
POST /merchants/:id/bookingGroup
Body
bookingGroupId(string, required): the booking group to read.from(string, required): start date (YYYY-MM-DD).to(string, required): end date (YYYY-MM-DD).onlyAvailiable(boolean, optional): iftrue, omit slots with no remaining capacity. This is a capacity filter only — it does not look at the clock, so a session that has already started today is still returned as long as it has free seats.onlyBookable(boolean, optional): iftrue, additionally omit slots that can no longer be ordered because their start time falls inside the product’s lead-time window (minTimeBeforeOrder, orminTimeBeforeOrderNonEmptyonce the slot has bookings). This is normally what you want when rendering “next available session” or a public calendar. Combine withonlyAvailiableto get slots that are both free and still orderable. Per-slotcustomMinTimeBeforeOrderoverrides andminTimeAfterOrderare not applied, so a product using those can still list a slot the checkout later refuses.includeDescription(boolean, optional): iftrue, include the product description on each event.
Response
{
"events": [
{
"url": "https://minside.periode.no/booking/<merchantId>/<manifestId>/<date>",
"name": "Klatrekurs",
"name_en": "Climbing course",
"image": "https://...",
"time": "17:00",
"endTime": "18:00",
"date": "2026-08-01",
"description": "…",
"capacity": 8,
"message": "…"
}
],
"manifests": ["/* full BookingManifest objects for the group */"]
}
capacity is the number of spots still available for the slot. Events are sorted
by date and start time.
Get events for a single booking product
Same as above, but scoped to one booking product (manifest) instead of a whole group.
POST /merchants/:id/bookingManifest
Body
bookingManifestId(string, required): the booking product to read.from(string, required): start date (YYYY-MM-DD).to(string, required): end date (YYYY-MM-DD).onlyAvailiable(boolean, optional): iftrue, omit slots with no remaining capacity. This is a capacity filter only — it does not look at the clock, so a session that has already started today is still returned as long as it has free seats.onlyBookable(boolean, optional): iftrue, additionally omit slots that can no longer be ordered because their start time falls inside the product’s lead-time window (minTimeBeforeOrder, orminTimeBeforeOrderNonEmptyonce the slot has bookings). This is normally what you want when rendering “next available session” or a public calendar. Combine withonlyAvailiableto get slots that are both free and still orderable. Per-slotcustomMinTimeBeforeOrderoverrides andminTimeAfterOrderare not applied, so a product using those can still list a slot the checkout later refuses.includeDescription(boolean, optional): iftrue, include the product description on each event.
Response
{
"events": ["/* same event shape as the booking group endpoint */"]
}
Get booked seats per product on a day
Returns, for each product in a booking group, its open hours and the number of booked seats on a single date. Where the booking-group and single-product endpoints above report remaining capacity for the public booking flow, this endpoint is meant for admin use and shows how full each slot is.
GET /merchants/:id/bookingGroup/:bookingGroupId/slots/:date
:bookingGroupId is the booking group to read and :date is YYYY-MM-DD.
Requires the x-api-key header.
Response
{
"date": "2026-08-01",
"products": [
{
"manifestId": "<manifestId>",
"name": "Klatrekurs",
"openHours": [
{
"time": "17:00",
"endTime": "18:00",
"capacity": 8,
"booked": 4,
"message": "…"
}
]
}
]
}
capacity is the slot’s total number of seats and booked is how many of them
are taken (so capacity - booked seats remain). Each product’s openHours are
sorted by start time.
Bookings
The booking record
Three endpoints return bookings — the two list endpoints below and the single-booking endpoint — and they all return the same record: the full stored booking, plus four fields the API adds on top.
startsAt— the start of the booking as an ISO timestamp (2026-08-01T17:30:00, in the booking’s owntimezone). The storedtimeis decimal hours (17.5= 17:30), so this is the readable form ofdate+time. It is the same field the booking webhook sends.cancelKey— the key that authorizes cancelling this booking.cancelUrl— the customer-facing cancel page,https://minside.periode.no/bookings-cancel/<merchantId>/<bookingId>/<cancelKey>. The key is created on first request and stays stable afterwards, so the same link keeps working in your own emails and pages. Anyone holding it can cancel the booking, so treat it as a capability and only send it to the customer who made that booking.user.acceptedMarketing—truewhen the customer ticked a marketing consent box on this booking,falsewhen they did not. It sits alongside the storeduser.marketingslist, which names what they consented to.
Campaign and consent parameters that reached Periode on the booking link come
back on the record too: gclid, fbclid, gaClientId (from
client_id), gaSessionId (from session_id), and the consent answer as
adUserData (true / false) with adUserDataAt (ISO 8601 in
UTC). The click IDs are stored exactly as sent, the consent answer as a boolean
(ad_user_data=granted becomes true), and a parameter that was not sent is
absent rather than defaulted. See the GA4 and Meta tracking guide for how to
pass them.
The rest of the record is the stored booking document, so the property names are
Periode’s internal ones: bookingManifestId for the product, manifestName for
its name, user for the customer, state for booked / confirmed (checked in)
/ cancelled / no-show / stopped. Amounts are in minor units and
length is in decimal hours.
These endpoints currently pass the stored booking straight through. Read the fields you need and ignore the rest — the document carries internal fields that are not part of the contract and may change.
Get one booking
Returns one booking your merchant owns.
GET /merchants/:id/bookings/:bookingId/details
:bookingId is the booking ID. Requires the x-api-key header.
Response — { "booking": { … } }, the record described above.
A booking that does not exist, or belongs to another merchant, is rejected the
same way as any other unknown resource — 400 with an empty body — so the
response never reveals which of the two it was.
Only finalized (paid) bookings are returned; a reservation that never completed payment is not a booking yet and is rejected the same way.
List bookings for a product on a day
Returns all bookings for a product on a single date.
GET /merchants/:id/getBookings/:manifestId/:date
:manifestId is the booking product ID and :date is YYYY-MM-DD.
Response — an array of booking records.
List all bookings on a date
Returns every booking at the merchant for a given date, across all products.
GET /merchants/:id/getBookingsMerchant/:date
:date is YYYY-MM-DD and filters on the booking’s own date — the day the
session takes place — not on when it was created. This is the same query the
booking statistics report uses, so a day’s total here matches what the Admin
portal shows for that day.
Cancelled bookings are left out. Everything still standing is included —
booked, confirmed, no-show and stopped.
Response — an array of booking records, ordered by date.
List bookings created on a date
Returns every booking created at the merchant on a given date, across all products.
GET /merchants/:id/bookings/:date
:date is YYYY-MM-DD and covers the full 24 hours in the merchant’s
timezone.
The date filters on each booking’s creation time (createdAt), not the date
the booking is for.
Response — an array of stored booking documents. This endpoint does not
yet add startsAt, cancelKey, cancelUrl or user.acceptedMarketing.
Check in / confirm a booking
Marks a booking as checked in, or reverts a check-in.
PUT /merchants/:id/bookings/:bookingId/confirm
Body
checkedIn(boolean, required):truechecks the booking in;falsereverts the check-in.
Response — 200 with "SUCCESS" on success. Returns 404 if the booking
does not exist and 401 if it belongs to another merchant.
Validating an external membership (JWT)
Use this when another platform sends its own members to book a Periode session and you want to confirm the membership before the booking goes through, without creating a Periode login for the customer. You render the calendar from the Availability endpoints on your side, then link the customer into the Periode booking page with a signed token that vouches for them.
How it works
- Give the product a webhook integration. The token is only honoured on a
product that has a Webhook url set (
Booking > Products > Edit > Admin). On a product without one the token is ignored. - Mint a JWT signed with your merchant shared secret when a validated member
clicks through. It is the same shared secret used for webhooks, under
Settings > Developer. - Pass it to the booking page as the
tokenquery parameter. Periode verifies it against your shared secret. A valid token marks the booking as a member booking, so it may book members-only sessions, and records the customer as your external user instead of a Periode account. The customer is not asked to log in.
A token that is missing, expired, or signed with the wrong secret is ignored, and the page falls back to the normal non-member flow.
The token confirms membership and unlocks members-only sessions; it does not by itself apply a discount. Set the price the external platform pays on the product the token books (see Track each channel separately below).
The token
Sign an HS256 JWT with your shared secret. Claims:
user_id(string, required): the customer’s ID in your system. Saved on the booking asexternalOwnerIdand used as the owner, so the same member’s bookings stay linked.exp(number, required): standard JWT expiry, seconds since the epoch. Expired tokens are rejected.name,email,phone(optional): the customer’s details. When present they are saved as the booking’s customer, so nothing has to be re-entered.
Put it on the booking link (see Link parameters for the full URL shape):
/booking/<merchantId>/<productId>/2026-09-14/18:00?quantities=...&token=<jwt>
The token is remembered per merchant in the browser and pairs with autoBook to
take an entitled member straight to payment (or to a free confirmation when the
price is 0).
Track each channel separately
These bookings share the physical session with your walk-in and direct customers,
so create a separate linked product for the external platform rather than
sending everyone through one product: a child product with parentProduct set to
the main session (so the two share one capacity pool, see Two products only
block each other if they are linked above), priced at the rate the platform
pays and restricted to members so only a valid token can book it. Bookings made
with the token then land on that product, which is how you report how many seats
each channel filled. See Linked products and shared capacity in the admin
guide.
Booking products (manifests)
Prefer
POST/PUT /merchants/:id/products/booking. The two endpoints on this page are the older, booking-specific ones. They rejectdiscountsandlockSystemswithunrecognized_keys, and they requireinfoLinkas a bare string even though the stored field is an object — so a full product configuration cannot be expressed through them. The unified product endpoints in the Integrations guide accept the whole package, includingpublishHours. These remain for existing integrations.
Create a booking product
Creates a new booking product.
POST /merchants/:id/createBookingManifest
Update a booking product
Updates an existing booking product. Pass the product’s id in the body.
PUT /merchants/:id/updateBookingManifest
Required on create. This endpoint validates strictly and rejects unknown keys, and 26 fields are required — omitting any one fails the whole call:
name, description, vat, capacity, maxOrderSize, type,
generateDaysAhead, price, paymentType, supportDiscountCode, startDate,
resourceNames (exactly 2), receiptNames (exactly 2), refundType,
closedDates, emailDetails, termsAndConditionsUrl, publishNightly,
enabled, contactPhoneNumber, mapsUrl, mapsDescription, infoLink,
length, isFullDay, schedule (exactly 7 entries).
Optional: id, merchantEmail, endDate, imageUrl, backgroundImageUrl,
externalId, webhookUrl, customContinueMessage, minTimeBeforeOrder,
merchantSms, publishHours.
Anything else — discounts, lockSystems, extraFields, minTimeBeforeCancel,
timezone, tooLateMessage — is rejected with unrecognized_keys. Use
POST/PUT /products/booking instead, which accepts the full product.
Note that infoLink is a plain string here, while the stored field (and the
/products/booking endpoints) use an object { url, name }. This is a known
wart of this older endpoint.
Both endpoints share the same body schema. Key fields:
id(string): product ID. Omit to create a new product; include it to update an existing one.name(string): product name.description(string): product description.type(string): one ofevent,recurring,ticket,open-event.price(number): price in minor units (cents).vat(number): VAT as a fraction between0and1(e.g.0.25).capacity(number): slot capacity.maxOrderSize(number): maximum spots per order.length(number): session length in decimal hours (1= one hour,1.5= 1h30m). Not minutes.paymentType(string array): use["ADYEN"]. Adyen’s checkout already includes Vipps when enabled underSettings > Payment methods; sending["VIPPS"]on an Adyen merchant makes checkout fail with400 Failed creating session. See Payment types in Getting started.refundType(string):money,only-giftCard,50-percent-money-or-100-percent-giftcard, ornone.generateDaysAhead(number): how many days of slots to publish ahead.startDate(string): first bookable date (YYYY-MM-DD).closedDates(string array): dates (YYYY-MM-DD) with no availability.schedule(array): weekly schedule — exactly 7 entries (validated), Monday-first (index0= Monday … index6= Sunday), each with astartTimesarray (see below).enabled(boolean): whether the product is live.webhookUrl(string, optional): webhook called on booking events.externalId(string, optional): identifier from your own system.
Each entry in schedule is { "startTimes": [ … ] }. Each start time is a
block covering a whole continuous opening window, not one entry per session —
individual sessions are cut from the block using the product’s length. To open
Monday 08:00–16:00 with one-hour sessions, the Monday entry is a single
{ "hour": 8, "minute": 0, "length": 8 } block, not eight one-hour entries.
Split a block only when part of it needs a different capacity, price or message.
A start time is:
{
"hour": 17,
"minute": 0,
"length": 4,
"customCapacity": null,
"priceAdjustments": null,
"customLength": null,
"onlyMembers": null
}
Response — the created or updated product’s id.
Door locks
Update the static pin code for a product
Sets the shared static pin code for a product configured with a STATICPIN
lock system.
POST /merchants/:id/updatePincode/:manifestId
Body
code(string, required): the new pin code.
Set an external pin code for a booking
Stores a per-booking pin code for a product configured with an EXTERNALPIN
lock system.
POST /merchants/:id/bookings/:bookingId/pincode
Body
code(string, required): the pin code for this booking.codeFormatted(string, optional): a display-formatted version of the code.