# Addresses

> Every addresses endpoint, generated from the API's own schemas.

Base URL `https://pidgeon.ai/api/v1`. Every request carries `Authorization: Bearer pgn_live_…`. The machine-readable version of this page is https://pidgeon.ai/openapi.json.

## GET /identities

List addresses

Alphabetical by address. `cursor` is the last address on the page before it, not a timestamp — this is the one list that is not ordered by time, because it is read by somebody looking for a particular address.

**Parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | number | no |  |
| `cursor` | string | no |  |
| `tenant_id` | string | no |  |
| `pod_id` | uuid | no |  |

**Responses**

- `200` A page of addresses.
- `400` The request is not valid. `error.field` names the input at fault when there is one.
- `401` No API key, or one that is not valid.
- `403` Refused. `error.code` says why: `forbidden` when this key may not do this or the account is on hold, `abuse_blocked` when a limit says no.
- `404` No such resource, or one this key cannot reach. The two answer the same, so treat a 404 as “not available to you” rather than “does not exist”.
- `409` It conflicts with what is there: it already exists, or the `etag` you sent is not the current one.
- `429` Rate limited. `X-RateLimit-Reset` says when to retry.
- `500` Something went wrong on our side. Retry; if it keeps happening, tell us.
- `503` Something this needs is not answering right now. Retry in a moment.

## POST /identities

Create an address

Idempotent with an `Idempotency-Key` header: the same key returns the first result rather than creating a second address.

**Parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | string | no | Any string unique to this attempt, reused on its retries. A repeat with the same key and the same request returns the first answer with `Idempotent-Replay: true` and does nothing — no second email. The same key with a different request is a `409`. Kept for 24 hours; a failure is not kept, so a retry after one runs. |

**Body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `domain_id` | uuid | no | From `GET /domains`, or the `domain_id` on any address you already have. |
| `domain` | string | no | A domain name you own, or the shared one. An alternative to `domain_id`. |
| `address` | string | no | A whole address — the local part and the domain in one. An alternative to the other two. |
| `local_part` | string | no | Everything before the `@`. |
| `display_name` | string \| null | no | The name a recipient sees beside the address. |
| `inbox` | boolean | no | Whether it keeps mail. With an inbox what arrives is stored and a person can open it; without one it becomes an event and your webhook fires. Defaults from `type`. |
| `type` | enum | no | A label for what this address is for. It does not change what the address can do — that is `inbox`. One of: `human`, `team`, `application`, `workflow`, `agent`. |
| `mode` | enum | no | Who answers: a person, software, or both. One of: `human`, `automated`, `hybrid`. |
| `expires_at` | date-time \| null | no | When it stops accepting mail. Expiry suspends; it never deletes, so the correspondence survives. |
| `send_policy` | enum | no | Who it may write to. `reply_only` makes an address structurally unable to start a conversation with a stranger. One of: `open`, `reply_only`, `list`. |
| `tenant_id` | string \| null | no | Your own opaque grouping — a customer id, whatever your side calls it. Never parsed here. |
| `pod_id` | uuid \| null | no | The pod this belongs to — your account’s default when you name none. A pod is a sending boundary: mail from it lands on its own reputation, so one of your customers cannot damage another’s delivery. It does not change what an address can do, and no key is scoped to one. |

**Responses**

- `201` The address.
- `400` The request is not valid. `error.field` names the input at fault when there is one.
- `401` No API key, or one that is not valid.
- `402` Your plan does not include this. `error.message` says what does.
- `403` Refused. `error.code` says why: `forbidden` when this key may not do this or the account is on hold, `abuse_blocked` when a limit says no.
- `404` No such resource, or one this key cannot reach. The two answer the same, so treat a 404 as “not available to you” rather than “does not exist”.
- `409` This `Idempotency-Key` was used with a different request, or that request is still running. Nothing was sent by this call.
- `429` Rate limited. `X-RateLimit-Reset` says when to retry.
- `500` Something went wrong on our side. Retry; if it keeps happening, tell us.
- `503` Something this needs is not answering right now. Retry in a moment.

