# Categories

> Read, create, update, and delete the areas, activity types, and projects of a Reassign account.

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

Categories label the events of a day plan and the Inbox. Reassign has two types of category: areas and activity types. Projects group events and Inbox blocks. Use these endpoints to read and change all three.

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

## Areas and activity types

| Category      | What it is                                           | How Reassign shows it |
| ------------- | ---------------------------------------------------- | --------------------- |
| Area          | A category or group, for example "Work" or "Health". | A color               |
| Activity type | A tag, for example "Focus".                          | A fill pattern        |

- An event links to a maximum of one area and a maximum of one activity type.
- Some areas and activity types are shared defaults. For these, `isGlobal` is `true`. `isGlobal` is always present.
- You can delete a shared default. The delete hides it for your account only. Refer to [Delete a shared default](https://reassign.dev/reference/categories.md#delete-a-shared-default).
- When you update a shared default, Reassign makes your own copy (a fork). The fork gets a new ID. Its `overridesId` is the ID of the shared default.
- If you already have a fork of that default, Reassign updates the fork. It does not make a second fork.
- A fork takes the links of your events, Inbox items, trackers, and projects from the shared default. A calendar default stays on the shared default, and reads show the fork.
- A write that names a shared default that you forked uses your fork. This applies to `areaId` and `activityTypeId` on events, the backlog, and scheduling, and to `reassignTo` on a delete. The result gives the fork id.
- Names are unique for each type of category, among the categories that you can see. To compare names, Reassign ignores uppercase and lowercase, accents, and extra spaces. A create or a rename to a name that is already in use fails with `conflict` (409).

Use the IDs from this page in the `areaId` and `activityTypeId` fields of [events](https://reassign.dev/reference/events.md) and of the [backlog](https://reassign.dev/reference/backlog.md). Use a project ID in the `projectId` field of events and of the backlog.

## The area model

- `color` is a hex color with 6 lowercase digits, `#rrggbb`, for example `#3b82f6`.
- `overridesId` is always present. It is `null` when the area is not a fork.
- `order` is the display order, always a whole number. Refer to [Display order](https://reassign.dev/reference/categories.md#display-order).

* `id` (_string, required_)
* `name` (_string, required_)
* `order` (_integer, required_)
* `isGlobal` (_boolean, required_)
* `overridesId` (_string | null, required_)
* `color` (_string, required_)

## The activity type model

- `pattern` is the fill pattern. The values use snake\_case, for example `hatch_r` and `hatch_l`.
- `overridesId` is always present. It is `null` when the activity type is not a fork.
- `order` is the display order, always a whole number. Refer to [Display order](https://reassign.dev/reference/categories.md#display-order).

* `id` (_string, required_)
* `name` (_string, required_)
* `order` (_integer, required_)
* `isGlobal` (_boolean, required_)
* `overridesId` (_string | null, required_)
* `pattern` (_"solid" | "hatch\_r" | "hatch\_l" | "cross" | "horizontal" | "vertical" | "grid" | "dots" | "waves" | "chevron", required_)

## Projects

A project is a named goal, for example "Launch". It can be in one area, or in no area. Events and Inbox blocks link to a project with `projectId`.

- A project is your own. Reassign has no shared default project, and a project has no fork.
- A project links to a maximum of one area. It takes its color from that area.
- A synced task list or a Linear project can feed a project. Its Inbox items link to that project.
- A sync links a list or a Linear project to the live project with the same name, in any area. Thus one project can take lists from two or more task apps or accounts.
- When no live project has the name of a task list, the sync makes a new project in the default area of the list. A task list always feeds a project.
- To give a list or a Linear project its own project, use the [`split` op](https://reassign.dev/reference/categories.md#project-ops). To move a Linear project to a different project, use the `relink` op.
- An archived project stays in the list, with `archived: true`. It takes no new links, but its links stay.

## The project model

- `areaId` is the parent area. It is `null` when the project has no area.
- `archived` is `true` for an archived project.
- `publishCalendarId` is the task list that gets a new Inbox block of the project. It is `null` when there is no such list. Refer to [Source of a new block](https://reassign.dev/reference/backlog.md#source-of-a-new-block).
- `order` is the display order. It can be any number, not only a whole number.

* `id` (_string, required_)
* `name` (_string, required_)
* `areaId` (_string | null, required_)
* `order` (_number, required_)
* `archived` (_boolean, required_)
* `publishCalendarId` (_string | null, required_): The task list that a new Inbox item of the project is created in; null is off.

## Display order

`order` is a whole number that sets the position of an area or an activity type in the lists of the app. Areas and activity types each have their own order.

| Rule               | Details                                                                                                                                                                                                                                        |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Sort               | Reassign sorts the list from the lowest `order` to the highest. `GET /api/v1/categories` returns each list in this sequence.                                                                                                                   |
| Value on a create  | A `create` op does not take `order`. Reassign sets `order` to the number of categories of that type that you can see before the create. When no other category has this `order` or a higher one, the new category goes to the end of the list. |
| Value on an update | An `update` op can set `order` to a whole number. Reassign writes only that category. It does not change the `order` of the other categories. Two categories can have the same `order`.                                                        |
| Shared default     | An `update` with `order` on a shared default makes a fork, as for a change of name. The fork gets the new `order`.                                                                                                                             |

To move a category, send an `update` op with a new `order` for each category that must move.

Projects have their own order, with these differences:

- A project `create` puts the project at the end: its `order` is 1 more than the highest `order` of your projects.
- A project `update` can set `order` to any number, for example `1.5`.
- The `projects` list is in `order`. Two projects with the same `order` are in the sequence that you made them.

---

## List the categories

**GET** `/api/v1/categories`

Get all the areas and activity types that the account can see, and all the projects of the account. When the account has a fork of a shared default, the list shows only the fork.

- The token must have the `events:read` or the `events:write` scope.
- The account needs Reassign Pro. The 7-day free trial includes it.
- This endpoint takes no query parameter. An unknown query parameter returns `422` with the code `validation`.
- `areas` holds [area objects](https://reassign.dev/reference/categories.md#the-area-model). `activityTypes` holds [activity type objects](https://reassign.dev/reference/categories.md#the-activity-type-model). `projects` holds [project objects](https://reassign.dev/reference/categories.md#the-project-model).
- `projects` includes the archived projects.
- Each list is in [display order](https://reassign.dev/reference/categories.md#display-order): the lowest `order` first.

### Response fields

- `areas` (_object\[], required_)
  - `id` (_string, required_)
  - `name` (_string, required_)
  - `order` (_integer, required_)
  - `isGlobal` (_boolean, required_)
  - `overridesId` (_string | null, required_)
  - `color` (_string, required_)
- `activityTypes` (_object\[], required_)
  - `id` (_string, required_)
  - `name` (_string, required_)
  - `order` (_integer, required_)
  - `isGlobal` (_boolean, required_)
  - `overridesId` (_string | null, required_)
  - `pattern` (_"solid" | "hatch\_r" | "hatch\_l" | "cross" | "horizontal" | "vertical" | "grid" | "dots" | "waves" | "chevron", required_)
- `projects` (_object\[], required_)
  - `id` (_string, required_)
  - `name` (_string, required_)
  - `areaId` (_string | null, required_)
  - `order` (_number, required_)
  - `archived` (_boolean, required_)
  - `publishCalendarId` (_string | null, required_): The task list that a new Inbox item of the project is created in; null is off.

### 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`          | 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/categories`

**cURL**

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

**JavaScript**

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

**Response**

```json
{
  "areas": [
    {
      "id": "3c9d2a41-6b7e-4f80-9a1b-2c3d4e5f6071",
      "name": "Work",
      "color": "#3b82f6",
      "order": 0,
      "isGlobal": false,
      "overridesId": null
    }
  ],
  "activityTypes": [
    {
      "id": "5e6f7081-92a3-4b4c-8d5e-6f708192a3b4",
      "name": "Focus",
      "pattern": "solid",
      "order": 0,
      "isGlobal": false,
      "overridesId": null
    }
  ],
  "projects": [
    {
      "id": "f3a4b5c6-d7e8-4f90-a1b2-c3d4e5f60718",
      "name": "Launch",
      "areaId": "3c9d2a41-6b7e-4f80-9a1b-2c3d4e5f6071",
      "order": 0,
      "archived": false,
      "publishCalendarId": null
    }
  ]
}
```

---

## Change the categories

**POST** `/api/v1/categories`

Create, update, or delete areas, activity types, and projects in one request. Each item in `areas`, `activityTypes`, and `projects` is one operation (op). The `op` field of the item gives the action.

- The token must have the `events:write` scope.
- The account needs Reassign Pro. The 7-day free trial includes it.
- Send one or more ops in `areas`, `activityTypes`, `projects`, or in a combination of them. A body with no op returns `422` with the message `Provide at least one area, activity-type or project op.`
- Reassign runs the project ops after the area and activity type ops.
- By default, the request is atomic. If one op fails, Reassign writes no change. Set `partial` to `true` to apply the valid ops and to skip the ops that fail.

### Validation

Reassign checks the body before it runs an op.

- A body that is not valid as a whole returns `422` with the code `validation` and no rows. For example, `areas` is not an array, an array has more than 50 ops, or the body has an unknown key.
- An op that is not valid becomes an `error` row with the code `validation`. The request then follows the rules in "Status of a failed request" below.
- An atomic request with an op that is not valid writes nothing. When each failed op is not valid, the status is `422`, and the body has the rows.
- In `areas` and `activityTypes`, `op` must be `create`, `update`, or `delete`. An unknown `op` gets the message `Unknown op "<op>". Use one of: create, update, delete.`
- In `projects`, `op` must be `create`, `update`, `delete`, `split`, or `relink`. An unknown `op` gets the message `Unknown op "<op>". Use one of: create, update, delete, split, relink.`
- `name` has 1 to 60 characters. This also applies to a project.
- `color` must be a hex color with 6 lowercase digits, for example `#22c55e`. Reassign does not convert other color formats.
- `pattern` must be one of the values in [the activity type model](https://reassign.dev/reference/categories.md#the-activity-type-model).
- Reassign rejects a field that it does not know. An area op with `pattern` or an activity type op with `color` is not valid.
- The message of a bad op names the op and the first bad field, for example ``Invalid "create" op. `name`: Too small: expected string to have >=1 characters``. An op that is not an object gets the message `Each op must be an object.`

### Ops

| Op       | What it does                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `create` | Makes an area or an activity type. The name must not be blank, and it must not be the same as the name of a category of the same type. With no `color`, Reassign selects a color that no area uses, if possible. With no `pattern`, Reassign selects a pattern that no activity type uses, if possible. The op can carry its own `id`. With no `id`, Reassign makes a UUID.                                                                                |
| `update` | Changes `name`, `color` (areas), `pattern` (activity types), or `order`. Send one or more of them. With none of them, the op fails with `Provide a new name, color and/or order to update.` (or `… pattern and/or order …` for an activity type). When you update a shared default, the result has a new `id` and a `forkedFrom` field. Use the new `id` after this.                                                                                       |
| `delete` | Deletes an area or an activity type. For a shared default or your fork of it, the delete hides the default for your account only. When events, Inbox items, trackers, or calendar defaults use the category, send `reassignTo`. An ID of a different category of the same type moves the links to it. `null` clears the links. Refer to [Delete a category that items use](https://reassign.dev/reference/categories.md#delete-a-category-that-items-use). |

### Project ops

| Op       | What it does                                                                                                                                                                                                                                        |
| -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `create` | Makes a project with `name` and an optional `areaId`. With no `areaId`, the project has no area. The op can carry its own `id`. You cannot set `publishCalendarId` on a create.                                                                     |
| `update` | Changes `name`, `areaId`, `order`, `archived`, or `publishCalendarId`, by the `id` of the project. Send one or more of them. With none of them, the op fails with `Provide a new name, areaId, order, archived and/or publishCalendarId to update.` |
| `delete` | Deletes a project by its `id`. Its Inbox blocks and events keep their area and lose the project link.                                                                                                                                               |
| `split`  | Moves one task list or Linear project (`linkId`) that feeds the project `id` to a new project of its own.                                                                                                                                           |
| `relink` | Moves one Linear project (`linkId`) to the live project `id`.                                                                                                                                                                                       |

#### Project names

- A project name is unique among all your live projects, in all areas. An archived project does not block a name.
- To compare names, Reassign ignores uppercase and lowercase and extra spaces. Unlike areas and activity types, it does not ignore accents.
- A create or a rename to a name in use fails with `conflict`. The message names the other project and its area, for example `A project named Launch exists in Work.` For a project with no area, the area is `No area`.

#### Update a project

- `areaId` takes the ID of an area that you can see, or `null` for no area. A shared default that you forked gives your fork.
- `archived: true` archives the project. An archived project takes no new links, but its links stay. `archived: false` restores it.
- `publishCalendarId` takes a task list that feeds the project and can take a new item, or `null` to turn it off. A new Inbox block of the project goes to this list, unless the block names its own list.
- A change of `areaId` does not change the area of the Inbox blocks that the project has now.

#### Delete a project

- A project that a task list or a Linear project feeds fails the delete with `conflict`. A sync would make the project again.
- For a task list, the message is `Link the task list to a different project first.`
- For a Linear project, the message is `Link the Linear project to a different project first, or archive this project.` Use `relink`, or archive the project.

#### Split a project

- `linkId` is the `id` of a task list or of a Linear project that feeds the project `id` now.
- The new project gets the name `<list name> (<Provider>)`, for example `Launch (Todoist)`. Reassign cuts a long name to fit in 60 characters.
- When a live project has that name, the new project gets ` 2`, ` 3`, and so on at the end.
- The new project goes in the area of the old project.
- The Inbox blocks of the list or the Linear project move to the new project. A later sync keeps the list or the Linear project in the new project.
- When the old project used the list as its `publishCalendarId`, the split sets that field to `null`. For a Linear project, the split does the same when that field is a team of the Linear project, and no other Linear project of the old project holds that team.
- A `linkId` that does not feed the project fails with `not_found`: `Task list or Linear project not found, or it does not feed this project.`

#### Relink a Linear project

- `linkId` is the `id` of a Linear project. `id` is a live project, not an archived one.
- The Inbox blocks of the Linear project move to the project `id`. The tasks in Linear do not change.
- When the Linear project fed no project before, only its Inbox blocks with no project move.
- When the `publishCalendarId` of the old project is a team of the Linear project, and no other Linear project of the old project holds that team, the relink sets that field to `null`.
- A task list changes its project in its list settings, not with `relink`. A `linkId` that is not a Linear project fails with `not_found`: `Linear project not found or not accessible. A task list changes its project in its list settings.`
- When the Linear project feeds `id` already, the op changes nothing. Its result has `noop: true`, and the op records no undo.
- No public endpoint gives the `linkId` of a Linear project now. Only the Reassign app can read it.

### Results

Each op gets one row in the array of its type: `areas`, `activityTypes`, or `projects`. `index` is the position of the op in its own array, from 0. The response has `projects` only when the request has project ops.

| Op       | `result` of an `ok` row                                                                                                                                                                                                                                                                                                    |
| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `create` | `{ "created": { id, name, color } }` for an area, or `{ "created": { id, name, pattern } }` for an activity type.                                                                                                                                                                                                          |
| `update` | `{ "updated": { id, name, color or pattern } }`. When the op forked a shared default, `updated` also has `forkedFrom`. The result does not show `order`. Read `GET /api/v1/categories` to see the new order.                                                                                                               |
| `delete` | `{ "deletedIds": [id] }`. For a shared default or your fork of it, `deletedIds` also has the other ID of the pair when a fork existed. When `reassignTo` is an ID, the result also has `movedEvents`, the number of events that moved. `movedEvents` does not count Inbox items, trackers, calendar defaults, or projects. |

For a project op:

| Op       | `result` of an `ok` row                                                                                                                                                                      |
| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `create` | `{ "created": { id, name, areaId, publishCalendarId } }`                                                                                                                                     |
| `update` | `{ "updated": { id, name, areaId, archived, publishCalendarId } }`                                                                                                                           |
| `delete` | `{ "deletedIds": [id] }`                                                                                                                                                                     |
| `split`  | `{ "split": { id, name, areaId, fromId, linkId, movedItems } }`. `id` and `name` are of the new project. `fromId` is the old project. `movedItems` is the number of Inbox blocks that moved. |
| `relink` | `{ "relinked": { id, fromId, linkId, movedItems, noop? } }`. `fromId` is the project that the Linear project fed before, or `null`.                                                          |

- An `error` row has an `error` object with `code`, `message`, and optional detail fields. Refer to [Error format](https://reassign.dev/errors.md#error-format). A `skipped` row has a `reason`.
- When one or more ops apply and the server records an undo receipt, the response has `undoToken` and `expiresAt`. Send the token to [`POST /api/v1/actions/undo`](https://reassign.dev/reference/scheduling.md#undo-a-change) before `expiresAt` (30 minutes) to reverse all the ops of the request.
- A request that applies no op has no `undoToken`. If the server cannot record the undo, the response also has no `undoToken`, and the changes stay.
- The undo of a delete puts back each link that the delete moved or cleared. The undo of a delete of a shared default fails with `stale` when a category of the same type now has its name. Refer to [Stale tokens](https://reassign.dev/reference/scheduling.md#stale-tokens).
- The undo of a project delete makes the project again with its ID, and puts back its links.
- The undo of a `split` links the list or the Linear project back to the old project, and deletes the new project. The undo of a `relink` moves the Linear project and its Inbox blocks back.
- A project undo fails with `stale` in these cases:
  - An Inbox block, an event, a calendar, or a Linear project now links to a project that the request made.
  - The undo gives a live project the name of a different live project.
  - The list or the Linear project of a `split` or a `relink` now feeds a different project, or the old project is deleted or archived.

### Op errors

| Cause                                                                                                                                                                                                                                                                                                                                                                                       | `error.code` |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ |
| No area or activity type has the `id` of an `update` or a `delete` op. The message is `Area "<id>" not found or not accessible.` (or `Activity type …`). For a project, the message is `Project "<id>" not found or not accessible.`                                                                                                                                                        | `not_found`  |
| The `areaId` of a project op is not an area that you can see: `Area not found.` The `publishCalendarId` is not a writable task list of the project that can take a new item: `Task list not found, not writable, or not a list of this project.` A `publishCalendarId` that is not a UUID gets `Task list not found or not writable.` The `linkId` of a `split` or a `relink` is not valid. | `not_found`  |
| An op that is not valid (refer to "Validation" above), a blank name, an update with no field to change, a delete with no `reassignTo` of a category that items use, or a `reassignTo` that does not exist or is the same category.                                                                                                                                                          | `validation` |
| A create or a rename to the name of a different category of the same type. The message is `An area named "<name>" already exists (id <id>). Reuse it instead of creating a duplicate, or pick a different name.` A rename says `renaming into a duplicate`. For a type, the message starts with `An activity type named`.                                                                   | `conflict`   |
| A project name that a different live project has, or a delete of a project that a task list or a Linear project feeds. Refer to [Project ops](https://reassign.dev/reference/categories.md#project-ops).                                                                                                                                                                                    | `conflict`   |
| A server fault in the op                                                                                                                                                                                                                                                                                                                                                                    | `internal`   |

### Delete a category that items use

When live items use a category, a delete with no `reassignTo`, or with `""`, fails with `validation`. The message is:

``Items use this area. Pass `reassignTo` with another area's id to move them, or `reassignTo: null` to clear their link.``

For an activity type, the message says `activity type`. The `error` of the row also has these fields:

| Field             | Value                                                                                                                                                                                                                                                                                                                                                                               |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `usage`           | `{ events, inboxItems, trackers, calendars, projects? }`: the number of items that use the category. `calendars` counts the calendars that use it as their default. `projects` is for an area only: the number of live projects under the area. Deleted events, archived trackers, and archived projects are not in the counts. For a shared default, the counts include your fork. |
| `reassignTargets` | `[{ id, name }]`: the categories of the same type that can take the links. The list never has the category that you delete, or its fork.                                                                                                                                                                                                                                            |

Do these steps:

1. Show `usage` to the user, and ask where the items go.
2. Send the delete again with `reassignTo`. Use an `id` from `reassignTargets` to move the links, or `null` to clear them.

A category that no live item uses deletes with no `reassignTo`. A live project under an area counts as a use. An area delete with `reassignTo` moves each project of the area to that area. With `reassignTo: null`, the projects have no area. The move does not rename a project.

### Delete a shared default

- The delete hides the shared default, and your fork of it, for your account only. Other accounts still see the default.
- After the delete, `GET /api/v1/categories` does not show either ID.
- `deletedIds` has the ID that you sent. When you had a fork, it also has the other ID of the pair.
- The undo shows the default again and puts back each link.

### Status of a failed request

- In an atomic request with a failed op, only the failed op is an `error` row. The other ops are `skipped` rows, with the reason `Not applied — another op in this batch failed; fix the errors and resend.`
- An atomic request with one or more ops that are not valid runs no op. Each bad op is an `error` row, and the valid ops are `skipped`.
- For a server fault outside an op in an atomic request, all ops get the same message and the code of the error, usually `internal`.
- When no op applies, Reassign rejects the request. The body is `{ "error": { code, message }, areas, activityTypes, projects? }`. `projects` is there only when the request sent project ops. The status follows `error.code`, as the Responses table shows.
- A request where one or more ops apply returns `200`.

Refer to [Errors](https://reassign.dev/errors.md) for the error format.

### Request body

- `areas` (_object\[]_)
  - One form: `op: "create"`
    - `id` (_string_)
    - `name` (_string, required_)
    - `color` (_string_): Lowercase "#rrggbb"; omit to auto-pick.
  - One form: `op: "update"`
    - `id` (_string, required_)
    - `name` (_string_)
    - `color` (_string_): Lowercase "#rrggbb"; omit to auto-pick.
    - `order` (_integer_)
  - One form: `op: "delete"`
    - `id` (_string, required_)
    - `reassignTo` (_string | null_): Another area's id to move the linked items onto, or null to clear their link. Required when items use it.
- `activityTypes` (_object\[]_)
  - One form: `op: "create"`
    - `id` (_string_)
    - `name` (_string, required_)
    - `pattern` (_"solid" | "hatch\_r" | "hatch\_l" | "cross" | "horizontal" | "vertical" | "grid" | "dots" | "waves" | "chevron"_): A fill pattern; omit to auto-pick.
  - One form: `op: "update"`
    - `id` (_string, required_)
    - `name` (_string_)
    - `pattern` (_"solid" | "hatch\_r" | "hatch\_l" | "cross" | "horizontal" | "vertical" | "grid" | "dots" | "waves" | "chevron"_): A fill pattern; omit to auto-pick.
    - `order` (_integer_)
  - One form: `op: "delete"`
    - `id` (_string, required_)
    - `reassignTo` (_string | null_): Another activity type's id to move the linked items onto, or null to clear their link. Required when items use it.
- `projects` (_object\[]_)
  - One form: `op: "create"`
    - `id` (_string_)
    - `name` (_string, required_): The project name; unique among all live projects of the user, in any area.
    - `areaId` (_string | null_): The parent area id, or null for no area. When a write moves an Inbox item to the project and sets no area, the item takes this area.
  - One form: `op: "update"`
    - `id` (_string, required_)
    - `name` (_string_): The project name; unique among all live projects of the user, in any area.
    - `areaId` (_string | null_): The parent area id, or null for no area. When a write moves an Inbox item to the project and sets no area, the item takes this area.
    - `publishCalendarId` (_string | null_): The task list that a new Inbox item of this project is created in, unless the item names its own list. null turns it off.
    - `order` (_number_): The sort position.
    - `archived` (_boolean_): true archives the project: it leaves the lookups and takes no new links, but its links stay. false restores it.
  - One form: `op: "delete"`
    - `id` (_string, required_): The project to delete; its items and events keep their area and lose the project link.
  - One form: `op: "split"`
    - `id` (_string, required_): The shared project to split a link from.
    - `linkId` (_string, required_): The task list (`source_calendars` id) or Linear project (`source_projects` id) that feeds `id` and moves to a new project of its own, "{name} ({Provider})".
  - One form: `op: "relink"`
    - `id` (_string, required_): The live project that the Linear project feeds next.
    - `linkId` (_string, required_): The Linear project (`source_projects` id) to point at `id`. Its Inbox items move along. A task list changes its project in its list settings.
- `partial` (_boolean_): Best-effort mode instead of the default all-or-nothing.

### Response fields

- `undoToken` (_string_)
- `expiresAt` (_string_)
- `areas` (_object\[], required_)
  - One form: `status: "ok"`
    - `index` (_integer, required_)
    - `result` (_object, required_)
      - One form: With `created`
        - `created` (_object, required_)
          - `id` (_string, required_)
          - `name` (_string, required_)
          - `color` (_string, required_)
      - One form: With `updated`
        - `updated` (_object, required_)
          - `id` (_string, required_)
          - `forkedFrom` (_string_)
          - `name` (_string, required_)
          - `color` (_string, required_)
      - One form: With `deletedIds`
        - `deletedIds` (_string\[], required_)
        - `movedEvents` (_integer_)
    - `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_)
- `activityTypes` (_object\[], required_)
  - One form: `status: "ok"`
    - `index` (_integer, required_)
    - `result` (_object, required_)
      - One form: With `created`
        - `created` (_object, required_)
          - `id` (_string, required_)
          - `name` (_string, required_)
          - `pattern` (_"solid" | "hatch\_r" | "hatch\_l" | "cross" | "horizontal" | "vertical" | "grid" | "dots" | "waves" | "chevron", required_)
      - One form: With `updated`
        - `updated` (_object, required_)
          - `id` (_string, required_)
          - `forkedFrom` (_string_)
          - `name` (_string, required_)
          - `pattern` (_"solid" | "hatch\_r" | "hatch\_l" | "cross" | "horizontal" | "vertical" | "grid" | "dots" | "waves" | "chevron", required_)
      - One form: With `deletedIds`
        - `deletedIds` (_string\[], required_)
        - `movedEvents` (_integer_)
    - `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_)
- `projects` (_object\[]_)
  - One form: `status: "ok"`
    - `index` (_integer, required_)
    - `result` (_object, required_)
      - One form: With `created`
        - `created` (_object, required_)
          - `id` (_string, required_)
          - `name` (_string, required_)
          - `areaId` (_string | null, required_)
          - `publishCalendarId` (_string | null, required_)
      - One form: With `updated`
        - `updated` (_object, required_)
          - `id` (_string, required_)
          - `name` (_string, required_)
          - `areaId` (_string | null, required_)
          - `archived` (_boolean, required_)
          - `publishCalendarId` (_string | null, required_)
      - One form: With `deletedIds`
        - `deletedIds` (_string\[], required_)
      - One form: With `split`
        - `split` (_object, required_): A split: the link feeds a new project "{name} ({Provider})" now.
          - `id` (_string, required_): The new project of the link.
          - `name` (_string, required_)
          - `areaId` (_string | null, required_)
          - `fromId` (_string, required_): The shared project that the link left.
          - `linkId` (_string, required_): The task list or Linear project that moved.
          - `movedItems` (_integer, required_): The Inbox items that moved with the link.
      - One form: With `relinked`
        - `relinked` (_object, required_): A relink: the Linear project feeds `id` now.
          - `id` (_string, required_): The project that the Linear project feeds now.
          - `fromId` (_string | null, required_): The project that it fed before; null for none.
          - `linkId` (_string, required_): The Linear project that moved.
          - `movedItems` (_integer, required_): The Inbox items that moved with the link.
          - `noop` (_true_): The Linear project feeds `id` already: nothing changed and no undo receipt.
    - `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` | 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."                                                                                                                                   |
| `404`  | `not_found`           | An op names an item that does not exist.                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `409`  | `conflict`            | The request conflicts with the current state: `conflict` "A project named Launch exists in Work." or `conflict` "Link the task list to a different project first."                                                                                                                                                                                                                                                                                                                     |
| `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 `areas`, `activityTypes` and `projects` rows, so the caller sees which op failed.

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

**cURL**

```bash
curl -X POST "https://reassign.app/api/v1/categories" \
  -H "Authorization: Bearer rsk_live_YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "areas": [
    {
      "op": "create",
      "name": "Health",
      "color": "#22c55e"
    }
  ],
  "activityTypes": [
    {
      "op": "update",
      "id": "5e6f7081-92a3-4b4c-8d5e-6f708192a3b4",
      "name": "Deep focus"
    }
  ],
  "projects": [
    {
      "op": "split",
      "id": "f3a4b5c6-d7e8-4f90-a1b2-c3d4e5f60718",
      "linkId": "a4b5c6d7-e8f9-4a01-b2c3-d4e5f6071829"
    }
  ]
}'
```

**JavaScript**

```js
const res = await fetch("https://reassign.app/api/v1/categories", {
  method: "POST",
  headers: {
    Authorization: "Bearer rsk_live_YOUR_TOKEN",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "areas": [
      {
        "op": "create",
        "name": "Health",
        "color": "#22c55e"
      }
    ],
    "activityTypes": [
      {
        "op": "update",
        "id": "5e6f7081-92a3-4b4c-8d5e-6f708192a3b4",
        "name": "Deep focus"
      }
    ],
    "projects": [
      {
        "op": "split",
        "id": "f3a4b5c6-d7e8-4f90-a1b2-c3d4e5f60718",
        "linkId": "a4b5c6d7-e8f9-4a01-b2c3-d4e5f6071829"
      }
    ]
  }),
});
const data = await res.json();
```

**Response**

```json
{
  "undoToken": "00000000-0000-4000-8000-000000000001",
  "expiresAt": "2026-08-13T09:12:00.000Z",
  "areas": [
    {
      "index": 0,
      "status": "ok",
      "result": {
        "created": {
          "id": "6f7a8b9c-0d1e-4f2a-8b3c-4d5e6f7a8b9c",
          "name": "Health",
          "color": "#22c55e"
        }
      }
    }
  ],
  "activityTypes": [
    {
      "index": 0,
      "status": "ok",
      "result": {
        "updated": {
          "id": "5e6f7081-92a3-4b4c-8d5e-6f708192a3b4",
          "name": "Deep focus",
          "pattern": "solid"
        }
      }
    }
  ],
  "projects": [
    {
      "index": 0,
      "status": "ok",
      "result": {
        "split": {
          "id": "b5c6d7e8-f9a0-4b12-83d4-e5f60718293a",
          "name": "Launch (Todoist)",
          "areaId": "3c9d2a41-6b7e-4f80-9a1b-2c3d4e5f6071",
          "fromId": "f3a4b5c6-d7e8-4f90-a1b2-c3d4e5f60718",
          "linkId": "a4b5c6d7-e8f9-4a01-b2c3-d4e5f6071829",
          "movedItems": 3
        }
      }
    }
  ]
}
```
