Recipes
Worked examples for the pages most integrations need. All of them assume a small api() helper that adds the Authorization header — see Handling a rotated token without downtime for one.
A course catalogue page
One request. Group client-side by upcoming_class.type if you want in-person and online listings separated.
const { data: courses } = await api("/v2/public/courses");
A "find a class" search page
const params = new URLSearchParams({ limit: "25", page: String(page) });
if (query) params.set("search", query);
if (fromDate) params.set("start", format(fromDate, "MM-dd-yyyy"));
const { data, total, last_page } = await api(`/v2/public/classes?${params}`);
Build paging controls from last_page; show total as the result count.
A course detail page with its sittings
Two requests, safe to run in parallel:
const [course, classes] = await Promise.all([
api(`/v2/public/course/${id}`),
api(`/v2/public/course/${id}/classes?limit=50`),
]);
Rendering availability
Remember that null means uncapped, not sold out — see Seat counts.
function availability(cls) {
if (cls.available_seats === null)
return { label: "Open enrolment", canBook: true };
if (cls.available_seats === 0) return { label: "Class full", canBook: false };
if (cls.available_seats <= 3)
return { label: `Only ${cls.available_seats} seats left`, canBook: true };
return { label: `${cls.available_seats} seats available`, canBook: true };
}
Handling a rotated token without downtime
Read the token per request from a secret store or environment variable, and treat a 401 as a signal to re-read configuration once before failing:
async function api(path, { retried = false } = {}) {
const res = await fetch(`${API_BASE}${path}`, {
headers: { Authorization: `Bearer ${await getToken()}` },
});
if (res.status === 401 && !retried) {
await refreshTokenFromSecretStore();
return api(path, { retried: true });
}
if (!res.ok) throw new ApiError(res.status, await res.text());
return res.json();
}