## GET /identities/{id}

Fetch one address

**Parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |

**Responses**

- `200` The address.
- `400` The request is not valid. `error.field` names the input at fault when there is one.
- `401` No API key, or one that is not valid.
- `403` Refused. `error.code` says why: `forbidden` when this key may not do this or the account is on hold, `abuse_blocked` when a limit says no.
- `404` No such resource, or one this key cannot reach. The two answer the same, so treat a 404 as “not available to you” rather than “does not exist”.
- `409` It conflicts with what is there: it already exists, or the `etag` you sent is not the current one.
- `429` Rate limited. `X-RateLimit-Reset` says when to retry.
- `500` Something went wrong on our side. Retry; if it keeps happening, tell us.
- `503` Something this needs is not answering right now. Retry in a moment.

## PATCH /identities/{id}

Update an address

**Parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |

**Body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `display_name` | string \| null | no | The name a recipient sees beside the address. |
| `mode` | enum | no | Who answers: a person, software, or both. One of: `human`, `automated`, `hybrid`. |
| `capabilities` | object | no |  |
| `status` | enum | no |  One of: `active`, `suspended`, `archived`. |
| `expires_at` | date-time \| null | no | When it stops accepting mail. Expiry suspends; it never deletes, so the correspondence survives. |
| `send_policy` | enum | no | Who it may write to. `reply_only` makes an address structurally unable to start a conversation with a stranger. One of: `open`, `reply_only`, `list`. |
| `tenant_id` | string \| null | no | Your own opaque grouping — a customer id, whatever your side calls it. Never parsed here. |
| `pod_id` | uuid \| null | no | The pod this belongs to — your account’s default when you name none. A pod is a sending boundary: mail from it lands on its own reputation, so one of your customers cannot damage another’s delivery. It does not change what an address can do, and no key is scoped to one. |

**Responses**

- `200` The address.
- `400` The request is not valid. `error.field` names the input at fault when there is one.
- `401` No API key, or one that is not valid.
- `403` Refused. `error.code` says why: `forbidden` when this key may not do this or the account is on hold, `abuse_blocked` when a limit says no.
- `404` No such resource, or one this key cannot reach. The two answer the same, so treat a 404 as “not available to you” rather than “does not exist”.
- `409` It conflicts with what is there: it already exists, or the `etag` you sent is not the current one.
- `429` Rate limited. `X-RateLimit-Reset` says when to retry.
- `500` Something went wrong on our side. Retry; if it keeps happening, tell us.
- `503` Something this needs is not answering right now. Retry in a moment.

## DELETE /identities/{id}

Delete an address

Takes its messages, routing rules and webhooks with it.

**Parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |

**Responses**

- `200` Deleted.
- `400` The request is not valid. `error.field` names the input at fault when there is one.
- `401` No API key, or one that is not valid.
- `403` Refused. `error.code` says why: `forbidden` when this key may not do this or the account is on hold, `abuse_blocked` when a limit says no.
- `404` No such resource, or one this key cannot reach. The two answer the same, so treat a 404 as “not available to you” rather than “does not exist”.
- `409` It conflicts with what is there: it already exists, or the `etag` you sent is not the current one.
- `429` Rate limited. `X-RateLimit-Reset` says when to retry.
- `500` Something went wrong on our side. Retry; if it keeps happening, tell us.
- `503` Something this needs is not answering right now. Retry in a moment.

## GET /identities/{id}/access

Who can reach this address

Reading the list requires being able to reach the address: who has access to somebody’s mailbox is itself private.

**Parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |

**Responses**

- `200` The access list.
- `400` The request is not valid. `error.field` names the input at fault when there is one.
- `401` No API key, or one that is not valid.
- `403` Refused. `error.code` says why: `forbidden` when this key may not do this or the account is on hold, `abuse_blocked` when a limit says no.
- `404` No such resource, or one this key cannot reach. The two answer the same, so treat a 404 as “not available to you” rather than “does not exist”.
- `409` It conflicts with what is there: it already exists, or the `etag` you sent is not the current one.
- `429` Rate limited. `X-RateLimit-Reset` says when to retry.
- `500` Something went wrong on our side. Retry; if it keeps happening, tell us.
- `503` Something this needs is not answering right now. Retry in a moment.

