Data & API

REST API

Read widgets, stored bookings and funnel analytics from the BookingsXP REST API with a Bearer key. Parameters, cursor pagination, errors and examples.

The REST API gives read access to your widgets, the bookings BookingsXP has stored, and the analytics behind the dashboard. Use it to sync bookings into a data warehouse, build a report, or reconcile with your CRM. It is on the Business plan.

For real-time delivery of each new booking, use webhooks instead; the API is for pulling data on a schedule.

Authentication#

Create a key in the dashboard under Integrations → API keys. Keys start with bxp_live_ and are shown once; BookingsXP keeps only a SHA-256 hash of each key. Revoke a key there at any time.

Send the key as a Bearer token:

Shell
curl https://bookingsxp.com/api/v1/widgets \
  -H "Authorization: Bearer bxp_live_…"

Keys give access to every widget and stored booking in your organization, including customer contact details. Call the API from your server only and never put a key in browser code.

Base URL and format#

Text
https://bookingsxp.com/api/v1

All endpoints are GET, return JSON, and are not cached. Dates are ISO 8601 strings in UTC.

EndpointReturns
GET /widgetsYour widgets
GET /bookingsStored bookings, newest first, paginated
GET /analyticsTotals, a daily series, the funnel, and channel, source and campaign breakdowns

List widgets#

HTTP
GET /api/v1/widgets
Authorization: Bearer bxp_live_…
JSON
{
  "data": [
    {
      "id": "w_8fk2m1qz",
      "name": "Website – main",
      "bookingUrl": "https://outlook.office.com/book/ContosoPhysio@contoso.com/",
      "template": "classic",
      "status": "active",
      "collectData": true,
      "businessName": "Contoso Physio",
      "createdAt": "2026-08-14T09:30:12.000Z"
    }
  ]
}

status is active or paused. collectData is the Store bookings switch: only widgets with it on have bookings in the next endpoint. Widgets are listed newest first.

List bookings#

HTTP
GET /api/v1/bookings?since=2026-09-01T00:00:00Z&widget=w_8fk2m1qz&limit=100
Authorization: Bearer bxp_live_…

BookingsXP stores a booking only when the widget had Store bookings on at the time; otherwise the booking exists only in Microsoft Bookings and does not appear here. Stored bookings are deleted after your plan's retention period (24 months on Business).

Query parameters#

ParameterTypeDefaultDescription
sinceISO datenoneBookings created at or after this time.
untilISO datenoneBookings created before this time.
widgetWidget IDall widgetsOnly bookings from this widget. 404 if it is not yours.
limit1 to 500100Page size. Values above 500 are treated as 500.
cursorISO datenoneThe nextCursor from the previous page. When set, it replaces until.

Filters apply to when the booking was made (createdAt), not to the appointment time.

Response#

JSON
{
  "data": [
    {
      "reference": "BXP-7K3M9Q",
      "status": "confirmed",
      "createdAt": "2026-09-24T10:15:31.902Z",
      "start": "2026-10-02T14:00:00.000Z",
      "end": "2026-10-02T14:45:00.000Z",
      "timeZone": "Europe/London",
      "service": { "id": "a1b2c3", "name": "Initial consultation" },
      "staff": ["Sam Lee"],
      "customer": {
        "name": "Alex Morgan",
        "email": "alex@example.com",
        "phone": "+44 20 7946 0000",
        "notes": "Knee injury from running."
      },
      "answers": [
        { "questionId": "q1", "question": "Is this your first visit?", "answer": "Yes" }
      ],
      "attribution": {
        "source": "google",
        "medium": "cpc",
        "campaign": "brand",
        "channel": "Paid search",
        "pageUrl": "https://www.example.com/book",
        "landingPage": "/pricing?utm_source=google&utm_medium=cpc&utm_campaign=brand&gclid=Cj0KCQ…",
        "referrer": "https://www.google.com/",
        "utmSource": "google",
        "utmMedium": "cpc",
        "utmCampaign": "brand",
        "utmTerm": "physio near me",
        "gclid": "Cj0KCQ…",
        "gaClientId": "1234567890.1727172000",
        "firstSeenAt": "2026-09-22T08:02:11.000Z",
        "currentPage": "https://www.example.com/book",
        "touches": 2
      },
      "manageUrl": "https://outlook.office.com/book/…"
    }
  ],
  "nextCursor": "2026-09-24T10:15:31.902Z"
}
FieldNotes
referenceThe BXP-XXXXXX reference, also in the booking notes in Microsoft Bookings.
start, endAppointment time in UTC; timeZone is the visitor's time zone.
staffStaff names as an array of strings.
phone, notesnull when the visitor left them empty.
attributionThe classified source, medium, campaign, channel and pageUrl, plus the raw fields the loader collected: landingPage, referrer, utmSource, utmMedium, utmCampaign, utmTerm, utmContent, click IDs (gclid, gbraid, wbraid, fbclid, msclkid, liFatId, ttclid), gaClientId, fbp, fbc, firstSeenAt, currentPage and touches. Only fields that were present are included.
manageUrlMicrosoft's link to manage the booking, when available.

