# Bookings &amp; Calendars

URL: https://developers.uidu.org/docs/domains/bookings

> Public calendars, calendar events, and bookings (appointments).

* **Use it for:** [Public website](https://developers.uidu.org/docs/paths/public-website.md) · [Server integration](https://developers.uidu.org/docs/paths/server-integration.md)
* **Reads:** public token — works in the browser; Bearer for `getBooking`, `listBookings`
* **Writes:** none — read-only today
* **CLI:** `uidu bookings`
* **Auth:** what each token can do and where it may run — [Authentication](https://developers.uidu.org/docs/foundations/authentication.md)

Calendars and their events are the workspace's public schedule — opening hours, classes,
appointments on offer — and bookings are the reservations people made. Render the first on a
public site with the public token; read the second from your server.

## Mental model

```
Calendar            name, description, color ("#22c55e"), timeZone ("Europe/Rome")
  └─→ CalendarEvent name, startTime / endTime, allDay, timeZone, url,
                    primaryAddress, body (rich text — on getCalendarEvent only)

Booking             kind, number, status, currency, itemsCount, itemsTotal (cents),
                    paidAt / refundedAt / expiredAt / willExpireAt, contact,
                    fieldValuesByShortname (on getBooking only)
```

* **A `CalendarEvent` is lighter than an [`Event`](https://developers.uidu.org/docs/domains/events.md)**: no campaign, no
  attendance, no CMS body to build — a dated entry on a calendar. Use Events when people RSVP.
* **Two auth levels.** Calendars and calendar events are public reads: the public token works,
  including in the browser. Bookings are private data and need a Bearer (`apiKey`) client on the
  server — with a public token `listBookings` returns an empty list and `getBooking` returns
  `null`, no error. Nothing here writes: the domain is read-only today.
* **Times** (`startTime`, `endTime`) are ISO strings or `null`; an `allDay` event has no
  meaningful time of day. Each event and calendar carries its `timeZone` — format with it rather
  than the server's zone.
* **Money is in cents.** A booking's `itemsTotal` is an integer in the minor unit of its
  `currency`: divide by 100 to display it. `status` and `kind` are labels (strings); `number` is
  the human-readable booking number.
* **Filters.** `listCalendarEvents` narrows by `calendarIds` and by named `scopes` (e.g.
  `upcoming`). List items carry the event's `calendar` (`id`, `name`, `color`) and a short
  `primaryAddress` (`formattedAddress`, `city`); the full event, with `body` and the address's
  `name` and `countryCode`, comes from `getCalendarEvent`. `listCalendars` and `listBookings`
  page with `first`/`after` or `last`/`before`.

## Use cases

### Public calendar grid

Goal: a statically rendered page with a legend of calendars and the upcoming events grouped by
month.

```tsx
// app/calendar/page.tsx — Server Component
import { listCalendars, listCalendarEvents } from '@uidu/client';
import { uidu } from '@/lib/uidu'; // public token is enough

export default async function CalendarPage() {
  const [calendars, events] = await Promise.all([
    listCalendars(uidu, { first: 50 }),
    listCalendarEvents(uidu, { first: 100, scopes: ['upcoming'] }),
  ]);

  const byMonth = events.reduce<Record<string, typeof events>>((acc, e) => {
    if (!e.startTime) return acc;
    const month = e.startTime.slice(0, 7); // YYYY-MM
    (acc[month] ??= []).push(e);
    return acc;
  }, {});

  return (
    <div>
      <ul>
        {calendars.map((c) => (
          <li key={c.id} style={{ color: c.color ?? undefined }}>
            {c.name}
          </li>
        ))}
      </ul>
      {Object.entries(byMonth).map(([month, list]) => (
        <section key={month}>
          <h2>{month}</h2>
          <ul>
            {list.map((e) => (
              <li key={e.id} style={{ borderColor: e.calendar.color ?? undefined }}>
                <a href={`/calendar/${e.id}`}>{e.name}</a>{' '}
                {new Date(e.startTime!).toLocaleString(undefined, {
                  timeZone: e.timeZone ?? undefined,
                  dateStyle: 'medium',
                  timeStyle: e.allDay ? undefined : 'short',
                })}
                {e.primaryAddress?.city && ` · ${e.primaryAddress.city}`}
              </li>
            ))}
          </ul>
        </section>
      ))}
    </div>
  );
}
```

* To show one calendar only, pass `calendarIds: [calendarId]`; `getCalendar(uidu, { id })`
  returns its name, color and time zone for the page header.
* One page of events is usually enough for "upcoming"; for a full archive walk every page with
  `paginate` and `ListCalendarEventsDocument` — see
  [Pagination & field values](https://developers.uidu.org/docs/foundations/pagination-and-field-values.md).

### Calendar event page

Goal: the detail page each grid item links to, with the event's rich-text description.

```tsx
// app/calendar/[id]/page.tsx
import { notFound } from 'next/navigation';
import { getCalendarEvent } from '@uidu/client';
import { RichText } from '@uidu/react';
import { uidu } from '@/lib/uidu';

export default async function CalendarEventPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const event = await getCalendarEvent(uidu, { id });
  if (!event) notFound();

  return (
    <article>
      <p>{event.calendar.name}</p>
      <h1>{event.name}</h1>
      {event.primaryAddress && <p>{event.primaryAddress.formattedAddress}</p>}
      {event.body && <RichText doc={event.body} />} {/* Tiptap JSON */}
      {event.url && <a href={event.url}>More information</a>}
    </article>
  );
}
```

### Bookings report on the server

Goal: an internal page or export listing recent bookings with their customer and amount.

```ts
// app/admin/bookings/data.ts — server-only
import { listBookings, getBooking } from '@uidu/client';
import { uiduAdmin } from '@/lib/uidu-admin'; // createClient({ workspace, apiKey })

const money = (cents: number | null | undefined, currency: string | null | undefined) =>
  cents == null
    ? '—'
    : new Intl.NumberFormat(undefined, { style: 'currency', currency: currency ?? 'EUR' }).format(
        cents / 100, // itemsTotal is in cents
      );

export async function recentBookings() {
  const bookings = await listBookings(uiduAdmin, { first: 25 }); // [] with a public token

  return bookings.map((b) => ({
    number: b.number,
    kind: b.kind,
    status: b.status,
    paid: Boolean(b.paidAt),
    items: b.itemsCount,
    total: money(b.itemsTotal, b.currency),
    customer: b.contact?.name ?? b.contact?.email,
  }));
}

export async function bookingDetail(id: string) {
  const booking = await getBooking(uiduAdmin, { id }); // null with a public token
  return booking && {
    ...booking,
    answers: booking.fieldValuesByShortname, // custom fields, keyed by shortname
  };
}
```

* `getBooking` adds `paymentIntentId`, `refundedAt`, `updatedAt` and the custom-field map
  `fieldValuesByShortname` to what the list returns.
* From a terminal: `uidu bookings list --json`, `uidu bookings get <id> --json`.

## API at a glance

Every `@uidu/client` function in **Bookings & calendars**, generated from the source — the [full reference](https://developers.uidu.org/docs/reference/client.md#bookings) has the rest of the SDK.

| Function                                                                                             | Kind | Auth         | CLI             | Description                                                                                                                            |
| ---------------------------------------------------------------------------------------------------- | ---- | ------------ | --------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| [`getBooking`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/bookings.ts)         | read | Bearer       | `bookings get`  | One booking with its contact, totals in cents and custom fields (`fieldValuesByShortname`), or null; a public token sees none.         |
| [`getCalendar`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/bookings.ts)        | read | public token | —               | One calendar (name, color, time zone), or null.                                                                                        |
| [`getCalendarEvent`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/bookings.ts)   | read | public token | —               | One calendar event with its rich-text `body` and its calendar, or null.                                                                |
| [`listBookings`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/bookings.ts)       | read | Bearer       | `bookings list` | The workspace's bookings as a flat array; page with `first`/`after` (or `last`/`before`); a public token gets an empty list.           |
| [`listCalendarEvents`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/bookings.ts) | read | public token | —               | Calendar events as a flat array, optionally narrowed by `calendarIds` and named `scopes` (e.g. `upcoming`); page with `first`/`after`. |
| [`listCalendars`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/bookings.ts)      | read | public token | —               | The workspace's calendars as a flat array; page with `first`/`after` (or `last`/`before`).                                             |

**Types:** `Booking`, `Calendar`, `CalendarEvent`

**CLI:**

| Entity     | list | get | create | update | delete |
| ---------- | :--: | :-: | :----: | :----: | :----: |
| `bookings` |   ✓  |  ✓  |    —   |    —   |    —   |