## GET /identities/{id}/send-rules

What this address may write to

The allow and block list, and the `send_policy` it hangs off — the rules mean nothing without it. Requires `send` on the address rather than `admin`: a caller refused a recipient has to be able to find out why.

**Parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |

**Responses**

- `200` The list.
- `400` The request is not valid. `error.field` names the input at fault when there is one.
- `401` No API key, or one that is not valid.
- `403` Refused. `error.code` says why: `forbidden` when this key may not do this or the account is on hold, `abuse_blocked` when a limit says no.
- `404` No such resource, or one this key cannot reach. The two answer the same, so treat a 404 as “not available to you” rather than “does not exist”.
- `409` It conflicts with what is there: it already exists, or the `etag` you sent is not the current one.
- `429` Rate limited. `X-RateLimit-Reset` says when to retry.
- `500` Something went wrong on our side. Retry; if it keeps happening, tell us.
- `503` Something this needs is not answering right now. Retry in a moment.

## GET /identities/{id}/receive-rules

Who may write to this address

The rules that decide what reaches this address. Requires `read` rather than `admin`: anybody who may read the mail may see why some of it did not arrive. **Workspace-wide rules are not listed here** — these are the address’s own, and a key that administers one address must not be able to silence a sender for every other address in the workspace.

**Parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |

**Responses**

- `200` The list.
- `400` The request is not valid. `error.field` names the input at fault when there is one.
- `401` No API key, or one that is not valid.
- `403` Refused. `error.code` says why: `forbidden` when this key may not do this or the account is on hold, `abuse_blocked` when a limit says no.
- `404` No such resource, or one this key cannot reach. The two answer the same, so treat a 404 as “not available to you” rather than “does not exist”.
- `409` It conflicts with what is there: it already exists, or the `etag` you sent is not the current one.
- `429` Rate limited. `X-RateLimit-Reset` says when to retry.
- `500` Something went wrong on our side. Retry; if it keeps happening, tell us.
- `503` Something this needs is not answering right now. Retry in a moment.

## GET /identities/{id}/calendar

A mailbox’s calendar

Every mailbox has exactly one, from the moment it exists, so there is nothing to create. `created_at` is the mailbox’s, because the calendar is the mailbox.

**Parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |

**Responses**

- `200` The calendar.
- `400` The request is not valid. `error.field` names the input at fault when there is one.
- `401` No API key, or one that is not valid.
- `403` Refused. `error.code` says why: `forbidden` when this key may not do this or the account is on hold, `abuse_blocked` when a limit says no.
- `404` No such resource, or one this key cannot reach. The two answer the same, so treat a 404 as “not available to you” rather than “does not exist”.
- `409` It conflicts with what is there: it already exists, or the `etag` you sent is not the current one.
- `429` Rate limited. `X-RateLimit-Reset` says when to retry.
- `500` Something went wrong on our side. Retry; if it keeps happening, tell us.
- `503` Something this needs is not answering right now. Retry in a moment.

## POST /identities/{id}/access

Grant access

Name exactly one of `user_id`, `api_key_id` or `everyone`. Granting **replaces** any grant that principal already had, so calling this twice changes the rung rather than leaving two answers behind. Answers 404 rather than 403 when you may not administer the address — including when you can read it.

**Parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |

**Body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `user_id` | uuid | no |  |
| `api_key_id` | uuid | no |  |
| `everyone` | boolean | no |  |
| `access` | enum | yes | What a key may do. Give an agent the narrowest one that works. One of: `read`, `send`, `admin`. |

**Responses**