Pagination#

Results are newest first. When there are more, nextCursor holds the creation time of the last booking on the page; pass it back as cursor to get the next page. nextCursor is null on the last page.

JavaScript
const BASE = "https://bookingsxp.com/api/v1";
const headers = { Authorization: `Bearer ${process.env.BOOKINGSXP_API_KEY}` };

async function allBookingsSince(since) {
  const out = [];
  let cursor = null;
  do {
    const url = new URL(`${BASE}/bookings`);
    url.searchParams.set("since", since);
    url.searchParams.set("limit", "500");
    if (cursor) url.searchParams.set("cursor", cursor);
    const res = await fetch(url, { headers });
    if (!res.ok) {
      const { error } = await res.json();
      throw new Error(`${res.status} ${error.code}: ${error.message}`);
    }
    const page = await res.json();
    out.push(...page.data);
    cursor = page.nextCursor;
  } while (cursor);
  return out;
}

const bookings = await allBookingsSince("2026-09-01T00:00:00Z");
console.log(`${bookings.length} bookings`);

For an incremental sync, store the newest createdAt you have seen and pass it as since next time. since is inclusive, so the booking you saw last is returned again: upsert by reference.

Analytics#

HTTP
GET /api/v1/analytics?days=30&widget=w_8fk2m1qz
Authorization: Bearer bxp_live_…
ParameterTypeDefaultDescription
days1 to 73030Look-back window ending now.
widgetWidget IDall widgetsOnly this widget. 404 if it is not yours.

Analytics come from the anonymous funnel events every saved widget records, so they do not need Store bookings. Days are counted in your organization's time zone (set in the dashboard; UTC if not set).

JSON
{
  "days": 30,
  "timeZone": "Europe/London",
  "totals": {
    "views": 1840,
    "slotSessions": 412,
    "formSessions": 236,
    "bookings": 97,
    "failed": 2,
    "noAvailability": 15,
    "conversionRate": 0.0527,
    "avgLeadDays": 6.4
  },
  "daily": [
    { "day": "2026-08-26", "views": 58, "bookings": 3 },
    { "day": "2026-08-27", "views": 71, "bookings": 4 }
  ],
  "funnel": {
    "widget_viewed": 1840,
    "service_selected": 903,
    "date_selected": 655,
    "slot_selected": 412,
    "form_started": 236,
    "booking_completed": 97
  },
  "channels": [
    { "key": "Paid search", "views": 620, "bookings": 41, "conversionRate": 0.0661 }
  ],
  "sources": [
    { "key": "google / cpc", "views": 590, "bookings": 39, "conversionRate": 0.0661 }
  ],
  "campaigns": [
    { "key": "brand", "views": 310, "bookings": 27, "conversionRate": 0.0871 }
  ]
}

(Numbers are illustrative.)

FieldNotes
totals.viewsWidget loads (visits that saw the widget), not page views.
totals.slotSessions, formSessionsVisits that picked a time, and visits that started the form.
totals.bookings, failedCompleted and failed bookings.
totals.noAvailabilityVisits that found no free times.
totals.conversionRatebookings / views, from 0 to 1.
totals.avgLeadDaysAverage days between booking and appointment, or null.
dailyOne entry per day in the window, including days with zero.
funnelVisits that reached each step or a later one. A visit that picked a time counts as having picked a day, because the widget opens on the first free day.
channelsUp to 12 channels, most bookings first.
sourcesUp to 25 source / medium pairs.
campaignsUp to 25 campaigns; (not set) for traffic without one.

Funnel events are kept for your plan's analytics period (24 months on Business), so days beyond what has been kept returns what is left.

Errors#

Errors use a standard shape with an HTTP status:

JSON
{ "error": { "code": "plan_required", "message": "The REST API is on the Business plan." } }
StatuscodeCause
400bad_requestA parameter is invalid, for example since is not an ISO date.
401unauthorizedNo Authorization: Bearer bxp_live_… header, or the key is unknown or revoked.
403plan_requiredThe organization is not on the Business plan.
404not_foundThe widget parameter does not match one of your widgets.
500internalSomething failed on our side. Retry with backoff; write to hello@bookingsxp.com (opens in a new tab) if it persists.

Edit or question? hello@bookingsxp.com