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:
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#
https://bookingsxp.com/api/v1All endpoints are GET, return JSON, and are not cached. Dates are ISO 8601 strings in UTC.
| Endpoint | Returns |
|---|---|
GET /widgets | Your widgets |
GET /bookings | Stored bookings, newest first, paginated |
GET /analytics | Totals, a daily series, the funnel, and channel, source and campaign breakdowns |
List widgets#
GET /api/v1/widgets
Authorization: Bearer bxp_live_…{
"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#
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#
| Parameter | Type | Default | Description |
|---|---|---|---|
since | ISO date | none | Bookings created at or after this time. |
until | ISO date | none | Bookings created before this time. |
widget | Widget ID | all widgets | Only bookings from this widget. 404 if it is not yours. |
limit | 1 to 500 | 100 | Page size. Values above 500 are treated as 500. |
cursor | ISO date | none | The 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#
{
"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"
}| Field | Notes |
|---|---|
reference | The BXP-XXXXXX reference, also in the booking notes in Microsoft Bookings. |
start, end | Appointment time in UTC; timeZone is the visitor's time zone. |
staff | Staff names as an array of strings. |
phone, notes | null when the visitor left them empty. |
attribution | The 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. |
manageUrl | Microsoft'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.
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#
GET /api/v1/analytics?days=30&widget=w_8fk2m1qz
Authorization: Bearer bxp_live_…| Parameter | Type | Default | Description |
|---|---|---|---|
days | 1 to 730 | 30 | Look-back window ending now. |
widget | Widget ID | all widgets | Only 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).
{
"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.)
| Field | Notes |
|---|---|
totals.views | Widget loads (visits that saw the widget), not page views. |
totals.slotSessions, formSessions | Visits that picked a time, and visits that started the form. |
totals.bookings, failed | Completed and failed bookings. |
totals.noAvailability | Visits that found no free times. |
totals.conversionRate | bookings / views, from 0 to 1. |
totals.avgLeadDays | Average days between booking and appointment, or null. |
daily | One entry per day in the window, including days with zero. |
funnel | Visits 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. |
channels | Up to 12 channels, most bookings first. |
sources | Up to 25 source / medium pairs. |
campaigns | Up 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:
{ "error": { "code": "plan_required", "message": "The REST API is on the Business plan." } }| Status | code | Cause |
|---|---|---|
400 | bad_request | A parameter is invalid, for example since is not an ISO date. |
401 | unauthorized | No Authorization: Bearer bxp_live_… header, or the key is unknown or revoked. |
403 | plan_required | The organization is not on the Business plan. |
404 | not_found | The widget parameter does not match one of your widgets. |
500 | internal | Something failed on our side. Retry with backoff; write to hello@bookingsxp.com (opens in a new tab) if it persists. |
Related#
- Webhooks for push delivery of each booking.
- Privacy & data for what is stored and how to erase it.
- Pricing for plan limits.
Edit or question? hello@bookingsxp.com