Search the docs

Find a page, a section, or an endpoint.

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:

curl "https://pidgeon.ai/api/v1/identities/idn_4f2a1/calendar/agenda?after=2026-11-03T00:00:00Z&before=2026-11-04T00:00:00Z" \
-H "Authorization: Bearer $PIDGEON_API_KEY"

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 has both directions.