Перейти к содержимому

Custom widgets and the channel API

A Custom widget lets you build your own booking experience. You set the rules in Bookingmood, and your own code uses the channel API to load availability and accept bookings.

Pre-release: the channel API can still change, including in ways that break existing code, and this page may not cover everything yet. JavaScript packages for the API aren't available yet. Test your integration carefully before you rely on it.

Create a Custom widget

Custom widgets are available from the Booking plan.

  • Go to Channels and click New channel.

  • Choose Custom widget, enter a title and select the units it includes.

  • To accept bookings, set Interaction to Book.

  • Copy the Publishable key.

A Custom widget has no display settings. It only sets what its API returns and allows: the units, how booked, closed and pending dates show, whether guests can book, what happens after booking, whether reviews are available and, optionally, a password.

The Publishable key is the widget's ID. It's public, so you can use it in a browser. Never put a Bookingmood API key in browser code: those are secret.

Requests

The base URL is https://www.bookingmood.com/api/public/v1. Requests are allowed from any website.

const channelId = "YOUR_PUBLISHABLE_KEY";
const baseUrl = "https://www.bookingmood.com/api/public/v1";

const response = await fetch(`${baseUrl}/channels/${channelId}`);
const { availability, channel, organization, products } = await response.json();
Method and pathWhat it does
GET /channels/:idThe channel, its organization and units, and their availability. Counts as a view of the channel.
GET /channels/:id/availabilityAvailability only. Add product_ids once per unit to limit it to those units.
POST /channels/:id/calendars/syncSyncs the imported calendars of units.
GET /channels/:id/reviewsReviews of a unit, if the channel shows reviews.
GET /channels/:id/couponsCoupons by code. Add codes once per code.
POST /channels/:id/coupons/verifyChecks whether a coupon code can be used.
POST /channels/:id/cart/validateChecks whether a cart can be booked, and what it costs.
POST /channels/:id/cart/checkoutBooks a cart.
POST /channels/:id/accessExchanges the channel's password for a token.
GET /bookings/:idThe status and payments of a booking.
POST /bookings/:id/payGets a payment link for a pending booking.
POST /bookings/:id/cancelCancels a pending booking.

Book a stay

Bookings go through a cart, which holds one or more stays:

const cart = {
  coupon_codes: [],
  items: [
    {
      id: "stay-1", // Your own ID, to match results to items
      product_id: "UNIT_ID",
      interval: { start: "2027-03-01", end: "2027-03-08" },
      occupancy: { CAPACITY_GROUP_ID: 2 },
      form_values: { SERVICE_ID: "Arriving around 16:00" },
    },
  ],
};
  • interval: dates as YYYY-MM-DD. A nightly stay ends on the day guests leave. A daily stay ends the day after the last day.

  • occupancy: the number of guests per capacity group, by the ID in organization.capacity_groups.

  • form_values: the answers to the booking form, by the ID in the unit's services.

1. Validate the cart

Validate the cart before you ask a guest for their details or payment. The result says whether each stay can be booked and what it costs. Amounts are in cents.

const response = await fetch(`${baseUrl}/channels/${channelId}/cart/validate`, {
  body: JSON.stringify(cart),
  headers: { "Content-Type": "application/json" },
  method: "POST",
});
{
  "items": [
    {
      "availabilityMatch": true,
      "id": "stay-1",
      "occupancyMatch": true,
      "price": {
        "currency": "EUR",
        "deposit": 0,
        "lines": [],
        "subtotal": 70000,
        "tax": 6300,
        "total": 76300
      },
      "valid": true
    }
  ],
  "valid": true
}

2. Check out

Send the cart with an optional display currency and language. Checking out creates a real booking and returns a 201 response. The cart can still be refused, because availability can change after you validate it.

const response = await fetch(`${baseUrl}/channels/${channelId}/cart/checkout`, {
  body: JSON.stringify({ cart, currency: null, language: "en-US" }),
  headers: { "Content-Type": "application/json" },
  method: "POST",
});
const { booking_id, payment_expiry_timestamp, payment_url, reference, secret } =
  await response.json();

If payment_url isn't null, send the guest there to pay. After paying, guests go where the widget's After booking setting says.

3. Pay or cancel later

Use the booking's secret as bearer token for the /bookings/:id routes. It gives access to the booking's status and payments, without guest details. Share it only with the guest who made the booking.

const response = await fetch(`${baseUrl}/bookings/${booking_id}/pay`, {
  body: JSON.stringify({ language: "en-US" }),
  headers: {
    Authorization: `Bearer ${secret}`,
    "Content-Type": "application/json",
  },
  method: "POST",
});
const { payment_url } = await response.json();

Paying and cancelling only work while a booking is waiting for its payment. Otherwise they return 409 with the code booking_not_pending. A successful cancellation returns 204 without a body.

Coupons

POST /channels/:id/coupons/verify takes the code, the product_ids in the cart and the current_codes already entered. Its result is one of:

ResultMeaning
VALIDThe code can be used.
INVALIDThe code doesn't exist or doesn't apply to these units.
STACK_NOT_ALLOWEDA code that's already entered can't be combined with others.
LIMIT_REACHEDThe cart already has 5 codes, the maximum.

Reviews

GET /channels/:id/reviews needs a product_id. You can add a limit from 1 to 1000, which is the default, and an order of most-recent (the default), highest-rating or lowest-rating.

Sync imported calendars

If your units import calendars, send { "product_ids": [...] } to POST /channels/:id/calendars/sync to bring them up to date. The response holds the totals. With Accept: application/x-ndjson, you get a line of JSON as each calendar finishes instead.

A request stops starting new calendars after 40 seconds. If remaining in the result is above 0, send another request.

Password-protected channels

If the widget has a password, every channel request returns 401 with the code password_required until the visitor signs in:

const response = await fetch(`${baseUrl}/channels/${channelId}/access`, {
  body: JSON.stringify({ password }),
  headers: { "Content-Type": "application/json" },
  method: "POST",
});
const { expiresAt, token } = await response.json();

Send the token as Authorization: Bearer <token> with later requests. It's valid for a week, and expiresAt gives the time in milliseconds.

Errors and limits

Errors have a code and a message:

{
  "code": "channel_not_found",
  "details": null,
  "hint": null,
  "message": "No channel found with this ID"
}
StatusCodeMeaning
400BM400The request doesn't match the schema of the route. details says what's wrong.
400product_not_listedA unit in the request isn't part of this channel.
400checkout_failedThe cart can't be booked. message says why.
400password_not_setThe channel doesn't have a password.
401password_requiredThe channel needs an access token.
401invalid_passwordThe password is incorrect.
403channel_inactiveThe organization's plan or subscription doesn't allow this channel.
403channel_not_bookableThe channel's Interaction isn't Book.
403reviews_disabledThe channel doesn't show reviews.
404channel_not_foundNo channel has this ID.
404booking_not_foundThe booking doesn't exist, or the secret is wrong.
409booking_not_pendingThe booking isn't waiting for a payment.
429rate_limitedToo many requests.

Per IP address, the limits are 300 requests a minute per channel, 60 a minute to the booking routes and 10 a minute to verify coupons. Password attempts are limited too.

Последнее изменение 6 ноября 2026 г.


Возьмите управление своими бронированиями в свои руки. Создайте собственный бизнес.

Присоединяйтесь к 1200+ независимым компаниям по аренде жилья для отдыха, которые используют Bookingmood для увеличения количества прямых бронирований и укрепления отношений с гостями.

Начать бесплатный пробный период на 14 дн.