Prerequisites
Use of endpoints mentioned in this article requires a Partner API Key; to obtain one, contact your Booxi representative.
Register a webhook callback with Booxi; contact your Booxi representative to do so.
Notes
Your Partner API Key must be used exclusively in server-side (backend) code to keep it confidential. Never expose it in client-side JavaScript.
API request URLs depend on your hosting region. All examples below use North American hosting (api.booxi.com). Replace with api.booxi.eu if you are hosted in Europe.
Before you begin, review Booxi's API Fair Use Policy.
API documentation:
Overview
By default, Booxi provides clients with a hosted tracking page that lets them view, cancel, or reschedule their appointments. However, if you need a fully branded, custom experience integrated into your own platform, you can build your own tracking page using the Booxi API and webhooks.
This article walks you through every step:
Architecture at a glance
Step 1 — Capture Booking Identifiers via Webhook
When a booking is confirmed in Booxi, a webhook event is dispatched to your registered endpoint. Two fields from this payload are essential for your tracking page:
Field | Description |
booking.confirmationNumber | The unique booking reference, used as the bookingId when calling Booxi APIs |
booking.client.trackingPage.viewURL | The URL of Booxi's native tracking page, which contains the tracking code |
Extracting the tracking code
The viewURL follows this pattern:
https://book.booxi.com/tracking/TRACKING_CODE
Extract the tracking code by splitting the URL on tracking/ and taking the last segment:
// Node.js example
function extractTrackingCode(viewURL) {
const parts = viewURL.split("tracking/");
if (parts.length < 2) throw new Error("Invalid tracking URL format");
return parts[1];
}
Sample webhook payload (relevant fields)
{
"event": "booking.confirmed",
"booking": {
"confirmationNumber": "A00006500",
"status": "Approved",
"client": {
"firstName": "Jane",
"lastName": "Doe",
"email": "jane.doe@example.com",
"trackingPage": {
"viewURL": "https://book.booxi.com/tracking/abc123XYZ"
}
}
}
}Webhook handler (Express.js example)
app.post("/webhooks/booxi", async (req, res) => {
const { event, booking } = req.body;
if (event === "booking.confirmed") {
const bookingId = booking.confirmationNumber;
const trackingCode = extractTrackingCode(booking.client.trackingPage.viewURL);
await saveBookingReference(bookingId, trackingCode); // see Step 2
}
res.sendStatus(200);
});Tip: Always respond with a 200 status quickly, and perform heavy processing asynchronously, to avoid webhook timeouts.
Step 2 — Store the Data in Your Database
Persist both identifiers so your tracking page can later resolve a visitor's tracking code to the Booxi bookingId needed for API calls.
Recommended schema
CREATE TABLE booking_tracking (
id SERIAL PRIMARY KEY,
booking_id VARCHAR(64) NOT NULL UNIQUE, -- Booxi confirmationNumber
tracking_code VARCHAR(128) NOT NULL UNIQUE, -- extracted from viewURL
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_tracking_code ON booking_tracking (tracking_code);
Save function (Node.js + PostgreSQL)
async function saveBookingReference(bookingId, trackingCode) {
await db.query(
`INSERT INTO booking_tracking (booking_id, tracking_code)
VALUES ($1, $2)
ON CONFLICT (booking_id) DO UPDATE SET
tracking_code = EXCLUDED.tracking_code,
updated_at = NOW()`,
[bookingId, trackingCode]
);
}Note: Use ON CONFLICT … DO UPDATE (or your DB's equivalent upsert) so that if a booking is modified and a new tracking code is issued, your table stays in sync. Subscribe to booking.modified and booking.cancelled webhook events to update or remove records accordingly.
Step 3 — Build a Backend Proxy Layer
All Booxi API calls that require your Partner API Key must go through your backend. Your frontend should never talk to the Booxi API directly.
Expose three internal endpoints from your server:
Your endpoint | Method | Purpose |
/api/tracking/:code | GET | Resolve tracking code → fetch booking details |
/api/tracking/:code/cancel | POST | Cancel the booking |
/api/tracking/:code/modify | POST | Modify the booking |
Resolve tracking code to booking details
const BOOXI_API_BASE = "https://api.booxi.com/booking/v1";
const BOOXI_API_KEY = process.env.BOOXI_PARTNER_API_KEY; // never expose this
app.get("/api/tracking/:code", async (req, res) => {
try {
const row = await db.query(
"SELECT booking_id FROM booking_tracking WHERE tracking_code = $1",
[req.params.code]
);
if (!row) return res.status(404).json({ error: "Booking not found" });
const bookingId = row.booking_id;
const booking = await fetchBookingFromBooxi(bookingId);
res.json(booking);
} catch (err) {
console.error(err);
res.status(500).json({ error: "Internal server error" });
}
});
Step 4 — Fetch and Display the Booking Details
The frontend only ever knows the tracking code — extracted from the URL slug of your landing page (e.g., /track/wrFQzZecN1zLZLLchJeKOcEO9UziArM9XWrR). It calls your backend with that code, and your backend is responsible for resolving it to a bookingId and querying Booxi. The bookingId never leaves the server.
Frontend — read the tracking code from the URL and request details
// e.g. your tracking page lives at /track/:code
const trackingCode = window.location.pathname.split("/track/")[1];
async function loadBooking() {
const res = await fetch(`/api/tracking/${trackingCode}`);
if (!res.ok) {
showErrorMessage("Booking not found or no longer available.");
return;
}
const booking = await res.json();
renderBookingCard(booking); // see "What to display" below
}
loadBooking();
Backend — resolve tracking code → bookingId → Booxi API
Your backend endpoint (already introduced in Step 3) handles the full resolution chain. The frontend receives only the data needed to render the page — the bookingId is never included in the response.
app.get("/api/tracking/:code", async (req, res) => {
try {
// 1. Resolve the tracking code to an internal bookingId
const row = await db.query(
"SELECT booking_id FROM booking_tracking WHERE tracking_code = $1",
[req.params.code]
);
if (!row) return res.status(404).json({ error: "Booking not found" });
// 2. Use the bookingId server-side to call the Booxi API
const booking = await fetchBookingFromBooxi(row.booking_id);
// 3. Return only the data the frontend needs — never expose bookingId
res.json({
trackingCode: req.params.code, // needed client-side for BookNow.openModify()
status: booking.status,
isCompleted: booking.isCompleted,
startsOn: booking.totalClientTimespan.start,
endsOn: booking.totalClientTimespan.end,
duration: booking.totalClientTimespan.duration,
serviceName: booking.items[0]?.serviceName,
client: booking.client
});
} catch (err) {
console.error(err);
res.status(500).json({ error: "Internal server error" });
}
});Booxi API call — GET /booking/{bookingId}
This call happens exclusively on your backend.
Request URL
GET https://api.booxi.com/booking/v1/booking/{bookingId}
cURL
curl -X GET \
"https://api.booxi.com/booking/v1/booking/A00006500" \
-H "accept: application/json" \
-H "x-api-key: YOUR_PARTNER_API_KEY"
API reference:
Region | Link |
North America | |
Europe |
Helper function
async function fetchBookingFromBooxi(bookingId) {
const response = await fetch(`${BOOXI_API_BASE}/booking/${bookingId}`, {
headers: {
"accept": "application/json",
"x-api-key": BOOXI_API_KEY
}
});
if (!response.ok) throw new Error(`Booxi API error: ${response.status}`);
return response.json();
}Sample response
{
"bookingId": "A00006500",
"merchantId": 1002,
"bookingMethod": "Appointment",
"status": "Approved",
"isCompleted": false,
"startsOn": "2025-04-10T14:00:00Z",
"totalClientTimespan": {
"start": "2025-04-10T14:00:00Z",
"end": "2025-04-10T14:30:00Z",
"duration": 30
},
"staffId": 42,
"items": [
{
"serviceId": 100,
"serviceName": "Haircut",
"duration": 30
}
],
"client": {
"firstName": "Jane",
"lastName": "Doe",
"email": "jane.doe@example.com"
}
}What to display
From the response, render a confirmation-style summary card containing at minimum:
Service name (items[0].serviceName)
Date and time (totalClientTimespan.start, formatted for the user's locale)
Duration (totalClientTimespan.duration)
Status badge (status: Approved, Cancelled, etc.)
Client name as a personalized greeting
CTA buttons: Cancel and Modify (conditionally hidden if status is Cancelled or isCompleted is true)
Step 5 — Implement the Cancel Action
Cancellation is the simplest action: show a confirmation dialog, then call your backend which forwards a PUT /booking/{bookingId} to Booxi with the status set to Cancelled.
Backend endpoint
app.post("/api/tracking/:code/cancel", async (req, res) => {
try {
const row = await db.query(
"SELECT booking_id FROM booking_tracking WHERE tracking_code = $1",
[req.params.code]
);
if (!row) return res.status(404).json({ error: "Booking not found" });
const result = await cancelBookingInBooxi(row.booking_id);
res.json(result);
} catch (err) {
console.error(err);
res.status(500).json({ error: "Failed to cancel booking" });
}
});Booxi API call — PUT /booking/{bookingId}
Request URL
PUT https://api.booxi.com/booking/v1/booking/{bookingId}
cURL
curl -X PUT \
"https://api.booxi.com/booking/v1/booking/A00006500" \
-H "accept: application/json" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_PARTNER_API_KEY" \
-d '{ "status": "Cancelled" }'
API reference:
Region | Link |
North America | |
Europe |
Helper function
async function cancelBookingInBooxi(bookingId) {
const response = await fetch(`${BOOXI_API_BASE}/booking/${bookingId}`, {
method: "PUT",
headers: {
"accept": "application/json",
"Content-Type": "application/json",
"x-api-key": BOOXI_API_KEY
},
body: JSON.stringify({ status: "Cancelled" })
});
if (!response.ok) throw new Error(`Booxi cancel error: ${response.status}`);
return response.json();
}Frontend confirmation dialog
async function handleCancel(trackingCode) {
const confirmed = window.confirm(
"Are you sure you want to cancel your appointment? This action cannot be undone."
);
if (!confirmed) return;
const res = await fetch(`/api/tracking/${trackingCode}/cancel`, {
method: "POST"
});
if (res.ok) {
showSuccessMessage("Your appointment has been cancelled.");
refreshBookingStatus(); // re-fetch and update the UI
} else {
showErrorMessage("We couldn't cancel your appointment. Please try again.");
}
}UX recommendation: Replace window.confirm with a styled modal dialog for a polished experience. Disable the Cancel button immediately after the click to prevent double submissions.
Step 6 — Implement the Modify Action
There are two approaches to modification; choose the one that best fits your product.
Option A — Open the Booxi Widget in Modify Mode (Recommended for quick setup)
The Booxi booking widget supports a modify mode that pre-populates the existing booking. This is the fastest path to a working modify flow with full validation handled by Booxi.
Make sure the Booxi widget script is loaded on your page, then call BookNow.openModify() with the trackingNumber you extracted from the webhook and stored in your database (see Step 1 and Step 2):
function handleModify(trackingCode) {
BookNow.openModify({ trackingNumber: trackingCode });
}The trackingNumber is the code you extracted from booking.client.trackingPage.viewURL — the segment after tracking/. For example, if the URL was:
https://book.booxi.com/tracking/wrFQzZecN1zLZLLchJeKOcEO9UziArM9XWrR
Then your call would be:
BookNow.openModify({ trackingNumber: 'wrFQzZecN1zLZLLchJeKOcEO9UziArM9XWrR' });
Your tracking page fetches this value from your backend (which resolved it from the URL slug), so the frontend simply passes it through:
// Fetch tracking data from your backend (includes the tracking code)
const { trackingCode, booking } = await fetch(`/api/tracking/${slug}`).then(r => r.json());
// Wire up the Modify button
document.getElementById("btn-modify").addEventListener("click", () => {
BookNow.openModify({ trackingNumber: trackingCode });
});
Security note: BookNow.openModify() uses only the tracking code — no Partner API Key is involved on the client side.
When the client completes the modification, Booxi will fire a new webhook event (booking.modified). Your webhook handler should then update your database with any new tracking code issued for the modified booking.
Option B — Custom Modification Flow Using the API
For a fully custom, in-page modification experience without opening the widget, you can use the Booxi APIs directly. This approach mirrors the flow described in this article.
Step 6B-1 — Fetch available services (if allowing service change)
GET https://api.booxi.com/booking/v1/service?bookingMethod=Appointment
Step 6B-2 — Fetch updated availability for the (new or existing) service
GET https://api.booxi.com/booking/v1/service/{serviceId}/availability?from=START&to=ENDStep 6B-3 — Apply the modification using PUT /booking/{bookingId}
curl -X PUT \
"https://api.booxi.com/booking/v1/booking/A00006500" \
-H "accept: application/json" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_PARTNER_API_KEY" \
-d '{
"startsOn": "2025-04-12T10:00:00Z",
"staffId": 42,
"items": [{ "serviceId": 100 }]
}'
Your backend endpoint:
app.post("/api/tracking/:code/modify", async (req, res) => {
try {
const row = await db.query(
"SELECT booking_id FROM booking_tracking WHERE tracking_code = $1",
[req.params.code]
);
if (!row) return res.status(404).json({ error: "Booking not found" });
const { startsOn, staffId, serviceId } = req.body;
const response = await fetch(`${BOOXI_API_BASE}/booking/${row.booking_id}`, {
method: "PUT",
headers: {
"accept": "application/json",
"Content-Type": "application/json",
"x-api-key": BOOXI_API_KEY
},
body: JSON.stringify({
startsOn,
staffId,
items: [{ serviceId }]
})
});
if (!response.ok) throw new Error(`Booxi modify error: ${response.status}`);
const updated = await response.json();
res.json(updated);
} catch (err) {
console.error(err);
res.status(500).json({ error: "Failed to modify booking" });
}
});Recommendation: Option A (widget in modify mode) is strongly recommended for most use cases. It handles edge cases such as staff unavailability, service rule validation, and payment adjustments automatically. Choose Option B only if you need a deeply embedded, fully custom UI with no widget dependency.
Keeping Your Data in Sync with Webhooks
Subscribe to all booking lifecycle events to keep your database accurate:
Webhook Event | Action |
booking.confirmed | Insert new booking_id + tracking_code |
booking.modified | Upsert with potentially new tracking_code |
booking.cancelled | Mark as cancelled or delete the record |
This ensures your tracking page always reflects the true state of the appointment, even if the booking is modified from the Booxi back office directly.
Summary
Here is a recap of everything you built:
Step | What you did |
1 | Listened to Booxi webhooks and extracted confirmationNumber + tracking code |
2 | Persisted both identifiers in your database |
3 | Built a secure backend proxy so your Partner API Key never reaches the client |
4 | Called GET /booking/{bookingId} to render a rich booking summary |
5 | Called PUT /booking/{bookingId} with status: "Cancelled" behind a confirmation step |
6 | Offered modification via the Booxi widget in modify mode, or a fully custom API-driven flow |
With this setup, you have complete control over the design, language, and user experience of your appointment tracking page, while Booxi remains the authoritative source of truth for all booking data.