- `200` The access list.
- `400` The request is not valid. `error.field` names the input at fault when there is one.
- `401` No API key, or one that is not valid.
- `403` Refused. `error.code` says why: `forbidden` when this key may not do this or the account is on hold, `abuse_blocked` when a limit says no.
- `404` No such resource, or one this key cannot reach. The two answer the same, so treat a 404 as “not available to you” rather than “does not exist”.
- `409` It conflicts with what is there: it already exists, or the `etag` you sent is not the current one.
- `429` Rate limited. `X-RateLimit-Reset` says when to retry.
- `500` Something went wrong on our side. Retry; if it keeps happening, tell us.
- `503` Something this needs is not answering right now. Retry in a moment.

## POST /identities/{id}/send-rules

Allow or block a recipient

`value` is an address or a domain and the string says which: `billing@acme.com` matches one person, `acme.com` matches everybody there. **The more specific rule wins**, so blocking a domain and allowing one person at it does what it looks like. Adding **replaces** any rule already there for that value, so `allow` where `block` sat is a change of mind rather than a conflict. A `block` refuses under every policy, including `open`.

**Parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |

**Body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `effect` | enum | yes |  One of: `allow`, `block`. |
| `value` | string | yes |  |

**Responses**

- `200` The list.
- `400` The request is not valid. `error.field` names the input at fault when there is one.
- `401` No API key, or one that is not valid.
- `403` Refused. `error.code` says why: `forbidden` when this key may not do this or the account is on hold, `abuse_blocked` when a limit says no.
- `404` No such resource, or one this key cannot reach. The two answer the same, so treat a 404 as “not available to you” rather than “does not exist”.
- `409` It conflicts with what is there: it already exists, or the `etag` you sent is not the current one.
- `429` Rate limited. `X-RateLimit-Reset` says when to retry.
- `500` Something went wrong on our side. Retry; if it keeps happening, tell us.
- `503` Something this needs is not answering right now. Retry in a moment.

## POST /identities/{id}/receive-rules

Decide what happens to a sender

`value` is an address or a domain and the string says which: `billing@acme.com` matches one person, `acme.com` matches everybody there. **The more specific rule wins**, and an address’s own rule wins over a workspace-wide one. Adding **replaces** any rule already there for that value.

`action` is `reject`, `spam`, `trash`, `archive` or `inbox`. **`reject` refuses before the message is stored** — no row, no thread, no storage charged and no webhook — which is what an address nobody will ever open wants. The other four file it, and `inbox` means *never classify this sender as spam*.

**It is not a security control.** The `From:` header is written by the sender, and anybody blocked can change it for the price of a new address. What this buys is not paying for mail from senders who have not bothered.

**Parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |

**Body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `action` | enum | yes |  One of: `reject`, `spam`, `trash`, `archive`, `inbox`. |
| `value` | string | yes |  |

**Responses**

- `200` The list.
- `400` The request is not valid. `error.field` names the input at fault when there is one.
- `401` No API key, or one that is not valid.
- `403` Refused. `error.code` says why: `forbidden` when this key may not do this or the account is on hold, `abuse_blocked` when a limit says no.
- `404` No such resource, or one this key cannot reach. The two answer the same, so treat a 404 as “not available to you” rather than “does not exist”.
- `409` It conflicts with what is there: it already exists, or the `etag` you sent is not the current one.
- `429` Rate limited. `X-RateLimit-Reset` says when to retry.
- `500` Something went wrong on our side. Retry; if it keeps happening, tell us.
- `503` Something this needs is not answering right now. Retry in a moment.

## PATCH /identities/{id}/calendar

Set the default time zone

The zone an event created without one gets. **Changing it does not move existing events**: an event stores the wall time it was given, so "every Tuesday at 09:00" is still 09:00 afterwards.

**Parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |
| `If-Match` | string | no | The `etag` you last read. A stale one is a `409` naming the current version — two agents on one mailbox is the case this product is for, and last-write-wins loses a meeting. |

**Body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `timezone` | string | yes | An IANA name, like `Europe/Lisbon`. The times above are read in it. |

**Responses**

