UltimateBookings

API guide

For a developer or technical helper of a customer. With the API, your website, CRM or accountant can read data from Ultimate Bookings. Questions? info@ultimate-bookings.com.

The names in the API itself (sources, fields, parameters and error codes) are Dutch, for example artiest (artist) or limiet (limit). Use them exactly as written; this guide explains each one in English. The screens in Ultimate Bookings are still in Dutch, so we name the Dutch labels you will see there.

What it is

An API you can only read from. Writing, changing or deleting is not possible. Every key belongs to one organisation and sees only that organisation's data. Where that boundary lies is not an agreement in our code that someone could forget: every request goes through the database, which checks the key and returns only rows of that organisation.

There are two kinds of key:

Creating a key

  1. In Ultimate Bookings, go to Instellingen, API-sleutels (Settings, API keys). Only the owner of the organisation can create, pause and delete keys here.
  2. Give the key a name (up to 80 characters), for example Website or CRM.
  3. Choose the kind: website key or full key.
  4. For a full key, tick what it may read: Shows (always on), Agenda, Contacten (contacts), Artiesten (artists) and Geld (money). Money is off by default: only once you tick it does the key see fees and invoices.
  5. Choose Sleutel maken (create key). The key is then shown once. Copy it straight away. We only store a derived value (a hash), so we cannot show it to you again. Lost it? Delete it and create a new one.

You can pause a key: it stops working immediately and you can switch it back on later. Deleting is permanent. The list shows for each key when it was last used and how often.

A show only appears for the website key once you have ticked Op je website tonen (show on your website) yourself in the booking form. It is off by default, so a private party or an option (hold) never becomes public by itself.

Authenticating with your key

Send the key in the header of every request:

Authorization (HTTP header)
Authorization: Bearer ub_live_YOUR_KEY

A website key may also go in the address, as ?sleutel=ub_web_... (sleutel means key). That is meant for a browser: this source can be reached from any website (CORS is open), so you can fetch directly from your own page. This only works for openbare-shows. A full key in the address is ignored; it must always go in the header and belongs on a server only.

The six sources

All addresses are GET and start with https://ultimate-bookings.com/api/v1. A full key needs the permission listed; every full key has Shows. Fields without a value are left out of the response instead of being null.

The sources of the API with the permission they need and their parameters
AddressPermissionParametersWhat you get
/showsShows (always on)van, tot, limiet, vanafAll shows, including options and cancelled ones
/agendaAgendavan, tot, limiet, vanafBlocks, travel and notes
/contactenContactslimiet, vanafYour address book, without notes
/artiestenArtistsnone (all artists at once)Your artists, without invoicing details
/facturenMoneyvan, tot, limiet, vanafSent invoices (no drafts)
/openbare-showsWebsite keylimiet (1 to 200, default 100), artiest (id of an artist, optional)Upcoming public shows, for your website

Parameters

A parameter that a source does not use is still checked for validity, but otherwise does nothing.

GET /api/v1/shows

Permission: Shows (always on). Parameters: van, tot, limiet, vanaf.

All shows of your organisation, sorted by start time, with every status (from enquiry to paid, and cancelled). Filter on the status field yourself if you only want confirmed shows. van and tot refer to the start time of the show.

Fields

id
titel
title
status
inquiry, quote, option, confirmed, performed, invoiced, paid, cancelled or no_show
soort
type
start
einde
end
hele_dag
all day
zaal
venue
stad
city
land
country code
artiest_id
artist id
artiest
name of the artist
openbaar
is Op je website tonen (show on your website) ticked for this show?

Only with the Money permission

gage
fee, in the currency of the munt field, with two decimals
munt
currency

GET /api/v1/agenda

Permission: Agenda. Parameters: van, tot, limiet, vanaf.

The calendar items that are not a show: blocks, travel and notes, sorted by start time. van and tot refer to the start time.

Fields

id
soort
blokkade (block), reis (travel) or notitie (note)
reis_soort
type of travel
titel
title
start
einde
end
hele_dag
all day
plaats
place
artiest_id
artist id
show_id
the show this belongs to, if there is one

GET /api/v1/contacten

Permission: Contacts. Parameters: limiet, vanaf.

Your address book, sorted by name. Anonymised contacts are left out. van and tot do nothing here.

Fields

id
naam
name
bedrijf
company
soort
type
email
email_2
second email address
telefoon
phone
straat
street
postcode
postcode
stad
city
land
country code
btw_nummer
VAT number

Notes, travel profile, allergies and the unsubscribe key are deliberately left out.

GET /api/v1/artiesten

Permission: Artists. Parameters: none (all artists at once).

All your artists, sorted by name, in one go. There is no pagination here: limiet, vanaf, van and tot do nothing.

Fields

id
naam
name
soort
type
kleur
colour
actief
active
thuisstad
home city
thuisland
home country

IBAN, Dutch Chamber of Commerce (KvK) number and VAT number are deliberately left out.

GET /api/v1/facturen

Permission: Money. Parameters: van, tot, limiet, vanaf.

Your invoices, sorted by invoice date, without drafts. van and tot refer to the invoice date here (both days count). Amounts are in whole euros with two decimals, not in cents.

Fields

id
nummer
invoice number
status
soort
type
factuurdatum
invoice date
vervaldatum
due date
betaald_op
paid on
excl_btw
amount excluding VAT
btw
VAT
totaal
total
munt
currency
btw_verlegd
VAT reverse-charged
artiest_id
artist id
show_id
klant
company name of the contact
contact
name of the contact

GET /api/v1/openbare-shows

Permission: Website key. Parameters: limiet (1 to 200, default 100), artiest (id of an artist, optional).

