> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zenamu.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Build a public schedule

> Render a studio's classes and workshops on your own website.

This guide builds a month view showing classes and workshops together, with
lecturer names, locations, prices, and live availability.

Everything here uses the [public API key](/api/authentication#public-api-key),
so it can run in a browser.

## 1. Fetch reference data once

Lecturers and places change rarely, and the schedule references them by ID.
Fetch both up front and build lookup maps:

```js theme={null}
const headers = { Authorization: `Bearer ${PUBLIC_KEY}` };
const base = "https://api.zenamu.com";

const [lecturers, places] = await Promise.all([
  fetch(`${base}/v1/lecturers?includeInactive=true`, { headers })
    .then((r) => r.json())
    .then((r) => new Map(r.data.map((l) => [l._id, l]))),
  fetch(`${base}/v1/places`, { headers })
    .then((r) => r.json())
    .then((r) => new Map(r.data.map((p) => [p._id, p]))),
]);
```

Pass `includeInactive=true` so lecturers who have since left the studio still
resolve on older events.

<Tip>
  Cache these. Refetching them per event will exhaust your
  [rate limit](/api/rate-limits) on a busy month.
</Tip>

## 2. Fetch the schedule

Classes and workshops are separate endpoints with the same date range. Ask for
capacity only if you display availability.

```js theme={null}
const params = new URLSearchParams({
  dateFrom: "2026-03-01",
  dateTo: "2026-03-31",
  includeCapacity: "true",
});

const [classes, workshops] = await Promise.all([
  fetch(`${base}/v1/classes?${params}`, { headers }).then((r) => r.json()),
  fetch(`${base}/v1/workshops?${params}`, { headers }).then((r) => r.json()),
]);
```

Both ranges are read in the **studio's** time zone, not the visitor's. See
[Dates and time zones](/api/dates).

## 3. Merge the two lists

The two shapes differ in three places. Normalize them before rendering:

| Concept     | Class                               | Workshop           |
| ----------- | ----------------------------------- | ------------------ |
| Description | `description`                       | `shortDescription` |
| Location    | `locationId` → place, or `isOnline` | `address` inline   |
| Free seats  | compute `max - reserved`            | `available`        |

```js theme={null}
const clamp = (n) => Math.max(0, n);

const events = [
  ...classes.data.map((c) => ({
    id: c._id,
    kind: c.eventType,            // "class" or "course_session"
    canceled: c.isCanceled,
    title: c.name,
    summary: c.description,
    startsAt: c.start,
    timezone: c.timezone,
    location: c.isOnline ? "Online" : places.get(c.locationId)?.name ?? null,
    lecturers: c.lecturerIds.map((id) => lecturers.get(id)?.name).filter(Boolean),
    seatsLeft: c.capacity ? clamp(c.capacity.max - c.capacity.reserved) : null,
    prices: c.pricingOptions,
    url: c.publicUrl,
  })),
  ...workshops.data.map((w) => ({
    id: w._id,
    kind: "workshop",
    title: w.name,
    summary: w.shortDescription,
    startsAt: w.start,
    timezone: w.timezone,
    location: [w.address?.street, w.address?.city].filter(Boolean).join(", ") || null,
    lecturers: w.lecturerIds.map((id) => lecturers.get(id)?.name).filter(Boolean),
    seatsLeft: w.capacity ? w.capacity.available : null,
    prices: w.pricingOptions,
    url: w.publicUrl,
  })),
].sort((a, b) => a.startsAt.localeCompare(b.startsAt));
```

An unresolved `locationId` means the place was archived. Fall back to showing no
location rather than treating it as an error.

## 4. Display times

Format the UTC `start` in the event's `timezone`. A studio in Prague announces a
class at 09:00, and that is what a visitor expects to read regardless of their
own time zone:

```js theme={null}
new Intl.DateTimeFormat([], {
  hour: "2-digit",
  minute: "2-digit",
  timeZone: event.timezone,
}).format(new Date(event.startsAt));
```

Keep the UTC `start` for sorting and storage. Do not parse `localTimeStart` and
then format it in the visitor's time zone.

## 5. Display prices

A single event can offer several ways to pay. `value` is only a money amount
when `currency` is present:

```js theme={null}
function label(option) {
  if (option.currency) return `${option.value} ${option.currency}`;
  if (option.type === "credit") return `${option.value} credits`;
  if (option.type === "pass") return `${option.value} entries`;
  return option.name ?? "Included";
}
```

Prefer the studio's own `name` where you have room for it. See
[Pricing option](/api/objects/pricing-option).

## 6. Link back to booking

The API is read-only — it does not create reservations. Send visitors to
`publicUrl` to book. For a course session that link points at the parent course,
not the single session.

## Course sessions

Classes and course sessions arrive in the same list. If you want a timetable of
individual occurrences, render both. If you want a catalog of courses, group by
`courseId` and show each course once:

```js theme={null}
const courses = new Map();
for (const e of classes.data) {
  if (e.eventType === "course_session" && !courses.has(e.courseId)) {
    courses.set(e.courseId, e);
  }
}
```