- `200` The calendar.
- `400` The request is not valid. `error.field` names the input at fault when there is one.
- `401` No API key, or one that is not valid.
- `403` Refused. `error.code` says why: `forbidden` when this key may not do this or the account is on hold, `abuse_blocked` when a limit says no.
- `404` No such resource, or one this key cannot reach. The two answer the same, so treat a 404 as “not available to you” rather than “does not exist”.
- `409` It conflicts with what is there: it already exists, or the `etag` you sent is not the current one.
- `429` Rate limited. `X-RateLimit-Reset` says when to retry.
- `500` Something went wrong on our side. Retry; if it keeps happening, tell us.
- `503` Something this needs is not answering right now. Retry in a moment.

## DELETE /identities/{id}/access

Revoke access

By the grant’s own id, which `GET` returned as `id`.

**Parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |
| `grant_id` | uuid | yes |  |

**Responses**

- `200` Revoked.
- `400` The request is not valid. `error.field` names the input at fault when there is one.
- `401` No API key, or one that is not valid.
- `403` Refused. `error.code` says why: `forbidden` when this key may not do this or the account is on hold, `abuse_blocked` when a limit says no.
- `404` No such resource, or one this key cannot reach. The two answer the same, so treat a 404 as “not available to you” rather than “does not exist”.
- `409` It conflicts with what is there: it already exists, or the `etag` you sent is not the current one.
- `429` Rate limited. `X-RateLimit-Reset` says when to retry.
- `500` Something went wrong on our side. Retry; if it keeps happening, tell us.
- `503` Something this needs is not answering right now. Retry in a moment.

## DELETE /identities/{id}/send-rules

Remove a rule

By the rule’s own id, which `GET` returned as `id`.

**Parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |
| `rule_id` | uuid | yes |  |

**Responses**

- `200` Removed.
- `400` The request is not valid. `error.field` names the input at fault when there is one.
- `401` No API key, or one that is not valid.
- `403` Refused. `error.code` says why: `forbidden` when this key may not do this or the account is on hold, `abuse_blocked` when a limit says no.
- `404` No such resource, or one this key cannot reach. The two answer the same, so treat a 404 as “not available to you” rather than “does not exist”.
- `409` It conflicts with what is there: it already exists, or the `etag` you sent is not the current one.
- `429` Rate limited. `X-RateLimit-Reset` says when to retry.
- `500` Something went wrong on our side. Retry; if it keeps happening, tell us.
- `503` Something this needs is not answering right now. Retry in a moment.

## DELETE /identities/{id}/receive-rules

Remove a rule

By the rule’s own id, which `GET` returned as `id`.

**Parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |
| `rule_id` | uuid | yes |  |

**Responses**

- `200` Removed.
- `400` The request is not valid. `error.field` names the input at fault when there is one.
- `401` No API key, or one that is not valid.
- `403` Refused. `error.code` says why: `forbidden` when this key may not do this or the account is on hold, `abuse_blocked` when a limit says no.
- `404` No such resource, or one this key cannot reach. The two answer the same, so treat a 404 as “not available to you” rather than “does not exist”.
- `409` It conflicts with what is there: it already exists, or the `etag` you sent is not the current one.
- `429` Rate limited. `X-RateLimit-Reset` says when to retry.
- `500` Something went wrong on our side. Retry; if it keeps happening, tell us.
- `503` Something this needs is not answering right now. Retry in a moment.

## GET /identities/{id}/calendar/events

Everything on the calendar

Ordered by when it last changed. `GET /agenda` is the other read and the one you usually want: this answers *what is on this calendar*, that answers *what is on between these two moments*.

**Parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |
| `limit` | number | no |  |
| `cursor` | string | no |  |

**Responses**

- `200` The events.
- `400` The request is not valid. `error.field` names the input at fault when there is one.
- `401` No API key, or one that is not valid.
- `403` Refused. `error.code` says why: `forbidden` when this key may not do this or the account is on hold, `abuse_blocked` when a limit says no.
- `404` No such resource, or one this key cannot reach. The two answer the same, so treat a 404 as “not available to you” rather than “does not exist”.
- `409` It conflicts with what is there: it already exists, or the `etag` you sent is not the current one.
- `429` Rate limited. `X-RateLimit-Reset` says when to retry.
- `500` Something went wrong on our side. Retry; if it keeps happening, tell us.
- `503` Something this needs is not answering right now. Retry in a moment.