Only shows where you have ticked "Op je website tonen" (show on your website), that are confirmed (or already performed, invoiced or paid) and that take place today or later, by date. This is the only source for a website key, and the only one a website key may read. If each artist has their own site, pass artiest with the artiest_id from the response: you then get only that artist's shows. van, tot and vanaf do nothing here.

Fields

datum
date, YYYY-MM-DD, in Amsterdam time
start
missing for a show that lasts all day
zaal
venue
stad
city
land
country code
artiest
name of the artist
artiest_id
for the artiest filter

Responses, errors and limits

A successful request returns a data list and a meta block. aantal (count) is the number of rows in this response, limiet and vanaf are the values we used.

Response of GET /shows (JSON)
{
  "data": [
    {
      "id": "0b6f1c1e-...",
      "titel": "Zaterdagavond Paradiso",
      "status": "confirmed",
      "soort": "show",
      "start": "2026-10-10T20:00:00+00:00",
      "hele_dag": false,
      "zaal": "Paradiso",
      "stad": "Amsterdam",
      "land": "NL",
      "artiest_id": "5d0c9a52-...",
      "artiest": "Afro Bros",
      "openbaar": true
    }
  ],
  "meta": { "aantal": 1, "limiet": 50, "vanaf": 0 }
}

On an error you get an error block with a fixed code (which you can check against) and a message (readable, in Dutch, and it may change):

Example of an error (JSON)
{
  "error": {
    "code": "geen_recht",
    "message": "Deze sleutel mag dit onderdeel niet lezen. Zet het vinkje aan bij Instellingen, API-sleutels."
  }
}
Error codes with HTTP status and meaning
StatusCodeMeaning
400ongeldige_datumvan or tot is not a date like 2026-10-01 (year 1900 to 2100), or van is after tot.
400ongeldige_limietlimiet is not a whole number from 1 to 500 (200 for openbare-shows).
400ongeldige_artiestartiest is not the id of an artist.
400ongeldige_vanafvanaf is not a whole number of 0 or more.
401sleutel_ontbreektNo key was sent.
401sleutel_ongeldigThe key does not exist, is paused or has been deleted.
402geen_api_in_pakketA full key only works with the API in your plan (see above).
403geen_rechtThe key does not have this permission, or is the wrong type for this source.
403geen_pakketThe organisation has no active plan or has been closed.
404onbekende_bronThis address after /api/v1/ does not exist.
429te_veel_verzoekenYou are over the limit. The Retry-After header says how many seconds to wait.
500storingSomething went wrong on our side. Try again shortly.

Limits

Examples

Fetching October's shows with curl

Full key, 50 shows at a time, from the first.

curl (shell)
curl -H "Authorization: Bearer ub_live_YOUR_KEY" \
  "https://ultimate-bookings.com/api/v1/shows?van=2026-10-01&tot=2026-10-31&limiet=50&vanaf=0"

Upcoming shows on your website

With a website key. This fills a list on your page and inserts the data as text, not as HTML, so odd characters in a venue name cannot break anything. Want to try it first:

curl with a website key (shell)
curl "https://ultimate-bookings.com/api/v1/openbare-shows?limiet=10&sleutel=ub_web_YOUR_KEY"
List on a website (HTML and JavaScript)
<ul id="ub-shows"></ul>
<script>
  var KEY = "ub_web_YOUR_KEY";
  var ENDPOINT = "https://ultimate-bookings.com/api/v1/openbare-shows?limiet=10&sleutel=" + KEY;
  var list = document.getElementById("ub-shows");

  fetch(ENDPOINT)
    .then(function (response) {
      if (!response.ok) throw new Error("HTTP " + response.status);
      return response.json();
    })
    .then(function (result) {
      if (result.data.length === 0) {
        list.textContent = "No shows announced yet.";
        return;
      }
      result.data.forEach(function (show) {
        // datum is a plain day (YYYY-MM-DD); read it in UTC so it never shifts by a day.
        var date = new Date(show.datum + "T12:00:00Z").toLocaleDateString("en-GB", {
          day: "numeric", month: "long", year: "numeric", timeZone: "UTC"
        });
        var place = [show.zaal, show.stad].filter(Boolean).join(", ");
        var item = document.createElement("li");
        item.textContent = date + ": " + place + (show.artiest ? " (" + show.artiest + ")" : "");
        list.appendChild(item);
      });
    })
    .catch(function () {
      list.textContent = "The list of shows could not be loaded.";
    });
</script>

Fetching everything with pagination

Request one page at a time and increase vanaf by limiet, until a response has fewer rows than the limit. The order is fixed (by start time and then id), so a page never comes back twice or gets skipped as long as nothing changes while you fetch. This works for shows, agenda, contacten and facturen (invoices).

Pagination with waiting on 429 (JavaScript)
// Node 18 or newer. Fetches all shows, 500 at a time.
const KEY = process.env.UB_KEY; // ub_live_...
const LIMIT = 500;

async function fetchAllShows() {
  const all = [];
  let offset = 0;

  while (true) {
    const url = "https://ultimate-bookings.com/api/v1/shows?limiet=" + LIMIT + "&vanaf=" + offset;
    const response = await fetch(url, { headers: { Authorization: "Bearer " + KEY } });

    if (response.status === 429) {
      const wait = Number(response.headers.get("Retry-After")) || 60;
      await new Promise((done) => setTimeout(done, wait * 1000));
      continue; // the same page once more
    }
    if (!response.ok) {
      const { error } = await response.json();
      throw new Error(error.code + ": " + error.message);
    }

    const { data, meta } = await response.json();
    all.push(...data);
    if (meta.aantal < LIMIT) return all; // last page
    offset += LIMIT;
  }
}

What it can't do (yet)