# Events

> Poll, read, search, create, change, and delete events on the Reassign 24-hour planner with the REST API.

Source: https://reassign.dev/reference/events/

An event is one block of time on the Reassign 24-hour planner. Use these routes to poll for changes, to read and search events, and to write events.

The machine-readable spec of all public endpoints: [openapi.json](https://reassign.dev/openapi.json).

| Route                                                   | Scope                           |
| ------------------------------------------------------- | ------------------------------- |
| `GET /events`, `GET /events/{id}`, `GET /events/search` | `events:read` or `events:write` |
| `POST /events`                                          | `events:write`                  |

The `events:write` scope also gives read access. Each route needs a trial or Pro plan. Refer to [Authentication](https://reassign.dev/authentication.md) and [Errors](https://reassign.dev/errors.md).

- On `POST /events`, Reassign checks the scope before it reads the body. A token without `events:write` gets `403` with the code `scope`, also when the body is not valid.
- The old event routes are removed: `POST /events/batch`, `PATCH /events/{id}`, `DELETE /events/{id}`, and `GET /reviews/{date}`. A call to one of them gets `401` without valid auth. Else it gets `404` with the code `not_found`. Use `POST /events` in their place.

To read the events of one or more days, use `GET /schedule`. Refer to [Scheduling](https://reassign.dev/reference/scheduling.md).

## The event model

The API uses one event object in all reads and all write echoes. Refer to [Concepts](https://reassign.dev/concepts.md) for the full planner model.

### Time spans

- A span is `start` and `end`. Each value is a local datetime `"YYYY-MM-DDTHH:MM"`, with no seconds and no offset.
- The times are in the time zone of the account. Each response that has a span gives the time zone one time, in the top-level `timezone` field.
- `end` must be after `start`. An overnight event ends on the next date, for example `start` `"2026-09-23T22:00"` and `end` `"2026-09-24T01:00"`.
- An event that ends at midnight ends at `"<next day>T00:00"`, for example `"2026-09-24T00:00"`.
- The day of the event is the date part of `start`.
- `POST /events` refuses an event shorter than 5 minutes or longer than 168 hours (7 days).

### Ids of recurring events

- A bare series id addresses the full series.
- `seriesId@YYYY-MM-DD` addresses one occurrence. Reads give each occurrence this id.
- Each occurrence of a series has the fields `seriesId` and `originalDate`. They are the two parts of its id, also on a moved occurrence. A one-off event and a series echo by its bare id do not have them.
- A changed occurrence keeps the id `seriesId@<original date>`, also after a move to another day. Thus the date part can be different from the date of `start`.
- `scope: "future"` is the only scope value. Use it only with an occurrence id. It addresses that occurrence and all later occurrences.
- An occurrence id on an event that does not repeat is an error. A date that is not an occurrence of the rule is also an error.
- When you change one occurrence, its id does not change. The reads, `GET /events/search`, `GET /events/{id}`, and the write result give `seriesId@<original date>`.
- An `update` on the occurrence id of a deleted occurrence puts the occurrence back, with the fields that you send. An undo deletes it again.
- A changed occurrence carries the series' `recurrence`. It also inherits the series' `calendarId`, `mirrorCalendarIds`, and `mirrorStyles` unless it chose its own calendar.
- Older write results gave a changed occurrence a separate stored id. That id still works as a write address. No read gives it now.

### Fields

- `start` (_string, required_)
- `end` (_string, required_)
- `id` (_string, required_)
- `seriesId` (_string_)
- `originalDate` (_string_)
- `kind` (_"blocking" | "non\_blocking" | "reference", required_)
- `name` (_string, required_)
- `notes` (_string_)
- `source` (_"google" | "google\_tasks" | "linear" | "microsoft" | "microsoft\_todo" | "ticktick" | "todoist" | "reassign", required_)
- `sourceUrl` (_string_)
- `meeting` (_object_)
  - `url` (_string, required_)
  - `label` (_string, required_)
- `location` (_object_)
  - `text` (_string, required_)
  - `url` (_string_)
- `recurrence` (_string_)
- `warning` (_string_)
- `areaId` (_string | null, required_)
- `activityTypeId` (_string | null, required_)
- `projectId` (_string | null, required_)
- `priority` (_integer, required_)
- `calendarId` (_string | null_)
- `mirrorCalendarIds` (_string\[]_)
- `mirrorStyles` (_map of "full" | "private" | "busy"_)
- `readOnly` (_true_)
- `focusIntervals` (_object_)
  - `focusMin` (_integer, required_)
  - `breakMin` (_integer, required_)
- `completedFocusIntervals` (_integer_)
- `checklist` (_object_)
  - `items` (_object\[], required_)
    - `id` (_string, required_)
    - `text` (_string, required_)
    - `done` (_boolean, required_)
- `reflect` (_object_)
  - One form: Without `actualStart`, `actualEnd`
    - `status` (_"kept" | "skipped" | "changed" | "added", required_)
  - One form: With `actualStart`, `actualEnd`
    - `status` (_"changed", required_)
    - `actualStart` (_string, required_)
    - `actualEnd` (_string, required_)

* An optional value that is empty is not in the object. For example, an event with no notes has no `notes` field.
* `areaId`, `activityTypeId`, and `projectId` are always in the object. The value is `null` when the event has no area, no activity type, or no project.
* `priority` is always in the object: `0` none, `1` low, `2` medium, `3` high, or `4` urgent.
* An event that you schedule from the Inbox takes the project of its Inbox item.
* The API refers to areas, activity types, and projects by id only. `GET /schedule` gives the `areas`, `activityTypes`, and `projects` lists with the names.
* `readOnly: true` shows an event on a calendar that the user does not own. The API refuses `update`, `shift`, and `delete` on it with `read_only` (403). The message is `This event is read-only here. Make this change in its source calendar ("<calendar>"). Nothing changed.` The part in parentheses is there only when Reassign knows the calendar name. The `reflect` and `checklist` ops work on it.
* One `update` works on a read-only event: `calendarId: null` on the bare series id. The edits that the op carries also apply, because the event is local after the op.

### Writable and output-only fields

You can copy the writable fields of a read into a write without a change. The server calculates the output-only fields. A write op that sends an output-only field fails with `validation`.

| Field                                                                                                                                                                                        | On a write                                                                                                                                              |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                                                                                                                                                                                         | `update`: the address. An occurrence id from a read works. `create`: optional, and it must be a UUID. The server makes a UUID when you do not send one. |
| `start`, `end`, `kind`, `name`, `notes`, `sourceUrl`, `recurrence`, `areaId`, `activityTypeId`, `projectId`, `priority`, `calendarId`, `mirrorCalendarIds`, `mirrorStyles`, `focusIntervals` | Writable on `create` and `update`.                                                                                                                      |
| `checklist`                                                                                                                                                                                  | Writable on `create`. After that, use the `checklist` op. Its `items` have the shape of `checklist.items` in a read.                                    |
| `source`, `meeting`, `location`, `warning`, `readOnly`, `completedFocusIntervals`, `reflect`, `seriesId`, `originalDate`                                                                     | Output-only. To write `reflect`, use the `reflect` op.                                                                                                  |

The `warning` field shows a start time that a daylight saving change skips on that date.

Search results also have an output-only `score` for the name match. Leave it out when building a write from a search result.

---

## Poll for changes (Beta)

**GET** `/api/v1/events`

> This route is in Beta. The response can change.

Get the stored events that changed after a cursor, oldest first. Use it to keep a copy of the events in sync. Scope: `events:read`.

- On the first poll, do not send `cursor`.
- Each response has a `cursor`. Send it as `cursor` on the next poll. Do not change or parse it.
- A cursor that this API did not write returns 422 `validation`.
- `limit` is 1 to 500. The default is 100. Other values return 422.
- When `hasMore` is `true`, more rows wait. Send the next poll at once.
- The cursor holds the exact position of the last row. A page boundary does not skip or repeat a row.
- When no event changed, the response has no rows and a `cursor` that you can send again.

The rows are different from the event model:

- The response has one row for each stored event. It does not expand a recurring event into its occurrences.
- A changed occurrence is a separate stored event. Its row has the stored id of that event, not `seriesId@<original date>`. No other read gives this id. `GET /events/{id}` with it returns the occurrence with the id `seriesId@<original date>`.
- The response does not show deleted events.
- A row has only `id`, `name`, `start`, `end`, `recurrence` (for a series), `areaId`, `activityTypeId`, and `updatedAt`. `updatedAt` is an ISO 8601 instant.
- An event with no name has the name `"(untitled)"`.

To read a day, use `GET /schedule`.

### Query parameters

- `cursor` (_string_): The opaque `cursor` of the last poll. The rows that changed after it return. Omit it on the first poll.
- `limit` (_string_): The page size, 1 to 500 (default 100).

### Response fields

- `events` (_object\[], required_)
  - `start` (_string, required_)
  - `end` (_string, required_)
  - `id` (_string, required_)
  - `name` (_string, required_)
  - `recurrence` (_string_)
  - `areaId` (_string | null, required_)
  - `activityTypeId` (_string | null, required_)
  - `updatedAt` (_string, required_)
- `cursor` (_string, required_)
- `hasMore` (_boolean, required_)
- `timezone` (_string, required_)

### Responses

| Status | Codes                 | Meaning                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| ------ | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `200`  | —                     | Success.                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `401`  | `unauthorized`        | No Bearer token and no session cookie, or the token is invalid, expired, or revoked. The response carries a `WWW-Authenticate` challenge that points at the protected-resource metadata.                                                                                                                                                                                                                                                                                               |
| `403`  | `permission`, `scope` | A token without `events:read` or `events:write` gets `scope` "This token is not authorized to read events (requires events:read or events:write scope)." An account without Reassign Pro access gets `permission` "Reassign needs an active subscription." The 7-day free trial includes Reassign Pro. A cookie session from another site gets `permission` "Cross-origin request."                                                                                                    |
| `422`  | `validation`          | A query parameter is not valid. The message names the first bad field by its path. A repeated or unknown query parameter is also a 422.                                                                                                                                                                                                                                                                                                                                                |
| `429`  | `rate_limited`        | Too many requests. The edge firewall limits each IP to 2000 requests per minute across `/api/v1`; that 429 comes from the edge and can have a body that is not this JSON and no `Retry-After` header. When an edge 429 has no `Retry-After`, retry after 60 seconds. The app also limits each Bearer credential to 600 requests per minute, and some routes limit each user. These limits return this body. This app 429 always has `Retry-After`: retry after that number of seconds. |
| `503`  | `internal`            | A fault on our side. The request is safe to retry.                                                                                                                                                                                                                                                                                                                                                                                                                                     |

**Request** — **GET** `/api/v1/events`

**cURL**

```bash
curl "https://reassign.app/api/v1/events" \
  -H "Authorization: Bearer rsk_live_YOUR_TOKEN"
```

**JavaScript**

```js
const res = await fetch("https://reassign.app/api/v1/events", {
  headers: {
    Authorization: "Bearer rsk_live_YOUR_TOKEN",
  },
});
const data = await res.json();
```

**Response**

```json
{
  "events": [
    {
      "id": "b7f1c2a0-1d2e-4f3a-9b8c-5d6e7f801234",
      "name": "Team sync",
      "start": "2026-08-13T09:00",
      "end": "2026-08-13T10:30",
      "areaId": "3c9d2a41-6b7e-4f80-9a1b-2c3d4e5f6071",
      "activityTypeId": "5e6f7081-92a3-4b4c-8d5e-6f708192a3b4",
      "updatedAt": "2026-08-13T08:40:00.000Z"
    }
  ],
  "cursor": "MjAyNi0wOC0xM1QwODo0MDowMC4xMjM0NTZafGI3ZjFjMmEwLTFkMmUtNGYzYS05YjhjLTVkNmU3ZjgwMTIzNA",
  "hasMore": false,
  "timezone": "Europe/Ljubljana"
}
```

---

## Write events

**POST** `/api/v1/events`

Create, change, move, reflect, and delete events. This route is the one event write of the API. Send 1 to 50 `ops`, and an optional `partial`. Scope: `events:write`.

### Ops

Each op has an `op` field with its name.

| `op`        | What it does                                                                                                                                                                                                  |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `create`    | Makes an event. Send `start`, `end`, and `name`. Add `recurrence` (an RRULE) for a series.                                                                                                                    |
| `update`    | Changes an event. Send `id` and only the fields to change. A new `start` or `end` moves the event, also to another day. A `start` with no `end` keeps the duration. An `end` with no `start` keeps the start. |
| `shift`     | Moves an event by `byMinutes` (a whole number, not 0, from -1440 to 1440). A negative value moves it earlier. The duration and the exact minute stay.                                                         |
| `reflect`   | Records how a past event went: `status` is `kept`, `skipped`, `changed`, or `added`. Refer to [Day reviews](https://reassign.dev/reference/day-reviews.md).                                                   |
| `checklist` | Changes the checklist. `items` replaces the full list (100 items or less). `check` and `uncheck` take item ids (100 or less).                                                                                 |
| `delete`    | Deletes an event, a series, one occurrence, or (with `scope: "future"`) an occurrence and all later ones.                                                                                                     |
| `clear`     | Deletes all events from `from` to `to`, both dates included. The range is 31 days or less.                                                                                                                    |

### Examples of tasks

| Task                                       | Op to send                                                                         |
| ------------------------------------------ | ---------------------------------------------------------------------------------- |
| Add a meeting tomorrow from 09:00 to 10:00 | `create` with `start` `"2026-09-24T09:00"`, `end` `"2026-09-24T10:00"`, and `name` |
| Add an event from 22:00 to 01:00           | `create` with an `end` on the next date                                            |
| Move an event to tomorrow at 09:00         | `update` with `id` and `start` `"2026-09-24T09:00"`                                |
| Make an event end at 18:00                 | `update` with `id` and `end` only                                                  |
| Move an event 30 minutes later             | `shift` with `byMinutes` `30`                                                      |
| Remove the area of an event                | `update` with `areaId` `null`                                                      |
| Put an event in a project                  | `update` with `projectId`                                                          |
| Mark an event as urgent                    | `update` with `priority` `4`                                                       |
| Change one occurrence of a series          | `update` with the id `seriesId@2026-09-24`                                         |
| Change an occurrence and all later ones    | `update` with the id `seriesId@2026-09-24` and `scope` `"future"`                  |
| Stop a series                              | `update` with the bare series id and `recurrence` `null`                           |
| End a series before a date                 | `delete` with the id `seriesId@2026-09-24` and `scope` `"future"`                  |
| Mark a past event as done                  | `reflect` with `status` `"kept"`                                                   |
| Record the real time of a past event       | `reflect` with `status` `"changed"`, `actualStart`, and `actualEnd`                |
| Tick off a checklist item                  | `checklist` with `check` and the item id                                           |
| Delete all events of a week                | `clear` with `from` and `to`                                                       |

### Rules of the fields

- `areaId` and `activityTypeId` take an id or `null`. On `update`, `null` removes the link, and an omitted key keeps the link. On `create`, `null` and an omitted key are the same.
- On `create`, `kind` is a manual choice, also the default `blocking`. AI classification does not change it.
- On `create`, an `areaId` or an `activityTypeId` that you send is a manual choice. A field with no id stays open for AI classification.
- On `update`, `areaId: null` or `activityTypeId: null` is a manual choice of no link. AI classification does not fill that field again, also when it was already empty.
- `projectId` takes the id of a live project from the `projects` list of `GET /schedule`. On `update`, `null` removes the project and keeps the area. An omitted key keeps the project. On `create`, `null` and an omitted key are the same.
- When an op sets a new project and no `areaId`, the event takes the area of the project. This also replaces an area that you set before. It also applies to an op on an occurrence id.
- An unknown project, or an archived project, fails the row with `not_found`: `No live project with id "<id>".` An archived project that the event already has can stay.
- On an event from a task of a task app, a new `projectId` that needs a move of the task fails with `read_only`: `A scheduled task keeps the project of its list. Change the project in the Inbox.` Change the project in the Inbox.
- `priority` is a whole number from 0 to 4. On `create`, an omitted `priority` is `0`. On `update`, an omitted `priority` keeps the value.
- An undo puts back the project, the priority, and the area of the event.
- On an event that came from a task of a task app, a `priority` change can also go to the task in its app. Reassign sends it only after a pull of the task records its priority. Refer to [Backlog](https://reassign.dev/reference/backlog.md#priority).
- `calendarId` is the home calendar. On `create`, send `null` to keep the event only on the dial. When you omit it, the event goes to the default calendar of the account, if one is set. Otherwise it stays on the dial only. `mirrorCalendarIds` lists the calendars that get a one-way copy. On `update`, `[]` removes all copies.
- On `update`, an omitted `calendarId` keeps the home calendar. `mirrorCalendarIds` checks only the ids that the op adds. An id that is already on the event can stay, also when its calendar is now read-only or disconnected.
- On `update`, `calendarId: null` removes the event from its home calendar. It also deletes all the copies in `mirrorCalendarIds`. The event then stays on the dial only.
  - Send it with the bare series id and no `scope`. An occurrence id or a `scope` is a `validation` error.
  - It can carry `start`, `end`, `name`, `notes`, `areaId`, `activityTypeId`, `projectId`, `priority`, and `kind`. These edits apply first, in the same write.
  - With `mirrorCalendarIds`, `mirrorStyles`, `recurrence`, `recurrenceEnd`, `sourceUrl`, or `focusIntervals`, it is a `validation` error. Send that change in a separate op.
  - On an event that is already on the dial only, `calendarId: null` is not a change. The copies stay. To remove them, send `mirrorCalendarIds: []`.
- A one-off event on the dial only can have copies. On `create`, send `calendarId: null` with `mirrorCalendarIds`.
- A series with copies needs a home calendar. These ops fail with `validation` and the message `Copies of a series need a home calendar. Give the series a calendarId, or remove its mirrorCalendarIds.`:
  - A `create` with `recurrence`, `mirrorCalendarIds`, and `calendarId: null`. An omitted `calendarId` also fails when the account has no default calendar.
  - An `update` that adds `recurrence` to a dial-only event with copies.
  - An `update` that adds `mirrorCalendarIds` to a dial-only series.
- A series that came from a connected calendar is an exception. After `calendarId: null`, Reassign keeps the identity of the calendar series. An `update` can then add `mirrorCalendarIds`. A read does not show this identity.
- `mirrorStyles` sets how much each copy shows. Refer to [Copy styles](https://reassign.dev/reference/events.md#copy-styles).
- On `update`, `sourceUrl: null` removes the link, `recurrence: null` stops the repeat, and `focusIntervals: null` removes the focus rhythm.
- `recurrenceEnd` needs `recurrence`. `focusIntervals` is valid only on a blocking event.
- `recurrence`, `calendarId`, `mirrorCalendarIds`, `mirrorStyles`, and `sourceUrl` belong to the full series. On one occurrence, the same value as now is not a change. A new value is an error.
- An update that moves a series to another day needs a rule that occurs on that day.
- The API rejects a key that it does not know, and a key that has no effect.

### Copy styles

`mirrorStyles` maps a copy calendar `id` to a style. A read gives it only when the event has a style. Use `busy` to block the time on a second calendar and hide the details.

| Style     | Title of the copy                              | Notes of the copy | At the provider                   |
| --------- | ---------------------------------------------- | ----------------- | --------------------------------- |
| `full`    | The event title                                | The event notes   | A normal event                    |
| `private` | The event title                                | The event notes   | A private event                   |
| `busy`    | The busy title of the copy calendar, or "Busy" | None              | A private event with no reminders |

- Each key must be in the `mirrorCalendarIds` of the op. When an `update` has no `mirrorCalendarIds`, the current copies apply. Else the op fails with `validation`: `mirrorStyles has calendar "<id>", which is not in the event's mirrorCalendarIds. Add it to mirrorCalendarIds, or remove it from mirrorStyles.`
- A value that is not `full`, `private`, or `busy` fails the schema check with `validation`. The row has `error.issues` with a path of `["mirrorStyles", "<id>"]`. Refer to [Validation errors](https://reassign.dev/errors.md#validation-errors-422).
- On `create`, `{}` is the same as no map. On `update`, `mirrorStyles` replaces the map. `{}` removes all styles.
- A calendar that is not in the map uses the default style of that calendar. Without a default, the style is `full`. `GET /schedule` gives the default as `integrations.sources[].calendars[].copyStyle`.
- A `private` or `busy` copy of a `non_blocking` or `reference` event shows as free.
- The style belongs to the full series. On an occurrence id or with `scope`, a new value fails with `validation`.
- A changed occurrence inherits the styles of its series.

### The `reflect` op

- The occurrence must be on a past day in the account's time zone. Today and future days fail with `validation`, even when today's event has already ended.
- `actualStart` and `actualEnd` go together. Send them only with `changed` or `added`.
- With `added`, the actual times become the time of the event. `actualStart` must be on the day of the event.
- To mark one occurrence of a series, send its occurrence id. A bare series id is an error.
- The actual span must be from 5 minutes to 168 hours. Else the row fails with `Event is too short (minimum 5 minutes).` or `Event is too long (maximum 168 hours).`

### Order and batch rules

- The API applies the ops in their order. An op sees the result of the ops before it. For example, a `delete` frees the time for a later `create`.
- The API reads the result rows after all ops commit. Thus a later op can change the row of an earlier op, for example a `scope: "future"` split.
- An op can target an event id one time only in each call. A second op on the same id is an error row.
- A call can have one `scope: "future"` op for each series. It cannot also have an op on the bare series id. The later op is a `validation` error row.
- A `scope: "future"` update also changes an overridden occurrence on the split date. With a new `start` or `end`, it replaces that override with the new series occurrence. With other fields, it applies those fields to the override, keeps its own time, and returns that occurrence. Later overrides keep their individual fields.
- A `scope: "future"` split of a dial-only series with copies gives the new series no copies and no `mirrorStyles`. The old series keeps its copies.
- A blocking event that overlaps another blocking event fails with `conflict`. The error row has `conflicts`. It can also have up to 3 `nearestSlots`.
- A `non_blocking` or `reference` event can overlap other events. It does not cause a conflict.

### Atomic and partial batches

By default, the batch is atomic. When one op fails, the API applies no op. Each valid op then has `status: "skipped"` and a `reason`.

With `partial: true`, the API applies each op that it can. The failed ops are error rows.

- When one or more ops apply, the status is `200`. The body has `results`, and `undoToken` with `expiresAt` when the server recorded an undo receipt.
- Reassign does not reject a batch after its writes land. When the read of a written op fails after the write, that op is an `error` row with the code `internal` and the message `The change was saved, but the read-back failed. Read it again.` The body keeps any recorded `undoToken`.
- When no op applies, the body is the error envelope and `results`. The status follows `error.code`, as the Responses table shows.

The top-level `timezone` is there only when a row has a span: a `created` or `updated` event, `conflicts`, or `nearestSlots`. For example, a batch of only `delete` or `clear` ops has no `timezone`.

A batch where no op applies is a rejected batch. This is also true for `partial: true` when all ops fail. The `error.message` of a rejected batch is the message of the one failed op. When more ops fail, it tells you to look in `results`. A rejected batch has no `undoToken`.

### Rows

Each item of `results` has `index` (0-based), `status` (`ok`, `error`, or `skipped`), and one of `result`, `error`, or `reason`.

An `ok` row can also have `warnings: ["classification_pending"]`. The write succeeded and queued AI classification. AI classification can fill eligible fields after the reply, for example an area that you did not set. It does not change the `kind` of a `create`, or an `areaId` or `activityTypeId` that a `create` sent. This warning appears only when the account and plan allow classification. Read the event again if you need its latest classification.

| Op                                        | `result`                                                                                                                                                                                                         |
| ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `create`                                  | `created`: the new event. Also `firstOccurrence` when the series does not occur on the day of `start`.                                                                                                           |
| `update`, `shift`, `reflect`, `checklist` | `updated`: the event after the change. Also `firstOccurrence` when a new rule does not occur on the stored first day.                                                                                            |
| `delete`                                  | `deletedIds`. A `scope: "future"` delete also gives `updated`: the series that now ends earlier.                                                                                                                 |
| `clear`                                   | `breakdown` (counts of one-off events, occurrences, and changed occurrences), `items` (up to 50 names and dates), and `truncated`. Also `skippedReadOnly` (a count) when it kept events of a read-only calendar. |

An op that the API cannot read does not fail the full request. It becomes an error row with the code `validation`. The message names the problem, for example an unknown `op` or a bad field. The full request fails with 422 only when the body is not valid, for example when `ops` is empty or has more than 50 items. These items are also error rows:

| Item                                    | `error.message`                                                                               |
| --------------------------------------- | --------------------------------------------------------------------------------------------- |
| Not an object (also `null` or an array) | `Each op must be an object.`                                                                  |
| An unknown `op`                         | `Unknown op "<op>". Use one of: create, update, shift, reflect, checklist, delete, clear.`    |
| An object with no `op`                  | `Unknown op undefined. Use one of: create, update, shift, reflect, checklist, delete, clear.` |
| A bad field                             | ``Invalid "<op>" op. `<path>`: <reason>``, for example ``Invalid "create" op. `start`: …``    |
| A `create` `id` that is not a UUID      | ``Invalid "create" op. `id`: id must be a UUID; omit it to let the server generate one.``     |

Each of these rows also has `error.issues`. Refer to [Validation errors](https://reassign.dev/errors.md#validation-errors-422).

### Undo

A write that records an undo receipt returns one `undoToken` and its `expiresAt` (30 minutes) at the top level. Send the token to `POST /actions/undo` to reverse the full write. Refer to [Scheduling](https://reassign.dev/reference/scheduling.md). A write that changes nothing has no `undoToken`. If the server cannot record the undo, the response also has no `undoToken`, and the change stays. Thus, a response with no `undoToken` does not prove that nothing changed.

### Request body

- `ops` (_object\[], required_)
  - One form: `op: "create"`
    - `id` (_string_): Optional client-minted UUID event id, so the caller can read the new event back by a known id. Omit it and the server mints one.
    - `start` (_string, required_): Local datetime "YYYY-MM-DDTHH:MM" (no seconds, offset or Z). The end must be after the start; an overnight or multi-day event just ends on a later day. Its day is the event's day.
    - `end` (_string, required_): Local datetime "YYYY-MM-DDTHH:MM" (no seconds, offset or Z). The end must be after the start; an overnight or multi-day event just ends on a later day.
    - `name` (_string, required_)
    - `notes` (_string_)
    - `sourceUrl` (_string_): Where this was captured from — the http(s) page address. Shown as a source chip; never parsed. Set it only for something grabbed off a real page, not for an intention the user typed.
    - `checklist` (_object_): Starter microtasks written to the new event's checklist.
      - `items` (_object\[], required_)
        - `id` (_string_): Keep an existing item's id; omit for a new item.
        - `text` (_string, required_)
        - `done` (_boolean_): true ticks the item off, false clears it; omit to keep the current state (a new item starts not done).
    - `areaId` (_string | null_): The area id (from the schedule read). null or omitted leaves it unset.
    - `activityTypeId` (_string | null_): The activity type id (from the schedule read). null or omitted leaves it unset.
    - `projectId` (_string | null_): A live project id from get\_schedule `projects`. When the project changes and the op sets no areaId, the event takes the area of the project. null or omitted leaves it unset.
    - `priority` (_integer_): 0 none, 1 low, 2 medium, 3 high, 4 urgent. Omit for 0.
    - `recurrence` (_string_): How the event repeats, as an RRULE (e.g. "FREQ=WEEKLY;BYDAY=MO,WE,FR"). Omit for a one-off.
    - `recurrenceEnd` (_string_): Inclusive last day of the repeat, "YYYY-MM-DD". Needs `recurrence`.
    - `kind` (_"blocking" | "non\_blocking" | "reference"_): "blocking" (default) cannot overlap others; "non\_blocking" is an overlay (e.g. a fasting band or an away/OOO marker) that may overlap and never conflicts; "reference" is something the user wants to SEE but isn't doing (e.g. a kid's training they drop off at) — non-blocking, and don't move, delete, or schedule work into it unless asked.
    - `focusIntervals` (_object_): Split a BLOCKING block into a repeating focus/break rhythm (pomodoro), e.g. {focusMin:25,breakMin:5}. The breaks fall BETWEEN the focus intervals and the block always ends on a focus. A non\_blocking or reference event rejects it.
      - `focusMin` (_integer, required_)
      - `breakMin` (_integer, required_)
    - `calendarId` (_string | null_): The home calendar: a calendar id (from the calendars list or the schedule's `integrations`). Omit it to use the account's default calendar if one is set, else the event stays on the dial only. null keeps the event on the dial only. A calendar id requires Reassign Pro; whole-series only.
    - `mirrorCalendarIds` (_string\[]_): The one-way copy calendars, as calendar ids. It must not contain the home `calendarId`. On update it REPLACES the set; \[] clears it. An id already on the event may stay even if its calendar is read-only now; only an added id is checked. Adding an id requires Reassign Pro; whole-series only.
    - `mirrorStyles` (_map of "full" | "private" | "busy"_): How much each copy shows, per copy calendar id: 'full' (default) copies the title and notes; 'private' keeps them but marks the copy private; 'busy' is a shield: a private copy with a neutral title (the calendar's busy title, else "Busy"), no notes, and no reminders. Each key must be in the event's mirrorCalendarIds. A calendar left out uses that calendar's default copy style. On update it REPLACES the map; {} clears it. Needs Reassign Pro; whole-series only.
  - One form: `op: "update"`
    - `id` (_string, required_): The event id from a read. A bare series id addresses the whole series; `seriesId@YYYY-MM-DD` addresses one occurrence by its ORIGINAL date (the read's `originalDate`), also after the occurrence moved.
    - `scope` (_"future"_): Only with a `seriesId@YYYY-MM-DD` id: "future" edits that occurrence and every later one. Omit it to edit only that occurrence; a bare series id edits the whole series.
    - `start` (_string_): Local datetime "YYYY-MM-DDTHH:MM" (no seconds, offset or Z). The end must be after the start; an overnight or multi-day event just ends on a later day. A new start moves the event, also to another day (on an occurrence id, that occurrence only). A lone start keeps the duration.
    - `end` (_string_): Local datetime "YYYY-MM-DDTHH:MM" (no seconds, offset or Z). The end must be after the start; an overnight or multi-day event just ends on a later day. A lone end keeps the start.
    - `name` (_string_)
    - `notes` (_string_)
    - `sourceUrl` (_string | null_): Where this was captured from — the http(s) page address. Shown as a source chip; never parsed. Set it only for something grabbed off a real page, not for an intention the user typed.
    - `areaId` (_string | null_): The area id (from the schedule read). null clears the link; omit to keep it.
    - `activityTypeId` (_string | null_): The activity type id (from the schedule read). null clears the link; omit to keep it.
    - `projectId` (_string | null_): A live project id from get\_schedule `projects`. When the project changes and the op sets no areaId, the event takes the area of the project. null clears the project and keeps the area; omit to keep it.
    - `priority` (_integer_): 0 none, 1 low, 2 medium, 3 high, 4 urgent. Omit to keep it.
    - `recurrence` (_string | null_): A new RRULE for the series; null stops the repeat.
    - `recurrenceEnd` (_string_): Inclusive last day of the repeat, "YYYY-MM-DD". Needs `recurrence`.
    - `kind` (_"blocking" | "non\_blocking" | "reference"_): Change the event kind: "blocking", "non\_blocking", or "reference" (see-only — non-blocking, don't schedule work into it).
    - `calendarId` (_string | null_): Re-home the event: a calendar id (from the calendars list or the schedule's `integrations`) moves it to that calendar; null unlinks it and deletes its mirror copies (keeps the dial block). An unlink may carry start, end, name, notes, areaId, activityTypeId and kind, applied first in the same write. Omit to keep the current home. A calendar id requires Reassign Pro; whole-series only.
    - `mirrorCalendarIds` (_string\[]_): The one-way copy calendars, as calendar ids. It must not contain the home `calendarId`. On update it REPLACES the set; \[] clears it. An id already on the event may stay even if its calendar is read-only now; only an added id is checked. Adding an id requires Reassign Pro; whole-series only.
    - `mirrorStyles` (_map of "full" | "private" | "busy"_): How much each copy shows, per copy calendar id: 'full' (default) copies the title and notes; 'private' keeps them but marks the copy private; 'busy' is a shield: a private copy with a neutral title (the calendar's busy title, else "Busy"), no notes, and no reminders. Each key must be in the event's mirrorCalendarIds. A calendar left out uses that calendar's default copy style. On update it REPLACES the map; {} clears it. Needs Reassign Pro; whole-series only.
    - `focusIntervals` (_object | null_): Split a BLOCKING block into a repeating focus/break rhythm (pomodoro), e.g. {focusMin:25,breakMin:5}. The breaks fall BETWEEN the focus intervals and the block always ends on a focus. A non\_blocking or reference event rejects it. Pass null to remove an existing rhythm.
      - `focusMin` (_integer, required_)
      - `breakMin` (_integer, required_)
  - One form: `op: "shift"`
    - `id` (_string, required_): The event id from a read. A bare series id addresses the whole series; `seriesId@YYYY-MM-DD` addresses one occurrence by its ORIGINAL date (the read's `originalDate`), also after the occurrence moved.
    - `byMinutes` (_integer, required_): Minutes to shift by; negative is earlier.
    - `scope` (_"future"_): Only with a `seriesId@YYYY-MM-DD` id: "future" edits that occurrence and every later one. Omit it to edit only that occurrence; a bare series id edits the whole series.
  - One form: `op: "reflect"`
    - `id` (_string, required_): The event id, echoed verbatim from a schedule or search read. To mark one occurrence of a recurring event, pass the `seriesId@YYYY-MM-DD` id the read returned for that day — don't fabricate the @date yourself.
    - `status` (_"kept" | "skipped" | "changed" | "added", required_): How a past planned event actually went. "kept" = happened as planned; "skipped" = didn't happen; "changed" = happened but differently (pass actualStart + actualEnd to record the real time); "added" = unplanned but happened (its actualStart + actualEnd become the event's time). Marking only records the reflection — it never renames or re-times the plan.
    - `actualStart` (_string_): When it actually started. Local datetime "YYYY-MM-DDTHH:MM" (no seconds, offset or Z). The end must be after the start; an overnight or multi-day event just ends on a later day. Send it with actualEnd, only for "changed" or "added".
    - `actualEnd` (_string_): When it actually ended. Local datetime "YYYY-MM-DDTHH:MM" (no seconds, offset or Z). The end must be after the start; an overnight or multi-day event just ends on a later day. Send it with actualStart, only for "changed" or "added".
  - One form: `op: "checklist"`
    - `id` (_string, required_): The event id, echoed verbatim from a schedule or search read. A bare series id edits the template of the whole series; the `seriesId@YYYY-MM-DD` id edits or ticks off that one occurrence.
    - `items` (_object\[]_): REPLACE the whole checklist with these microtasks, in order — add, rename, remove, and reorder are all expressed as the resulting list. Read the current items first, then send the full desired list. Pass \[] to clear the checklist. A checkoff on a removed item is dropped automatically.
      - `id` (_string_): Keep an existing item's id; omit for a new item.
      - `text` (_string, required_)
      - `done` (_boolean_): true ticks the item off, false clears it; omit to keep the current state (a new item starts not done).
    - `check` (_string\[]_): Item ids to mark DONE for the occurrence. On a recurring event this ticks off that occurrence only.
    - `uncheck` (_string\[]_): Item ids to mark NOT-done for the occurrence.
  - One form: `op: "delete"`
    - `id` (_string, required_): The event id from a read. A bare series id addresses the whole series; `seriesId@YYYY-MM-DD` addresses one occurrence by its ORIGINAL date (the read's `originalDate`), also after the occurrence moved.
    - `scope` (_"future"_): Only with a `seriesId@YYYY-MM-DD` id: "future" edits that occurrence and every later one. Omit it to edit only that occurrence; a bare series id edits the whole series.
  - One form: `op: "clear"`
    - `from` (_string, required_): First day to clear, ISO "YYYY-MM-DD".
    - `to` (_string, required_): Last day to clear (inclusive), ISO "YYYY-MM-DD". Equal to `from` for one day.
- `partial` (_boolean_): Apply the ops that succeed instead of all or nothing.

### Response fields

- `timezone` (_string_)
- `undoToken` (_string_)
- `expiresAt` (_string_)
- `results` (_object\[], required_)
  - One form: `status: "ok"`
    - `index` (_integer, required_)
    - `result` (_object, required_)
      - One form: With `created`
        - `created` (_object, required_)
          - `start` (_string, required_)
          - `end` (_string, required_)
          - `id` (_string, required_)
          - `seriesId` (_string_)
          - `originalDate` (_string_)
          - `kind` (_"blocking" | "non\_blocking" | "reference", required_)
          - `name` (_string, required_)
          - `notes` (_string_)
          - `source` (_"google" | "google\_tasks" | "linear" | "microsoft" | "microsoft\_todo" | "ticktick" | "todoist" | "reassign", required_)
          - `sourceUrl` (_string_)
          - `meeting` (_object_)
            - `url` (_string, required_)
            - `label` (_string, required_)
          - `location` (_object_)
            - `text` (_string, required_)
            - `url` (_string_)
          - `recurrence` (_string_)
          - `warning` (_string_)
          - `areaId` (_string | null, required_)
          - `activityTypeId` (_string | null, required_)
          - `projectId` (_string | null, required_)
          - `priority` (_integer, required_)
          - `calendarId` (_string | null_)
          - `mirrorCalendarIds` (_string\[]_)
          - `mirrorStyles` (_map of "full" | "private" | "busy"_)
          - `readOnly` (_true_)
          - `focusIntervals` (_object_)
            - `focusMin` (_integer, required_)
            - `breakMin` (_integer, required_)
          - `completedFocusIntervals` (_integer_)
          - `checklist` (_object_)
            - `items` (_object\[], required_)
              - `id` (_string, required_)
              - `text` (_string, required_)
              - `done` (_boolean, required_)
          - `reflect` (_object_)
            - One form: Without `actualStart`, `actualEnd`
              - `status` (_"kept" | "skipped" | "changed" | "added", required_)
            - One form: With `actualStart`, `actualEnd`
              - `status` (_"changed", required_)
              - `actualStart` (_string, required_)
              - `actualEnd` (_string, required_)
        - `firstOccurrence` (_string_)
      - One form: With `updated`
        - `updated` (_object, required_)
          - `start` (_string, required_)
          - `end` (_string, required_)
          - `id` (_string, required_)
          - `seriesId` (_string_)
          - `originalDate` (_string_)
          - `kind` (_"blocking" | "non\_blocking" | "reference", required_)
          - `name` (_string, required_)
          - `notes` (_string_)
          - `source` (_"google" | "google\_tasks" | "linear" | "microsoft" | "microsoft\_todo" | "ticktick" | "todoist" | "reassign", required_)
          - `sourceUrl` (_string_)
          - `meeting` (_object_)
            - `url` (_string, required_)
            - `label` (_string, required_)
          - `location` (_object_)
            - `text` (_string, required_)
            - `url` (_string_)
          - `recurrence` (_string_)
          - `warning` (_string_)
          - `areaId` (_string | null, required_)
          - `activityTypeId` (_string | null, required_)
          - `projectId` (_string | null, required_)
          - `priority` (_integer, required_)
          - `calendarId` (_string | null_)
          - `mirrorCalendarIds` (_string\[]_)
          - `mirrorStyles` (_map of "full" | "private" | "busy"_)
          - `readOnly` (_true_)
          - `focusIntervals` (_object_)
            - `focusMin` (_integer, required_)
            - `breakMin` (_integer, required_)
          - `completedFocusIntervals` (_integer_)
          - `checklist` (_object_)
            - `items` (_object\[], required_)
              - `id` (_string, required_)
              - `text` (_string, required_)
              - `done` (_boolean, required_)
          - `reflect` (_object_)
            - One form: Without `actualStart`, `actualEnd`
              - `status` (_"kept" | "skipped" | "changed" | "added", required_)
            - One form: With `actualStart`, `actualEnd`
              - `status` (_"changed", required_)
              - `actualStart` (_string, required_)
              - `actualEnd` (_string, required_)
        - `firstOccurrence` (_string_)
      - One form: With `deletedIds`
        - `deletedIds` (_string\[], required_)
        - `updated` (_object_)
          - `start` (_string, required_)
          - `end` (_string, required_)
          - `id` (_string, required_)
          - `seriesId` (_string_)
          - `originalDate` (_string_)
          - `kind` (_"blocking" | "non\_blocking" | "reference", required_)
          - `name` (_string, required_)
          - `notes` (_string_)
          - `source` (_"google" | "google\_tasks" | "linear" | "microsoft" | "microsoft\_todo" | "ticktick" | "todoist" | "reassign", required_)
          - `sourceUrl` (_string_)
          - `meeting` (_object_)
            - `url` (_string, required_)
            - `label` (_string, required_)
          - `location` (_object_)
            - `text` (_string, required_)
            - `url` (_string_)
          - `recurrence` (_string_)
          - `warning` (_string_)
          - `areaId` (_string | null, required_)
          - `activityTypeId` (_string | null, required_)
          - `projectId` (_string | null, required_)
          - `priority` (_integer, required_)
          - `calendarId` (_string | null_)
          - `mirrorCalendarIds` (_string\[]_)
          - `mirrorStyles` (_map of "full" | "private" | "busy"_)
          - `readOnly` (_true_)
          - `focusIntervals` (_object_)
            - `focusMin` (_integer, required_)
            - `breakMin` (_integer, required_)
          - `completedFocusIntervals` (_integer_)
          - `checklist` (_object_)
            - `items` (_object\[], required_)
              - `id` (_string, required_)
              - `text` (_string, required_)
              - `done` (_boolean, required_)
          - `reflect` (_object_)
            - One form: Without `actualStart`, `actualEnd`
              - `status` (_"kept" | "skipped" | "changed" | "added", required_)
            - One form: With `actualStart`, `actualEnd`
              - `status` (_"changed", required_)
              - `actualStart` (_string, required_)
              - `actualEnd` (_string, required_)
      - One form: With `breakdown`, `items`, `truncated`
        - `breakdown` (_object, required_)
          - `oneOff` (_integer, required_)
          - `recurringOccurrence` (_integer, required_)
          - `override` (_integer, required_)
        - `items` (_object\[], required_)
          - `date` (_string, required_)
          - `name` (_string, required_)
        - `truncated` (_boolean, required_)
    - `warnings` (_"classification\_pending"\[]_)
  - One form: `status: "error"`
    - `index` (_integer, required_)
    - `error` (_object, required_)
      - `code` (_"conflict" | "not\_found" | "validation" | "permission" | "scope" | "stale" | "read\_only" | "rate\_limited" | "internal", required_)
      - `message` (_string, required_)
      - `issues` (_object\[]_)
        - `path` (_string | integer\[], required_)
        - `message` (_string, required_)
        - `expected` (_string_)
      - `feature` (_string_)
      - `conflictingRows` (_object\[]_)
        - `entity` (_string, required_)
        - `id` (_string, required_)
        - `reason` (_"changed" | "deleted" | "newly\_present", required_)
      - `conflictingRowsTruncated` (_true_)
      - `usage` (_object_)
        - `events` (_integer, required_)
        - `inboxItems` (_integer, required_)
        - `trackers` (_integer, required_)
        - `calendars` (_integer, required_)
        - `projects` (_integer_)
      - `reassignTargets` (_object\[]_)
        - `id` (_string, required_)
        - `name` (_string, required_)
      - `unlinkedCheckout` (_object_)
        - `sessionId` (_string, required_)
        - `paymentReceived` (_boolean, required_)
        - `supportUrl` (_string, required_)
      - `conflicts` (_object\[]_)
        - One form: With `id`
          - `start` (_string, required_)
          - `end` (_string, required_)
          - `id` (_string, required_)
          - `name` (_string, required_)
        - One form: With `batchIndex`
          - `start` (_string, required_)
          - `end` (_string, required_)
          - `batchIndex` (_integer, required_)
          - `name` (_string, required_)
      - `nearestSlots` (_object\[]_)
        - `start` (_string, required_)
        - `end` (_string, required_)
        - `score` (_number_)
        - `reason` (_string_)
  - One form: `status: "skipped"`
    - `index` (_integer, required_)
    - `reason` (_string, required_)

### Responses

| Status | Codes                              | Meaning                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| ------ | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `200`  | —                                  | Success.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `400`  | `batch_rejected`                   | Every op failed, and the failed ops have different codes. The code is `batch_rejected`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `401`  | `unauthorized`                     | No Bearer token and no session cookie, or the token is invalid, expired, or revoked. The response carries a `WWW-Authenticate` challenge that points at the protected-resource metadata.                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `403`  | `permission`, `scope`, `read_only` | A token without `events:write` gets `scope` "This token is not authorized to modify events (missing events:write scope)." An account without Reassign Pro access gets `permission` "Reassign needs an active subscription." The 7-day free trial includes Reassign Pro. A cookie session from another site gets `permission` "Cross-origin request." A rejected batch also answers 403 when every failed op has the same code: `read_only` (the event or the target calendar is not writable, or the op changes the kind of an all-day calendar event) or `permission` (a calendar sync needs Reassign Pro). |
| `404`  | `not_found`                        | An op names an item that does not exist.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `409`  | `conflict`                         | The request conflicts with the current state: `conflict` "The time overlaps an existing event." or `conflict` "The time overlaps an earlier op in this batch." or `conflict` "An event with this ID already exists."                                                                                                                                                                                                                                                                                                                                                                                         |
| `422`  | `validation`                       | The request body is not valid. The message names the first bad field by its path. A repeated or unknown query parameter is also a 422.                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `429`  | `rate_limited`                     | Too many requests. The edge firewall limits each IP to 2000 requests per minute across `/api/v1`; that 429 comes from the edge and can have a body that is not this JSON and no `Retry-After` header. When an edge 429 has no `Retry-After`, retry after 60 seconds. The app also limits each Bearer credential to 600 requests per minute, and some routes limit each user. These limits return this body. This app 429 always has `Retry-After`: retry after that number of seconds.                                                                                                                       |
| `503`  | `internal`                         | A fault on our side. The request is safe to retry.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |

Most error responses: When no op of the batch applied, the body also has the `results` rows, so the caller sees which op failed.

**Request** — **POST** `/api/v1/events`

**cURL**

```bash
curl -X POST "https://reassign.app/api/v1/events" \
  -H "Authorization: Bearer rsk_live_YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "ops": [
    {
      "op": "create",
      "name": "Deep work",
      "start": "2026-08-14T09:00",
      "end": "2026-08-14T10:30",
      "areaId": "3c9d2a41-6b7e-4f80-9a1b-2c3d4e5f6071",
      "activityTypeId": "5e6f7081-92a3-4b4c-8d5e-6f708192a3b4",
      "notes": "Draft the launch post.",
      "calendarId": "8a1f0c2e-3b4d-4e5f-9a0b-1c2d3e4f5061"
    },
    {
      "op": "update",
      "id": "c4d5e6f7-0819-4a2b-8c3d-4e5f60718293@2026-08-15",
      "start": "2026-08-15T16:00"
    },
    {
      "op": "delete",
      "id": "c4d5e6f7-0819-4a2b-8c3d-4e5f60718293@2026-08-16"
    }
  ]
}'
```

**JavaScript**

```js
const res = await fetch("https://reassign.app/api/v1/events", {
  method: "POST",
  headers: {
    Authorization: "Bearer rsk_live_YOUR_TOKEN",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "ops": [
      {
        "op": "create",
        "name": "Deep work",
        "start": "2026-08-14T09:00",
        "end": "2026-08-14T10:30",
        "areaId": "3c9d2a41-6b7e-4f80-9a1b-2c3d4e5f6071",
        "activityTypeId": "5e6f7081-92a3-4b4c-8d5e-6f708192a3b4",
        "notes": "Draft the launch post.",
        "calendarId": "8a1f0c2e-3b4d-4e5f-9a0b-1c2d3e4f5061"
      },
      {
        "op": "update",
        "id": "c4d5e6f7-0819-4a2b-8c3d-4e5f60718293@2026-08-15",
        "start": "2026-08-15T16:00"
      },
      {
        "op": "delete",
        "id": "c4d5e6f7-0819-4a2b-8c3d-4e5f60718293@2026-08-16"
      }
    ]
  }),
});
const data = await res.json();
```

**Response**

```json
{
  "timezone": "Europe/Ljubljana",
  "undoToken": "00000000-0000-4000-8000-000000000001",
  "expiresAt": "2026-08-13T09:12:00.000Z",
  "results": [
    {
      "index": 0,
      "status": "ok",
      "result": {
        "created": {
          "id": "b7f1c2a0-1d2e-4f3a-9b8c-5d6e7f801234",
          "start": "2026-08-14T09:00",
          "end": "2026-08-14T10:30",
          "kind": "blocking",
          "name": "Deep work",
          "notes": "Draft the launch post.",
          "source": "reassign",
          "areaId": "3c9d2a41-6b7e-4f80-9a1b-2c3d4e5f6071",
          "activityTypeId": "5e6f7081-92a3-4b4c-8d5e-6f708192a3b4",
          "projectId": null,
          "priority": 0,
          "calendarId": "8a1f0c2e-3b4d-4e5f-9a0b-1c2d3e4f5061"
        }
      }
    },
    {
      "index": 1,
      "status": "ok",
      "result": {
        "updated": {
          "id": "c4d5e6f7-0819-4a2b-8c3d-4e5f60718293@2026-08-15",
          "seriesId": "c4d5e6f7-0819-4a2b-8c3d-4e5f60718293",
          "originalDate": "2026-08-15",
          "start": "2026-08-15T16:00",
          "end": "2026-08-15T17:30",
          "kind": "blocking",
          "name": "Deep work",
          "recurrence": "FREQ=DAILY",
          "source": "reassign",
          "areaId": "3c9d2a41-6b7e-4f80-9a1b-2c3d4e5f6071",
          "activityTypeId": "5e6f7081-92a3-4b4c-8d5e-6f708192a3b4",
          "projectId": null,
          "priority": 0
        }
      }
    },
    {
      "index": 2,
      "status": "ok",
      "result": {
        "deletedIds": [
          "c4d5e6f7-0819-4a2b-8c3d-4e5f60718293@2026-08-16"
        ]
      }
    }
  ]
}
```

---

## Get an event

**GET** `/api/v1/events/{id}`

Get one event and the `timezone` of its span. Scope: `events:read`.

- A one-off event: send its id.
- One occurrence of a series: send the occurrence id `seriesId@YYYY-MM-DD`.
- A bare series id returns 404 `not_found`. The API looks for the id in the events of the first day of the series, and there each occurrence has an occurrence id.
- A changed occurrence keeps its id `seriesId@<original date>`. This id finds the occurrence also after a move to another day.
- A stored id of a changed occurrence from an older write result also works. The response then gives `event.id` as `seriesId@<original date>`.
- An unknown id returns 404 with the message `Event not found.`

### Path parameters

- `id` (_string, required_): The event id, or `seriesId@YYYY-MM-DD` for one occurrence.

### Response fields

- `event` (_object, required_)
  - `start` (_string, required_)
  - `end` (_string, required_)
  - `id` (_string, required_)
  - `seriesId` (_string_)
  - `originalDate` (_string_)
  - `kind` (_"blocking" | "non\_blocking" | "reference", required_)
  - `name` (_string, required_)
  - `notes` (_string_)
  - `source` (_"google" | "google\_tasks" | "linear" | "microsoft" | "microsoft\_todo" | "ticktick" | "todoist" | "reassign", required_)
  - `sourceUrl` (_string_)
  - `meeting` (_object_)
    - `url` (_string, required_)
    - `label` (_string, required_)
  - `location` (_object_)
    - `text` (_string, required_)
    - `url` (_string_)
  - `recurrence` (_string_)
  - `warning` (_string_)
  - `areaId` (_string | null, required_)
  - `activityTypeId` (_string | null, required_)
  - `projectId` (_string | null, required_)
  - `priority` (_integer, required_)
  - `calendarId` (_string | null_)
  - `mirrorCalendarIds` (_string\[]_)
  - `mirrorStyles` (_map of "full" | "private" | "busy"_)
  - `readOnly` (_true_)
  - `focusIntervals` (_object_)
    - `focusMin` (_integer, required_)
    - `breakMin` (_integer, required_)
  - `completedFocusIntervals` (_integer_)
  - `checklist` (_object_)
    - `items` (_object\[], required_)
      - `id` (_string, required_)
      - `text` (_string, required_)
      - `done` (_boolean, required_)
  - `reflect` (_object_)
    - One form: Without `actualStart`, `actualEnd`
      - `status` (_"kept" | "skipped" | "changed" | "added", required_)
    - One form: With `actualStart`, `actualEnd`
      - `status` (_"changed", required_)
      - `actualStart` (_string, required_)
      - `actualEnd` (_string, required_)
- `timezone` (_string, required_)

### Responses

| Status | Codes                 | Meaning                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| ------ | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `200`  | —                     | Success.                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `401`  | `unauthorized`        | No Bearer token and no session cookie, or the token is invalid, expired, or revoked. The response carries a `WWW-Authenticate` challenge that points at the protected-resource metadata.                                                                                                                                                                                                                                                                                               |
| `403`  | `permission`, `scope` | A token without `events:read` or `events:write` gets `scope` "This token is not authorized to read events (requires events:read or events:write scope)." An account without Reassign Pro access gets `permission` "Reassign needs an active subscription." The 7-day free trial includes Reassign Pro. A cookie session from another site gets `permission` "Cross-origin request."                                                                                                    |
| `404`  | `not_found`           | The resource that the request names does not exist.                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `422`  | `validation`          | The request has a query parameter; this operation takes none.                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `429`  | `rate_limited`        | Too many requests. The edge firewall limits each IP to 2000 requests per minute across `/api/v1`; that 429 comes from the edge and can have a body that is not this JSON and no `Retry-After` header. When an edge 429 has no `Retry-After`, retry after 60 seconds. The app also limits each Bearer credential to 600 requests per minute, and some routes limit each user. These limits return this body. This app 429 always has `Retry-After`: retry after that number of seconds. |
| `503`  | `internal`            | A fault on our side. The request is safe to retry.                                                                                                                                                                                                                                                                                                                                                                                                                                     |

**Request** — **GET** `/api/v1/events/{id}`

**cURL**

```bash
curl "https://reassign.app/api/v1/events/EVENT_ID" \
  -H "Authorization: Bearer rsk_live_YOUR_TOKEN"
```

**JavaScript**

```js
const res = await fetch("https://reassign.app/api/v1/events/EVENT_ID", {
  headers: {
    Authorization: "Bearer rsk_live_YOUR_TOKEN",
  },
});
const data = await res.json();
```

**Response**

```json
{
  "event": {
    "id": "b7f1c2a0-1d2e-4f3a-9b8c-5d6e7f801234",
    "start": "2026-08-13T09:00",
    "end": "2026-08-13T10:30",
    "kind": "blocking",
    "name": "Team sync",
    "source": "google",
    "meeting": {
      "url": "https://us02web.zoom.us/j/1234567890",
      "label": "Zoom"
    },
    "location": {
      "text": "Room 4, 2nd floor",
      "url": "https://www.google.com/maps/search/?api=1&query=Room%204%2C%202nd%20floor"
    },
    "areaId": "3c9d2a41-6b7e-4f80-9a1b-2c3d4e5f6071",
    "activityTypeId": "5e6f7081-92a3-4b4c-8d5e-6f708192a3b4",
    "projectId": null,
    "priority": 0,
    "calendarId": "8a1f0c2e-3b4d-4e5f-9a0b-1c2d3e4f5061",
    "mirrorCalendarIds": [
      "a1b2c3d4-5e6f-4071-8293-a4b5c6d7e8f9"
    ]
  },
  "timezone": "Europe/Ljubljana"
}
```

---

## Search events

**GET** `/api/v1/events/search`

Find events by name. The search does not look in the notes. Scope: `events:read`.

- `query` is required. An empty value returns 422 `validation`.

- The default window is from 7 days before today to 30 days after `from` (or after today).

- With only `to`, the window starts 7 days before today. If `to` is less than 30 days after that start, the window starts 30 days before `to`. If the window is then more than 92 days, it starts 91 days before `to`.

- A `to` before `from` returns 422 with the message `` `to` must be on or after `from`. ``

- The window is 92 days or less. A wider window returns 422 with the message `Range too wide (max 92 days).`

- The window includes each event that overlaps a day of the window. This includes an overnight event that started the day before. An event that ends exactly at `00:00` of the first day is not in the window. `timeOfDay` still uses the start time of the event.

- `timeOfDay` keeps the events that start in that part of the day:

  | `timeOfDay` | Start time      |
  | ----------- | --------------- |
  | `morning`   | 05:00 to 12:00  |
  | `afternoon` | 12:00 to 17:00  |
  | `evening`   | 17:00 to 21:00  |
  | `night`     | All other times |

- Each event in `events` has the full event model and a `score` from 0 to 1 for its name match. A series shows one time, as the occurrence that matches best, with its occurrence id. Thus a query that names a renamed occurrence gets that occurrence. When more occurrences match equally well, the search gives the one nearest to today: the first on or after today, else the last before today.

- A `score` of `1` is an exact match, `0.9` is a prefix, and `0.75` is a substring. Matches below `0.34` are omitted. Results are best match first.

- `ambiguous` is `true` when more than one event matches equally well.

### Query parameters

- `query` (_string, required_): Text to match against event names, e.g. "standup", "gym".
- `from` (_string_): Range start, ISO date, e.g. "2026-06-01". Defaults to 7 days ago.
- `to` (_string_): Range end, ISO date, e.g. "2026-07-01". Defaults to +30 days.
- `areaId` (_string_): Only match events linked to this area id.
- `activityTypeId` (_string_): Only match events linked to this activity-type id.
- `timeOfDay` (_"morning" | "afternoon" | "evening" | "night"_): Only match events that start in this part of the day.

### Response fields

- `events` (_object\[], required_)
  - `start` (_string, required_)
  - `end` (_string, required_)
  - `id` (_string, required_)
  - `seriesId` (_string_)
  - `originalDate` (_string_)
  - `kind` (_"blocking" | "non\_blocking" | "reference", required_)
  - `name` (_string, required_)
  - `notes` (_string_)
  - `source` (_"google" | "google\_tasks" | "linear" | "microsoft" | "microsoft\_todo" | "ticktick" | "todoist" | "reassign", required_)
  - `sourceUrl` (_string_)
  - `meeting` (_object_)
    - `url` (_string, required_)
    - `label` (_string, required_)
  - `location` (_object_)
    - `text` (_string, required_)
    - `url` (_string_)
  - `recurrence` (_string_)
  - `warning` (_string_)
  - `areaId` (_string | null, required_)
  - `activityTypeId` (_string | null, required_)
  - `projectId` (_string | null, required_)
  - `priority` (_integer, required_)
  - `calendarId` (_string | null_)
  - `mirrorCalendarIds` (_string\[]_)
  - `mirrorStyles` (_map of "full" | "private" | "busy"_)
  - `readOnly` (_true_)
  - `focusIntervals` (_object_)
    - `focusMin` (_integer, required_)
    - `breakMin` (_integer, required_)
  - `completedFocusIntervals` (_integer_)
  - `checklist` (_object_)
    - `items` (_object\[], required_)
      - `id` (_string, required_)
      - `text` (_string, required_)
      - `done` (_boolean, required_)
  - `reflect` (_object_)
    - One form: Without `actualStart`, `actualEnd`
      - `status` (_"kept" | "skipped" | "changed" | "added", required_)
    - One form: With `actualStart`, `actualEnd`
      - `status` (_"changed", required_)
      - `actualStart` (_string, required_)
      - `actualEnd` (_string, required_)
  - `score` (_number, required_)
- `ambiguous` (_boolean, required_)
- `timezone` (_string, required_)

### Responses

| Status | Codes                 | Meaning                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| ------ | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `200`  | —                     | Success.                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `401`  | `unauthorized`        | No Bearer token and no session cookie, or the token is invalid, expired, or revoked. The response carries a `WWW-Authenticate` challenge that points at the protected-resource metadata.                                                                                                                                                                                                                                                                                               |
| `403`  | `permission`, `scope` | A token without `events:read` or `events:write` gets `scope` "This token is not authorized to read events (requires events:read or events:write scope)." An account without Reassign Pro access gets `permission` "Reassign needs an active subscription." The 7-day free trial includes Reassign Pro. A cookie session from another site gets `permission` "Cross-origin request."                                                                                                    |
| `422`  | `validation`          | A query parameter is not valid. The message names the first bad field by its path. A repeated or unknown query parameter is also a 422.                                                                                                                                                                                                                                                                                                                                                |
| `429`  | `rate_limited`        | Too many requests. The edge firewall limits each IP to 2000 requests per minute across `/api/v1`; that 429 comes from the edge and can have a body that is not this JSON and no `Retry-After` header. When an edge 429 has no `Retry-After`, retry after 60 seconds. The app also limits each Bearer credential to 600 requests per minute, and some routes limit each user. These limits return this body. This app 429 always has `Retry-After`: retry after that number of seconds. |
| `503`  | `internal`            | A fault on our side. The request is safe to retry.                                                                                                                                                                                                                                                                                                                                                                                                                                     |

**Request** — **GET** `/api/v1/events/search`

**cURL**

```bash
curl "https://reassign.app/api/v1/events/search?query=SEARCH_ID" \
  -H "Authorization: Bearer rsk_live_YOUR_TOKEN"
```

**JavaScript**

```js
const res = await fetch("https://reassign.app/api/v1/events/search?query=SEARCH_ID", {
  headers: {
    Authorization: "Bearer rsk_live_YOUR_TOKEN",
  },
});
const data = await res.json();
```

**Response**

```json
{
  "events": [],
  "ambiguous": false,
  "timezone": "Europe/Ljubljana"
}
```