## GET /identities/{id}/calendar/agenda

What is on, in a window, in order

`include_overlapping` defaults to true, which is the difference between *what starts today* and *what is on today*: a meeting that began last night and runs into this morning is on today, and a naive `starts_at between` misses it. Cancelled events are left out — an agenda is what you are doing.

**Parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |
| `after` | date-time | no |  |
| `before` | date-time | no |  |
| `include_overlapping` | enum | no |  One of: `true`, `false`. |
| `limit` | number | no |  |

**Responses**

- `200` The agenda.
- `400` The request is not valid. `error.field` names the input at fault when there is one.
- `401` No API key, or one that is not valid.
- `403` Refused. `error.code` says why: `forbidden` when this key may not do this or the account is on hold, `abuse_blocked` when a limit says no.
- `404` No such resource, or one this key cannot reach. The two answer the same, so treat a 404 as “not available to you” rather than “does not exist”.
- `409` It conflicts with what is there: it already exists, or the `etag` you sent is not the current one.
- `429` Rate limited. `X-RateLimit-Reset` says when to retry.
- `500` Something went wrong on our side. Retry; if it keeps happening, tell us.
- `503` Something this needs is not answering right now. Retry in a moment.

## POST /identities/{id}/calendar/events

Put something on the calendar

`start` and `end` are **local wall times with no zone on them** — `2026-10-20T14:00:00` — and `timezone` is a field of its own. That pair is the truth; `starts_at` is the same moment as an instant, derived from it, and it is what survives a daylight-saving change. For an all-day event, `end` is the last day, inclusive.

**Parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |
| `Idempotency-Key` | string | no | Any string unique to this attempt, reused on its retries. A repeat with the same key and the same request returns the first answer with `Idempotent-Replay: true` and does nothing — no second email. The same key with a different request is a `409`. Kept for 24 hours; a failure is not kept, so a retry after one runs. |

**Body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `title` | string | yes |  |
| `start` | string | yes |  |
| `end` | string | yes |  |
| `timezone` | string \| null | no | An IANA name, like `Europe/Lisbon`. The times above are read in it. |
| `all_day` | boolean | no |  |
| `description` | string \| null | no |  |
| `location` | string \| null | no |  |
| `status` | enum | no |  One of: `confirmed`, `tentative`, `cancelled`. |
| `metadata` | object | no |  |
| `attendees` | object[] | no |  |
| `send_invites` | boolean | no |  |
| `recurrence` | object \| null | no |  |

**Responses**

- `201` The event.
- `400` The request is not valid. `error.field` names the input at fault when there is one.
- `401` No API key, or one that is not valid.
- `403` Refused, and nothing was sent to anybody. `error.code` says why: `blocked` (a rule), `not_permitted` (the send policy), `abuse_blocked` (a sending limit) or `forbidden` (the address is not one this key can send from, or is not active).
- `404` No such resource, or one this key cannot reach. The two answer the same, so treat a 404 as “not available to you” rather than “does not exist”.
- `409` This `Idempotency-Key` was used with a different request, or that request is still running. Nothing was sent by this call.
- `429` Rate limited. `X-RateLimit-Reset` says when to retry.
- `500` Something went wrong on our side. Retry; if it keeps happening, tell us.
- `503` Something this needs is not answering right now. Retry in a moment.

## GET /identities/{id}/calendar/events/{eventId}

One event

**Parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |
| `eventId` | string | yes |  |

**Responses**

- `200` The event.
- `400` The request is not valid. `error.field` names the input at fault when there is one.
- `401` No API key, or one that is not valid.
- `403` Refused. `error.code` says why: `forbidden` when this key may not do this or the account is on hold, `abuse_blocked` when a limit says no.
- `404` No such resource, or one this key cannot reach. The two answer the same, so treat a 404 as “not available to you” rather than “does not exist”.
- `409` It conflicts with what is there: it already exists, or the `etag` you sent is not the current one.
- `410` It was here and is not any more. Forget the id.
- `429` Rate limited. `X-RateLimit-Reset` says when to retry.
- `500` Something went wrong on our side. Retry; if it keeps happening, tell us.
- `503` Something this needs is not answering right now. Retry in a moment.

