6 min. leestijd

# Eigen widgets en de kanaal-API

**Met een** _**Eigen widget**_ **bouw je je eigen boekingservaring. Je stelt de regels in Bookingmood in, en je eigen code gebruikt de kanaal-API om beschikbaarheid op te halen en boekingen aan te nemen.**

**Pre-release:** de kanaal-API kan nog veranderen, ook op manieren die bestaande code laten stoppen met werken, en deze pagina behandelt misschien nog niet alles. JavaScript-pakketten voor de API zijn nog niet beschikbaar. Test je integratie goed voordat je erop vertrouwt.

## [Een eigen widget maken](#een-eigen-widget-maken)

Eigen widgets zijn beschikbaar vanaf het Booking-abonnement.

- Ga naar _Kanalen_ en klik op _Nieuw kanaal_.
- Kies _Eigen widget_, voer een titel in en selecteer de objecten die erbij horen.
- Om boekingen aan te nemen, zet je _Interactie_ op _Boeken_.
- Kopieer de _Publieke sleutel_.

Een eigen widget heeft geen weergave-instellingen. Hij bepaalt alleen wat de API teruggeeft en toestaat: de objecten, hoe geboekte, gesloten en openstaande data worden getoond, of gasten kunnen boeken, wat er na het boeken gebeurt, of beoordelingen beschikbaar zijn en, als je wilt, een wachtwoord.

