Docs menu and search Booking API

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 the x-api-key header.

Conventions (booking-specific)

  • Dates are ISO YYYY-MM-DD strings.
  • Times in responses are HH:mm strings.
  • Lead times are in hours. minTimeBeforeOrder, minTimeBeforeOrderNonEmpty and minTimeBeforeCancel are decimal hours, so 24 means 24 hours, not 24 minutes.
  • enabled: false does 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, set closedDates — those dates are filtered out of the availability responses at read time, so they stop selling straight away. Clearing the schedule and republishing with publishHours is 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 the days you 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 parentProduct set reads and writes the parent’s slot grid, so parent and all its children share one capacity pool; and products that share resource entries 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.id must 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): if true, 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): if true, additionally omit slots that can no longer be ordered because their start time falls inside the product’s lead-time window (minTimeBeforeOrder, or minTimeBeforeOrderNonEmpty once the slot has bookings). This is normally what you want when rendering “next available session” or a public calendar. Combine with onlyAvailiable to get slots that are both free and still orderable. Per-slot customMinTimeBeforeOrder overrides and minTimeAfterOrder are not applied, so a product using those can still list a slot the checkout later refuses.
  • includeDescription (boolean, optional): if true, 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): if true, 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): if true, additionally omit slots that can no longer be ordered because their start time falls inside the product’s lead-time window (minTimeBeforeOrder, or minTimeBeforeOrderNonEmpty once the slot has bookings). This is normally what you want when rendering “next available session” or a public calendar. Combine with onlyAvailiable to get slots that are both free and still orderable. Per-slot customMinTimeBeforeOrder overrides and minTimeAfterOrder are not applied, so a product using those can still list a slot the checkout later refuses.
  • includeDescription (boolean, optional): if true, 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 own timezone). The stored time is decimal hours (17.5 = 17:30), so this is the readable form of date + 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 — true when the customer ticked a marketing consent box on this booking, false when they did not. It sits alongside the stored user.marketings list, 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): true checks the booking in; false reverts 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

  1. 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.
  2. 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.
  3. Pass it to the booking page as the token query 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 as externalOwnerId and 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 reject discounts and lockSystems with unrecognized_keys, and they require infoLink as 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, including publishHours. 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 of event, recurring, ticket, open-event.
  • price (number): price in minor units (cents).
  • vat (number): VAT as a fraction between 0 and 1 (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 under Settings > Payment methods; sending ["VIPPS"] on an Adyen merchant makes checkout fail with 400 Failed creating session. See Payment types in Getting started.
  • refundType (string): money, only-giftCard, 50-percent-money-or-100-percent-giftcard, or none.
  • 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 (index 0 = Monday … index 6 = Sunday), each with a startTimes array (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.