## PATCH /identities/{id}/calendar/events/{eventId}

Change an event

An omitted field survives. `sequence` rises only when the change is one attendees see — the time, the title, the place, the status, the list. A `metadata` edit moves the `etag` and not the sequence, because nobody is ever told about it.

**Parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |
| `eventId` | string | yes |  |
| `mode` | enum | no | Which dates of a series this is about. Name one **date** of a series — the id `listEventInstances` gives you — and say whether you mean that date (`single`, the default) or that date onwards (`future`, which splits the series). Naming the series means the whole thing, which is why there is no `all`. One of: `single`, `future`. |
| `If-Match` | string | no | The `etag` you last read. A stale one is a `409` naming the current version — two agents on one mailbox is the case this product is for, and last-write-wins loses a meeting. |
| `Idempotency-Key` | string | no | Any string unique to this attempt, reused on its retries. A repeat with the same key and the same request returns the first answer with `Idempotent-Replay: true` and does nothing — no second email. The same key with a different request is a `409`. Kept for 24 hours; a failure is not kept, so a retry after one runs. |

**Body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `title` | string | no |  |
| `start` | string | no |  |
| `end` | string | no |  |
| `timezone` | string | no | An IANA name, like `Europe/Lisbon`. The times above are read in it. |
| `all_day` | boolean | no |  |
| `description` | string \| null | no |  |
| `location` | string \| null | no |  |
| `status` | enum | no |  One of: `confirmed`, `tentative`, `cancelled`. |
| `metadata` | object | no |  |
| `attendees` | object[] | no |  |
| `recurrence` | object \| null | no |  |
| `send_invites` | boolean | no |  |

**Responses**

- `200` The event.
- `400` The request is not valid. `error.field` names the input at fault when there is one.
- `401` No API key, or one that is not valid.
- `403` Refused, and nothing was sent to anybody. `error.code` says why: `blocked` (a rule), `not_permitted` (the send policy), `abuse_blocked` (a sending limit) or `forbidden` (the address is not one this key can send from, or is not active).
- `404` No such resource, or one this key cannot reach. The two answer the same, so treat a 404 as “not available to you” rather than “does not exist”.
- `409` This `Idempotency-Key` was used with a different request, or that request is still running. Nothing was sent by this call.
- `410` It was here and is not any more. Forget the id.
- `429` Rate limited. `X-RateLimit-Reset` says when to retry.
- `500` Something went wrong on our side. Retry; if it keeps happening, tell us.
- `503` Something this needs is not answering right now. Retry in a moment.

## DELETE /identities/{id}/calendar/events/{eventId}

Take it off the calendar

On one date of a series this records that the date does not happen rather than removing a row — the rule still generates it, so a removal would bring it straight back. `mode=future` ends the series there and leaves every earlier date alone.

**Parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |
| `eventId` | string | yes |  |
| `mode` | enum | no | Which dates of a series this is about. Name one **date** of a series — the id `listEventInstances` gives you — and say whether you mean that date (`single`, the default) or that date onwards (`future`, which splits the series). Naming the series means the whole thing, which is why there is no `all`. One of: `single`, `future`. |
| `If-Match` | string | no | The `etag` you last read. A stale one is a `409` naming the current version — two agents on one mailbox is the case this product is for, and last-write-wins loses a meeting. |
| `Idempotency-Key` | string | no | Any string unique to this attempt, reused on its retries. A repeat with the same key and the same request returns the first answer with `Idempotent-Replay: true` and does nothing — no second email. The same key with a different request is a `409`. Kept for 24 hours; a failure is not kept, so a retry after one runs. |

**Responses**

