# Overview

> Every mailbox has one, and a wall-clock time is the truth of an event.

Every mailbox has a calendar. It is not a second product bolted on: a meeting
invitation arrives as mail, and an agent that can read the mail should be able
to answer the meeting.

```sh
GET /v1/identities/{id}/calendar
```

There is nothing to create. The calendar exists because the address does, and
this returns it — its default time zone, and the identity it belongs to.

## A time is a wall clock, and the instant is derived

This is the one thing to understand before anything else on these pages.

An event carries **`start`, `end` and `timezone`** — a local wall-clock time
and the zone it is read in. That is the truth of the event. It also carries
`starts_at` and `ends_at`, the same moments as UTC instants, and those are
**derived**: Postgres computes them from the wall time and the zone.

```json
{
"start": "2026-11-03T09:00:00",
"end": "2026-11-03T09:30:00",
"timezone": "Europe/London",
"starts_at": "2026-11-03T09:00:00Z",
"ends_at": "2026-11-03T09:30:00Z"
}
```

**Write the wall time; read whichever you need.** "Every Tuesday at 09:00 in
London" is 08:00 UTC in October and 09:00 UTC in November, and a calendar that
stored the instant would move the meeting an hour twice a year. One conversion,
in one place, so there is one time-zone database in play rather than two that
can disagree.

An **all-day** event sets `all_day` and carries dates rather than times, and
its `end` is the **last day, inclusive** — the day somebody would say out loud,
not the exclusive bound iCalendar puts on the wire.

## The two reads

`GET /calendar/events` answers *what is on this calendar*, newest change
first, and is the one you want when you are syncing or auditing.

`GET /calendar/agenda` answers *what is on*, in a window, in order — and is
the one you usually want:

```bash
curl "https://pidgeon.ai/api/v1/identities/0c939bd7-2d69-4150-a2ac-de994f0e4887/calendar/agenda?after=2026-11-03T00:00:00Z&before=2026-11-04T00:00:00Z" \
-H "Authorization: Bearer $PIDGEON_API_KEY"
```

```ts
const { data } = await calendarGetAgenda({
path: { id: '0c939bd7-2d69-4150-a2ac-de994f0e4887' },
query: { after: '2026-11-03T00:00:00Z', before: '2026-11-04T00:00:00Z' },
});
```

```python
from pidgeon.api.calendar import calendar_get_agenda

agenda = calendar_get_agenda.sync(
  client=client,
  id="0c939bd7-2d69-4150-a2ac-de994f0e4887",
  after="2026-11-03T00:00:00Z",
  before="2026-11-04T00:00:00Z",
)
```

The agenda expands recurring series, so a weekly meeting appears on each of
its dates without a row existing for any of them. It also includes an event
that **started before the window and is still running** — `include_overlapping`
is on by default, because *what is on today* and *what starts today* are
different questions and the first is almost always the one being asked.

## Where events come from

`source` says it. An event you created through the API is `api`. One that
arrived as a meeting invitation in the mailbox is `mail`, and Pidgeon put it
there by reading the `text/calendar` part of the message — `origin_message_id`
is the mail it came from.

That distinction is load-bearing: Pidgeon sends invitations for events it
owns, and never for one it only heard about. [Invitations](https://pidgeon.ai/docs/calendar/invitations.md)
has both directions.
