# Errors

> The error format of the Reassign REST API, the error codes, rejected batches, the rate limits, and how to handle errors.

Source: https://reassign.dev/errors/

When a request fails, the Reassign REST API returns an HTTP status code and a JSON error body. Use the error `code` to find the cause.

## Error format

All REST API errors have an `error` object. A rejected batch also has the result of each op. Refer to [Batch requests](https://reassign.dev/errors.md#batch-requests).

- `error.code` (_string_): A stable code for the type of error. Refer to [Error codes](https://reassign.dev/errors.md#error-codes).
- `error.message` (_string_): A text for humans that tells what went wrong.
- `error.issues` (_array_): Optional, on a `validation` error from a schema check. Up to 20 items `{ path, message, expected? }`. Refer to "Validation errors (422)" below.
- `error.feature` (_string_): Optional, on a `permission` error from the plan. The value is `api`: the account has no trial or Pro plan. The schema also allows `calendar_sync`, but every public route refuses an account with no plan first.
- `error.conflictingRows` (_array_): Optional, on a `stale` undo row. Up to 50 rows that block the undo. Refer to [Stale tokens](https://reassign.dev/reference/scheduling.md#stale-tokens).
- `error.conflictingRowsTruncated` (_boolean_): Optional. `true` when more than 50 rows block the undo.
- `error.usage` (_object_): Optional, when a category delete needs a `reassignTo` choice. The counts `{ events, inboxItems, trackers, calendars }`. For an area, it also has `projects`: the count of its projects that are not archived. Refer to [Delete a category that items use](https://reassign.dev/reference/categories.md#delete-a-category-that-items-use).
- `error.reassignTargets` (_array_): Optional, when a category delete needs a `reassignTo` choice. The categories `{ id, name }` that can take the links.

Use `error.code` in your code. Do not use the text of `error.message` to make decisions.

The API sends a detail field only when it has a value. In a rejected batch, the detail fields are only in the `error` of each row. The top-level `error` has only `code` and `message`.

**401 Unauthorized**

```json
{
  "error": {
    "code": "unauthorized",
    "message": "Authentication required."
  }
}
```

**422 Unprocessable Content**

```json
{
  "error": {
    "code": "validation",
    "message": "`from`: date must be ISO \"YYYY-MM-DD\"",
    "issues": [
      {
        "path": ["from"],
        "message": "date must be ISO \"YYYY-MM-DD\"",
        "expected": "YYYY-MM-DD"
      }
    ]
  }
}
```

## Error codes

| Code             | HTTP status | Meaning                                                                                                                                                                                                                                         |
| ---------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `batch_rejected` | 400         | The server applied no op of a batch, and the failed ops have different codes. Refer to [Batch requests](https://reassign.dev/errors.md#batch-requests).                                                                                         |
| `unauthorized`   | 401         | The token is missing, not valid, expired, or revoked.                                                                                                                                                                                           |
| `permission`     | 403         | The account does not have a trial or Pro plan. Or the target item does not allow the action, for example a `park` of a recurring event.                                                                                                         |
| `scope`          | 403         | The token does not have the necessary scope.                                                                                                                                                                                                    |
| `read_only`      | 403         | You cannot change this event or this field from Reassign. For example, the event is on a calendar that the user does not own. Or a task app owns the planned day of an Inbox block. Or a project change cannot move the linked task in its app. |
| `not_found`      | 404         | The event, area, activity type, project, or day does not exist. Also a path or a method that no route serves.                                                                                                                                   |
| `conflict`       | 409         | The time, the name, or the id is already taken, or a short busy state blocks the write. A retry can work only for the busy state.                                                                                                               |
| `stale`          | 409         | An undo cannot apply, because a row that the write changed has a newer change. Do not retry.                                                                                                                                                    |
| `validation`     | 422         | An argument is not correct: out of range, contradictory, or not complete.                                                                                                                                                                       |
| `rate_limited`   | 429         | You sent too many requests.                                                                                                                                                                                                                     |
| `internal`       | 503         | A fault occurred on the server. A retry can work.                                                                                                                                                                                               |

- The API returns HTTP status 400 only with the code `batch_rejected`.
- A `POST /events` or `POST /backlog` call that changed data never gets HTTP status 503. Refer to [Batch requests](https://reassign.dev/errors.md#batch-requests).
- `unauthorized` and `batch_rejected` occur only in the REST API. An op row in `results` never has these codes.
- The schema also lists the code `checkout_unlinked` and the field `unlinkedCheckout`. Only the billing routes of the Reassign web app send them. A token or OAuth caller never gets them.

### Notes on some codes

- `unauthorized`: The response also has a `WWW-Authenticate` header. Refer to [Authentication](https://reassign.dev/authentication.md#authentication-errors).
- `permission`: If the account does not have a trial or Pro plan, the message is "Reassign needs an active subscription." The error also has `feature: "api"`.
- `scope`: A read needs `events:read` or `events:write`. A change needs `events:write`. Refer to [Scopes](https://reassign.dev/authentication.md#scopes).
- `conflict`: An undo can meet a calendar sync that sends the same change now. The undo row then fails with `conflict` and the message "A calendar sync is sending this change now. Try the undo again in a moment." The undo changes nothing. Send it again after a short time.
- `conflict` on `POST /categories`: A create or a rename uses the name of a different category of the same type. The message gives the name and the id of that category. Use that id, or select a different name. Do not retry.
- `conflict` on `POST /categories` for a project: Two live projects cannot have the same name, also in different areas. The message is, for example, "A project named Launch exists in Work." A delete of a project that a task list or a Linear project feeds also fails with `conflict`, for example "Link the task list to a different project first." Link the list to a different project, or archive the project.
- `stale`: Only `POST /actions/undo` sends this code. A row that the write changed has a newer change, or the token is from before the change stamps. The undo changes nothing. The row can have `conflictingRows`. Refer to [Stale tokens](https://reassign.dev/reference/scheduling.md#stale-tokens).
- `internal`: The API also uses this code for an unexpected server error. The message is then "Something went wrong on our side. Please try again."
- `rate_limited`: Refer to [Rate limits](https://reassign.dev/errors.md#rate-limits).
- `validation`: The error can have `issues`. Refer to "Validation errors (422)" below. A category delete refused because it needs a `reassignTo` choice also has `usage` and `reassignTargets`.

### Order of the checks

The API checks a request in this order. The first check that fails gives the error.

| Order | Check                                                   | Error              |
| ----- | ------------------------------------------------------- | ------------------ |
| 1     | The signature and the audience of an OAuth access token | 401 `unauthorized` |
| 2     | The limit for each OAuth grant or personal access token | 429 `rate_limited` |
| 3     | The token                                               | 401 `unauthorized` |
| 4     | The plan                                                | 403 `permission`   |
| 5     | The `events:write` scope, on a route that is not `GET`  | 403 `scope`        |
| 6     | The query and the body                                  | 422 `validation`   |
| 7     | The `events:read` scope, on a `GET` route               | 403 `scope`        |

- The API counts a personal access token before it looks up the token. Thus, a personal access token that is not valid can get 429, not 401. An OAuth access token that is not valid gets 401 before the count. Refer to [Rate limits](https://reassign.dev/errors.md#rate-limits).
- A token with only `events:read` gets 403 `scope` on a write route, also when the body is not correct.
- On a `GET` route, a query that is not correct gets 422 before the scope check.
- `GET /me` needs no plan and no scope.

### Removed routes

These routes do not exist now: `POST /events/batch`, `PATCH /events/{id}`, `DELETE /events/{id}`, and `GET /reviews/{date}`. The API answers them as a path that no route serves:

| Request                                                                                | Answer                                                                       |
| -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| More than 600 requests in one minute for the same OAuth grant or personal access token | 429 `rate_limited`                                                           |
| No token, or a token that is not valid                                                 | 401 `unauthorized`                                                           |
| A valid token, but no trial or Pro plan                                                | 403 `permission`                                                             |
| A valid token                                                                          | 404 `not_found`, with the message "No API route has this path."              |
| A request from the Reassign Chrome extension 1.1.x                                     | 422 `validation`, with the message "Update the Reassign extension to 1.2.0." |

Use `POST /events` for all event writes. `GET /schedule` gives the review of a confirmed day.

### Validation errors (422)

Each route checks its query and its body against a schema. The API refuses a value in a different format. It does not change the value for you.

- The message names the first bad field by its path. The format is `` `<path>`: <reason> ``, for example `` `ops`: Too small: expected array to have >=1 items ``.
- The path `body` means the full body. The path `query` means the full query.
- The reason text comes from the schema library. It can change. Use `error.code`, not the text.
- `error.issues` lists up to 20 bad fields.
- Each issue has `path`, `message`, and an optional `expected`. `path` is a list of keys and indexes. It starts at the root of the body or the query, with no `body` or `query` part.
- `expected` names the expected type or format, for example `number`, `uuid`, `YYYY-MM-DD`, or `YYYY-MM-DDTHH:MM`. An issue about a relation between fields has no `expected`.
- The message and `issues` of a schema check do not repeat the values that you sent.
- These 422 errors have no `issues`: a repeated query parameter, a query parameter on a route that takes none, and a body that is not valid JSON.
- A query value that is not a real date gets one of these messages:
  - `` `from`: date must be ISO "YYYY-MM-DD" ``
  - `` `from`: date is not a real calendar date (e.g. "2026-13-45" and "2027-02-29" are invalid) ``

The API refuses keys that it does not know:

| Case                                                                        | Message                                |
| --------------------------------------------------------------------------- | -------------------------------------- |
| An unknown key at the top level of the body                                 | ``Unknown body key `<key>`.``          |
| An unknown key in a nested object                                           | `` `<path>`: Unknown key `<key>`. ``   |
| An unknown query parameter, or a query parameter on a route that takes none | ``Unknown query parameter `<name>`.``  |
| The same query parameter two times                                          | ``Repeated query parameter `<name>`.`` |

An unknown key is the first error that the message shows, also when the request has other errors.

### AI model errors (503)

`POST /command` sends the input to an AI model. When the model fails, the API returns HTTP status 503 with the code `internal`.

| Message                                            | Cause                                          |
| -------------------------------------------------- | ---------------------------------------------- |
| "The AI model did not respond. Try again."         | The model call failed.                         |
| "The AI model reply could not be read. Try again." | The reply of the model is not a readable plan. |

These 503 responses have no `Retry-After` header.

### Request bodies

- Send request bodies as JSON, with `Content-Type: application/json`.
- A body that is not valid JSON gets HTTP status 422 with the message "The request body is not valid JSON."
- An empty body reads as an empty object (`{}`).

## Batch requests

A batch route takes a list of ops and returns one row for each op in `results`. The batch routes are `POST /events`, `POST /schedule/plan`, `POST /actions/undo`, and `POST /backlog`. `POST /categories` has the row lists `areas` and `activityTypes`, and `projects` when the request sent project ops. Each list has its own `index` values.

### Result rows

| Field      | Meaning                                                                                                                                                                                                                                                       |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `index`    | The position of the op in the request, from 0                                                                                                                                                                                                                 |
| `status`   | `"ok"`, `"error"`, or `"skipped"`                                                                                                                                                                                                                             |
| `result`   | On `ok` only: the result of the op                                                                                                                                                                                                                            |
| `error`    | On `error` only: `{ code, message }`, and for a conflict also `conflicts` and `nearestSlots`. It can also have the optional detail fields. Refer to [Error format](https://reassign.dev/errors.md#error-format).                                              |
| `reason`   | On `skipped` only: why the server did not apply the op                                                                                                                                                                                                        |
| `warnings` | Optional on `ok` only. An event write can return `["classification_pending"]` when queued AI classification can still fill eligible fields. A `create` keeps its `kind`. The write succeeded. Refer to [Rows](https://reassign.dev/reference/events.md#rows). |

- A conflict item is a span with `name`, and `id` for a stored event or `batchIndex` for an op of the same batch.
- `nearestSlots` gives free spans near the requested time: up to three for event and backlog writes, and up to five for `POST /schedule/plan`. For their start times, refer to [Two forms of a request](https://reassign.dev/reference/scheduling.md#two-forms-of-a-request).

### Atomic and partial batches

- A batch of `POST /events`, `POST /backlog`, `POST /categories`, or `POST /schedule/plan` is atomic by default. If one op fails, the server applies none of them. The failed ops have `status: "error"`. The valid ops have `status: "skipped"`.
- With `"partial": true`, the server applies the ops that succeed. The failed ops have `status: "error"`.
- In `POST /events`, `POST /backlog`, and `POST /categories`, a bad op does not fail the full request. An unknown `op`, an unknown key, or a bad field becomes an error row with the code `validation`. The row message is one of these:
  - ``Invalid "<op>" op. `<path>`: <reason>``, for example ``Invalid "create" op. `name`: Too small: …``
  - `Unknown op "<op>". Use one of: …` for an unknown `op` value, or `Unknown op undefined. Use one of: …` for an object with no `op`
  - "Each op must be an object." for an item that is not an object, for example `null`, a string, or an array
- Each of these rows also has `error.issues`. The paths start at the op.
- Thus, the response always has the rows. But in an atomic batch, a bad op still rejects the batch. Refer to "Rejected batch" below.
- For example, an atomic `POST /categories` request with a bad op writes nothing. It returns HTTP status 422 with a `validation` row for the bad op and `skipped` rows for the valid ops.
- The request fails with HTTP status 422 and no rows only when the body itself is not correct. For example, `ops` is missing, `ops` has more than 50 items, `areas` is not an array, or the body has an unknown top-level key.
- In `POST /backlog`, a `schedule` op obeys the rules of an event write. The span is 5 minutes to 168 hours. A span that overlaps a blocking event fails with `conflict`, `conflicts`, and `nearestSlots`. A non-blocking block can overlap. A `park` op earlier in the same batch makes its time free for a later `schedule` op.
- In `POST /schedule/plan`, each request row is also checked alone. A bad row becomes an error row with the code `validation`. A taken exact `start` gives up to 5 `nearestSlots`.
- `POST /actions/undo` is not atomic. Each token succeeds or fails alone.

### Rejected batch

The server rejects a batch when one or more ops have `status: "error"` and no op has `status: "ok"`. The server then applied nothing.

| Failed ops                          | HTTP status                                                                     | `error.code`     |
| ----------------------------------- | ------------------------------------------------------------------------------- | ---------------- |
| All failed ops have the same code   | The status of that code, for example 409 for `conflict` or 422 for `validation` | That code        |
| The failed ops have different codes | 400                                                                             | `batch_rejected` |

- The body is `{ error, timezone?, results }`. `error` has only the normal `code` and `message`. `results` shows the row of each op, with its detail fields.
- For `POST /categories`, the body is `{ error, areas, activityTypes }`, and `projects` when the request sent project ops.
- When one op failed, `error.message` is the message of that op. Otherwise, it is "No op was applied; each failed op is in `results`."
- For `POST /categories`, that message names each list that has a failed op: `areas`, `activityTypes`, or `projects`.
- A rejected batch has no `undoToken`.
- If one or more ops succeed, the response has HTTP status 200. Examine the row of each op.

**400 Bad Request (rejected batch)**

```json
{
  "error": {
    "code": "batch_rejected",
    "message": "No op was applied; each failed op is in `results`."
  },
  "timezone": "Europe/Ljubljana",
  "results": [
    {
      "index": 0,
      "status": "error",
      "error": {
        "code": "conflict",
        "message": "The time overlaps an existing event.",
        "conflicts": [
          {
            "id": "b7f1c2a0-1d2e-4f3a-9b8c-5d6e7f801234",
            "name": "Team sync",
            "start": "2026-09-24T09:00",
            "end": "2026-09-24T10:30"
          }
        ],
        "nearestSlots": [
          { "start": "2026-09-24T11:00", "end": "2026-09-24T12:00" }
        ]
      }
    },
    {
      "index": 1,
      "status": "error",
      "error": {
        "code": "not_found",
        "message": "Event \"00000000-0000-4000-8000-00000000dead\" not found."
      }
    },
    {
      "index": 2,
      "status": "skipped",
      "reason": "Not applied — another op in this batch failed; fix the errors and resend."
    }
  ]
}
```

### A write that changed data

The server never rejects a batch after its writes changed data. This is true for `POST /events` and `POST /backlog`.

- The response has HTTP status 200. It has the `undoToken` when the server recorded the undo.
- After the write, the server reads each changed item again for its row. If this read fails, the row has `status: "error"` and the code `internal`. For an unexpected fault, the message is "The change was saved, but the read-back failed. Read it again."
- The server reads the rows after the full batch commits. Thus a later op in the batch can change the row of an earlier op.
- Do not send the write again. Read the data again, for example with `GET /schedule`. To reverse the write, send the `undoToken` to `POST /actions/undo`.

## Rate limits

The Vercel Firewall limits the requests from each IP address.

| Path                            | Limit for each IP address   |
| ------------------------------- | --------------------------- |
| `/api/v1` (all REST API routes) | 2000 requests per minute    |
| `/api/oauth/token`              | 600 requests per minute     |
| `/api/oauth/register`           | 200 requests per 10 minutes |

- The app also limits each OAuth grant and each personal access token to 600 requests per minute. An OAuth grant is one app for one user. All the access tokens of the grant share one count.
  - The refusal is HTTP status 429 with the code `rate_limited`, the message "Too many requests.", and `Retry-After: 60`.
  - The API counts a personal access token before it looks up the token. Thus, a personal access token that is not valid can also get 429.
  - The limit applies to every `/api/v1` route that needs a token, also to a path that no route serves.
  - The limit does not apply to the web session of the Reassign app. The MCP server has no limit for each token.
  - Each server instance counts alone. Thus, the limit is a soft limit. The real limit across all instances can be higher.
- The endpoints in this reference have no limit for each user. Some routes of the Reassign app have a limit for each user. The MCP tool `send_feedback` also has one. Refer to [Rate limits](https://reassign.dev/mcp.md#rate-limits).
- A limit of the firewall returns `429`. Its body can differ from the normal error format. It can have no `Retry-After` header. Then, wait 60 seconds and retry.
- A limit of the app returns `429` with the normal error format and the code `rate_limited`. It always has a `Retry-After` header.
- `Retry-After` gives the window of the limit in seconds: `60` for a limit for each minute, `120` for a lock of the app, and `3600` for the feedback limits. When the limit gives no window, the value is `3600`.
- After a `429`, wait and retry. Use `Retry-After` when it is present.

## OAuth errors

The OAuth endpoints (`/api/oauth/*`) use the OAuth 2.0 error format (RFC 6749), not the REST API format. The default HTTP status is 400.

**OAuth error**

```json
{
  "error": "invalid_grant",
  "error_description": "PKCE verification failed"
}
```

| Error                       | Cause                                                                                                                                                     |
| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `invalid_request`           | A parameter is missing or not correct. For example, `code_challenge` is missing.                                                                          |
| `invalid_client`            | The `client_id` or the `client_secret` is not correct.                                                                                                    |
| `invalid_grant`             | The code or the refresh token is not valid, is used, or is revoked. Or the `redirect_uri` or the PKCE check does not match.                               |
| `invalid_scope`             | The refresh request asks only for scopes that the grant does not have. The server removes extra scopes from a request that also asks for a granted scope. |
| `invalid_target`            | The `resource` value is not known.                                                                                                                        |
| `unsupported_response_type` | The `response_type` is not `code`.                                                                                                                        |
| `unsupported_grant_type`    | The `grant_type` is not `authorization_code` or `refresh_token`.                                                                                          |
| `invalid_client_metadata`   | The registration body is not correct.                                                                                                                     |
| `invalid_redirect_uri`      | A redirect URI in the registration body is not allowed.                                                                                                   |
| `access_denied`             | The user denied access, or the account does not have Reassign Pro or the 7-day free trial.                                                                |
| `temporarily_unavailable`   | The server cannot do the request now. Try again after the `Retry-After` time.                                                                             |
| `server_error`              | A fault occurred on the server.                                                                                                                           |

For the OAuth flow, refer to [Authentication](https://reassign.dev/authentication.md#o-auth-2-0).

## Handle errors

| Status | What to do                                                                                                                                                                                                                                                                                                           |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400    | The server applied no op of a batch (`batch_rejected`). Examine the row of each op in `results`. Then correct the request.                                                                                                                                                                                           |
| 401    | Get a new token. For OAuth 2.0, refresh the access token. For a personal access token, make sure that you did not revoke it.                                                                                                                                                                                         |
| 403    | For `scope`, get a token with the necessary scope. For `permission`, the account needs a trial or Pro plan (`error.feature` names the feature), or the action is not possible on that item. For `read_only`, change the item in its calendar or task app.                                                            |
| 404    | Make sure that the ID, the date, or the path is correct.                                                                                                                                                                                                                                                             |
| 409    | For `conflict`, select a different time, for example one of the `nearestSlots`. For a category name, use the category that has the name, or select a different name. For a project delete, link the task list or the Linear project to a different project first, or archive the project. For `stale`, do not retry. |
| 422    | Correct the request. Do not send the same request again.                                                                                                                                                                                                                                                             |
| 429    | Wait and retry. Use `Retry-After` when it is present.                                                                                                                                                                                                                                                                |
| 503    | Wait, and then send the request again.                                                                                                                                                                                                                                                                               |

- Send a request again only after a 429 or a 503.
- Increase the wait time after each retry that fails. Stop after a small number of retries.
- Do not send a request again after a different 4xx status. The same request gets the same error.

## Get help

If you cannot correct an error, send an email to <developers@reassign.dev>. Include these details:

- The HTTP method and the path
- The time of the request, with the time zone
- The error `code` and the error `message`

> Never send your token or your client secret.