- `200` Deleted.
- `400` The request is not valid. `error.field` names the input at fault when there is one.
- `401` No API key, or one that is not valid.
- `403` Refused, and nothing was sent to anybody. `error.code` says why: `blocked` (a rule), `not_permitted` (the send policy), `abuse_blocked` (a sending limit) or `forbidden` (the address is not one this key can send from, or is not active).
- `404` No such resource, or one this key cannot reach. The two answer the same, so treat a 404 as “not available to you” rather than “does not exist”.
- `409` This `Idempotency-Key` was used with a different request, or that request is still running. Nothing was sent by this call.
- `410` It was here and is not any more. Forget the id.
- `429` Rate limited. `X-RateLimit-Reset` says when to retry.
- `500` Something went wrong on our side. Retry; if it keeps happening, tell us.
- `503` Something this needs is not answering right now. Retry in a moment.

## GET /identities/{id}/calendar/events/{eventId}/instances

The dates a series actually happens

Most of them have no row — they are expanded from the rule, which is what makes a weekly meeting cheap to store. An id here is `{seriesId}::{originalStart}`, derived rather than allocated, and it does **not** change when you edit that date. A date somebody edited comes back in place of the one the rule generated, under that id.

**Parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |
| `eventId` | string | yes |  |
| `after` | date-time | no |  |
| `before` | date-time | no |  |
| `limit` | number | no |  |

**Responses**

- `200` The dates.
- `400` The request is not valid. `error.field` names the input at fault when there is one.
- `401` No API key, or one that is not valid.
- `403` Refused. `error.code` says why: `forbidden` when this key may not do this or the account is on hold, `abuse_blocked` when a limit says no.
- `404` No such resource, or one this key cannot reach. The two answer the same, so treat a 404 as “not available to you” rather than “does not exist”.
- `409` It conflicts with what is there: it already exists, or the `etag` you sent is not the current one.
- `410` It was here and is not any more. Forget the id.
- `429` Rate limited. `X-RateLimit-Reset` says when to retry.
- `500` Something went wrong on our side. Retry; if it keeps happening, tell us.
- `503` Something this needs is not answering right now. Retry in a moment.

## POST /identities/{id}/calendar/events/{eventId}/respond

Answer a meeting from the calendar

The same act as `POST /messages/{id}/invitation/respond`, reached from the other end — an agent working from an agenda holds an event id and not a message id. When the meeting came from mail and `send_reply` is true (the default), an iCalendar `REPLY` goes to the organiser and the answer is written onto the event. When it did not — a meeting this mailbox created itself — there is nobody to reply to, so the answer is recorded and `reply_sent` is false. One of this mailbox’s own addresses must be on the attendee list, including an address routed to it.

**Parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |
| `eventId` | string | yes |  |
| `If-Match` | string | no | The `etag` you last read. A stale one is a `409` naming the current version — two agents on one mailbox is the case this product is for, and last-write-wins loses a meeting. |
| `Idempotency-Key` | string | no | Any string unique to this attempt, reused on its retries. A repeat with the same key and the same request returns the first answer with `Idempotent-Replay: true` and does nothing — no second email. The same key with a different request is a `409`. Kept for 24 hours; a failure is not kept, so a retry after one runs. |

**Body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `status` | enum | yes |  One of: `accepted`, `declined`, `tentative`. |
| `comment` | string | no |  |
| `send_reply` | boolean | no |  |

**Responses**

- `200` Answered.
- `400` The request is not valid. `error.field` names the input at fault when there is one.
- `401` No API key, or one that is not valid.
- `403` Refused, and nothing was sent to anybody. `error.code` says why: `blocked` (a rule), `not_permitted` (the send policy), `abuse_blocked` (a sending limit) or `forbidden` (the address is not one this key can send from, or is not active).
- `404` No such resource, or one this key cannot reach. The two answer the same, so treat a 404 as “not available to you” rather than “does not exist”.
- `409` This `Idempotency-Key` was used with a different request, or that request is still running. Nothing was sent by this call.
- `410` It was here and is not any more. Forget the id.
- `429` Rate limited. `X-RateLimit-Reset` says when to retry.
- `500` Something went wrong on our side. Retry; if it keeps happening, tell us.
- `503` Something this needs is not answering right now. Retry in a moment.