De _Publieke sleutel_ is de ID van de widget. Die is openbaar, dus je kunt hem in een browser gebruiken. Zet nooit een Bookingmood-[API-sleutel](https://www.bookingmood.com/nl-NL/docs/ontwikkelaars) in browsercode: die zijn geheim.

## [Verzoeken](#verzoeken)

De basis-URL is `https://www.bookingmood.com/api/public/v1`. Verzoeken zijn toegestaan vanaf elke 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();
```

| Methode en pad | Wat het doet |
| --- | --- |
| `GET /channels/:id` | Het kanaal, de organisatie en objecten ervan, en hun beschikbaarheid. Telt als een weergave van het kanaal. |
| `GET /channels/:id/availability` | Alleen de beschikbaarheid. Voeg `product_ids` per object één keer toe om het tot die objecten te beperken. |
| `POST /channels/:id/calendars/sync` | Synchroniseert de geïmporteerde kalenders van objecten. |
| `GET /channels/:id/reviews` | Beoordelingen van een object, als het kanaal beoordelingen toont. |
| `GET /channels/:id/coupons` | Kortingscodes op code. Voeg `codes` per code één keer toe. |
| `POST /channels/:id/coupons/verify` | Controleert of een kortingscode gebruikt kan worden. |
| `POST /channels/:id/cart/validate` | Controleert of een winkelwagen geboekt kan worden en wat die kost. |
| `POST /channels/:id/cart/checkout` | Boekt een winkelwagen. |
| `POST /channels/:id/access` | Wisselt het wachtwoord van het kanaal in voor een token. |
| `GET /bookings/:id` | De status en betalingen van een boeking. |
| `POST /bookings/:id/pay` | Haalt een betaallink op voor een openstaande boeking. |
| `POST /bookings/:id/cancel` | Annuleert een openstaande boeking. |

## [Een verblijf boeken](#een-verblijf-boeken)

Boekingen lopen via een winkelwagen, met daarin een of meer verblijven:

```
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`: data als `YYYY-MM-DD`. Een verblijf per nacht eindigt op de dag dat gasten vertrekken. Een verblijf per dag eindigt de dag na de laatste dag.
- `occupancy`: het aantal gasten per capaciteitsgroep, met de ID uit `organization.capacity_groups`.
- `form_values`: de antwoorden op het [boekingsformulier](https://www.bookingmood.com/nl-NL/docs/boekingsformulier), met de ID uit de `services` van het object.

### [1. De winkelwagen valideren](#1-de-winkelwagen-valideren)

Valideer de winkelwagen voordat je een gast om gegevens of een betaling vraagt. Het resultaat laat zien of elk verblijf geboekt kan worden en wat het kost. Bedragen zijn in centen.

```
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. Afrekenen](#2-afrekenen)

Stuur de winkelwagen mee, met optioneel een weergavevaluta in `currency` en een taal in `language`. Afrekenen maakt een echte boeking aan en geeft een `201`-antwoord. De winkelwagen kan nog steeds worden geweigerd, omdat de beschikbaarheid kan veranderen nadat je hebt gevalideerd.

```
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();
```

Als `payment_url` niet `null` is, stuur je de gast daarheen om te betalen. Na het betalen gaan gasten naar wat de instelling _Na boeking_ van de widget aangeeft.

### [3. Later betalen of annuleren](#3-later-betalen-of-annuleren)

Gebruik de `secret` van de boeking als bearer-token voor de routes onder `/bookings/:id`. Die geeft toegang tot de status en betalingen van de boeking, zonder gegevens van de gast. Deel hem alleen met de gast die de boeking heeft gemaakt.

```
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();
```

Betalen en annuleren werken alleen zolang een boeking op de betaling wacht. Anders krijg je `409` terug met de code `booking_not_pending`. Een geslaagde annulering geeft `204` terug, zonder inhoud.

## [Kortingscodes](#kortingscodes)

`POST /channels/:id/coupons/verify` krijgt de `code`, de `product_ids` in de winkelwagen en de al ingevoerde `current_codes`. Het `result` is een van deze waarden:

| Resultaat | Betekenis |
| --- | --- |
| `VALID` | De code kan worden gebruikt. |
| `INVALID` | De code bestaat niet of geldt niet voor deze objecten. |
| `STACK_NOT_ALLOWED` | Een code die al is ingevoerd, kan niet met andere worden gecombineerd. |
| `LIMIT_REACHED` | De winkelwagen heeft al 5 codes, het maximum. |

## [Beoordelingen](#beoordelingen)

`GET /channels/:id/reviews` heeft een `product_id` nodig. Je kunt een `limit` van 1 tot 1000 (de standaardwaarde) toevoegen, en een `order`: `most-recent` (de standaardwaarde), `highest-rating` of `lowest-rating`.

## [Geïmporteerde kalenders synchroniseren](#geimporteerde-kalenders-synchroniseren)

Als je objecten [kalenders importeren](https://www.bookingmood.com/nl-NL/docs/synchronisatie/ical), stuur je `{ "product_ids": [...] }` naar `POST /channels/:id/calendars/sync` om ze bij te werken. Het antwoord bevat de totalen. Met `Accept: application/x-ndjson` krijg je in plaats daarvan een regel JSON zodra elke kalender klaar is.

Een verzoek start na 40 seconden geen nieuwe kalenders meer. Als `remaining` in het resultaat boven de 0 ligt, stuur je nog een verzoek.

## [Kanalen met een wachtwoord](#kanalen-met-een-wachtwoord)

Als de widget een wachtwoord heeft, geeft elk kanaalverzoek `401` terug met de code `password_required`, totdat de bezoeker is ingelogd:

```
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();
```

Stuur het `token` als `Authorization: Bearer <token>` mee met volgende verzoeken. Het is een week geldig, en `expiresAt` geeft die tijd in milliseconden.

## [Fouten en limieten](#fouten-en-limieten)

Fouten hebben een `code` en een `message`:

```
{
  "code": "channel_not_found",
  "details": null,
  "hint": null,
  "message": "No channel found with this ID"
}
```

| Status | Code | Betekenis |
| --- | --- | --- |
| `400` | `BM400` | Het verzoek past niet bij het schema van de route. `details` geeft aan wat er mis is. |
| `400` | `product_not_listed` | Een object in het verzoek hoort niet bij dit kanaal. |
| `400` | `checkout_failed` | De winkelwagen kan niet worden geboekt. `message` geeft de reden. |
| `400` | `password_not_set` | Het kanaal heeft geen wachtwoord. |
| `401` | `password_required` | Het kanaal heeft een toegangstoken nodig. |
| `401` | `invalid_password` | Het wachtwoord is onjuist. |
| `403` | `channel_inactive` | Het abonnement van de organisatie staat dit kanaal niet toe. |
| `403` | `channel_not_bookable` | De _Interactie_ van het kanaal staat niet op _Boeken_. |
| `403` | `reviews_disabled` | Het kanaal toont geen beoordelingen. |
| `404` | `channel_not_found` | Er is geen kanaal met deze ID. |
| `404` | `booking_not_found` | De boeking bestaat niet, of de secret klopt niet. |
| `409` | `booking_not_pending` | De boeking wacht niet op een betaling. |
| `429` | `rate_limited` | Te veel verzoeken. |

Per IP-adres zijn de limieten 300 verzoeken per minuut per kanaal, 60 per minuut naar de boekingsroutes en 10 per minuut om kortingscodes te controleren. Ook het aantal wachtwoordpogingen is beperkt.

Laatst gewijzigd 6 november 2026
