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.
- Name
error.code- Type
- string
- Description
A stable code for the type of error. Refer to Error codes.
- Name
error.message- Type
- string
- Description
A text for humans that tells what went wrong.
- Name
error.issues- Type
- array
- Description
Optional, on a
validationerror from a schema check. Up to 20 items{ path, message, expected? }. Refer to "Validation errors (422)" below.
- Name
error.feature- Type
- string
- Description
Optional, on a
permissionerror from the plan. The value isapi: the account has no trial or Pro plan. The schema also allowscalendar_sync, but every public route refuses an account with no plan first.
- Name
error.conflictingRows- Type
- array
- Description
Optional, on a
staleundo row. Up to 50 rows that block the undo. Refer to Stale tokens.
- Name
error.conflictingRowsTruncated- Type
- boolean
- Description
Optional.
truewhen more than 50 rows block the undo.
- Name
error.usage- Type
- object
- Description
Optional, when a category delete needs a
reassignTochoice. The counts{ events, inboxItems, trackers, calendars }. For an area, it also hasprojects: the count of its projects that are not archived. Refer to Delete a category that items use.
- Name
error.reassignTargets- Type
- array
- Description
Optional, when a category delete needs a
reassignTochoice. 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
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
422 Unprocessable Content
{
"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. |
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 /eventsorPOST /backlogcall that changed data never gets HTTP status 503. Refer to Batch requests. unauthorizedandbatch_rejectedoccur only in the REST API. An op row inresultsnever has these codes.- The schema also lists the code
checkout_unlinkedand the fieldunlinkedCheckout. 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 aWWW-Authenticateheader. Refer to Authentication.permission: If the account does not have a trial or Pro plan, the message is "Reassign needs an active subscription." The error also hasfeature: "api".scope: A read needsevents:readorevents:write. A change needsevents:write. Refer to Scopes.conflict: An undo can meet a calendar sync that sends the same change now. The undo row then fails withconflictand 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.conflictonPOST /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.conflictonPOST /categoriesfor 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 withconflict, for example "Link the task list to a different project first." Link the list to a different project, or archive the project.stale: OnlyPOST /actions/undosends 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 haveconflictingRows. Refer to 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.validation: The error can haveissues. Refer to "Validation errors (422)" below. A category delete refused because it needs areassignTochoice also hasusageandreassignTargets.
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.
- A token with only
events:readgets 403scopeon a write route, also when the body is not correct. - On a
GETroute, a query that is not correct gets 422 before the scope check. GET /meneeds 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
bodymeans the full body. The pathquerymeans the full query. - The reason text comes from the schema library. It can change. Use
error.code, not the text. error.issueslists up to 20 bad fields.- Each issue has
path,message, and an optionalexpected.pathis a list of keys and indexes. It starts at the root of the body or the query, with nobodyorquerypart. expectednames the expected type or format, for examplenumber,uuid,YYYY-MM-DD, orYYYY-MM-DDTHH:MM. An issue about a relation between fields has noexpected.- The message and
issuesof 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. |
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. |
- A conflict item is a span with
name, andidfor a stored event orbatchIndexfor an op of the same batch. nearestSlotsgives free spans near the requested time: up to three for event and backlog writes, and up to five forPOST /schedule/plan. For their start times, refer to Two forms of a request.
Atomic and partial batches
- A batch of
POST /events,POST /backlog,POST /categories, orPOST /schedule/planis atomic by default. If one op fails, the server applies none of them. The failed ops havestatus: "error". The valid ops havestatus: "skipped". - With
"partial": true, the server applies the ops that succeed. The failed ops havestatus: "error". - In
POST /events,POST /backlog, andPOST /categories, a bad op does not fail the full request. An unknownop, an unknown key, or a bad field becomes an error row with the codevalidation. The row message is one of these:Invalid "<op>" op. `<path>`: <reason>, for exampleInvalid "create" op. `name`: Too small: …Unknown op "<op>". Use one of: …for an unknownopvalue, orUnknown op undefined. Use one of: …for an object with noop- "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 /categoriesrequest with a bad op writes nothing. It returns HTTP status 422 with avalidationrow for the bad op andskippedrows for the valid ops. - The request fails with HTTP status 422 and no rows only when the body itself is not correct. For example,
opsis missing,opshas more than 50 items,areasis not an array, or the body has an unknown top-level key. - In
POST /backlog, ascheduleop obeys the rules of an event write. The span is 5 minutes to 168 hours. A span that overlaps a blocking event fails withconflict,conflicts, andnearestSlots. A non-blocking block can overlap. Aparkop earlier in the same batch makes its time free for a laterscheduleop. - In
POST /schedule/plan, each request row is also checked alone. A bad row becomes an error row with the codevalidation. A taken exactstartgives up to 5nearestSlots. POST /actions/undois 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 }.errorhas only the normalcodeandmessage.resultsshows the row of each op, with its detail fields. - For
POST /categories, the body is{ error, areas, activityTypes }, andprojectswhen the request sent project ops. - When one op failed,
error.messageis the message of that op. Otherwise, it is "No op was applied; each failed op is inresults." - For
POST /categories, that message names each list that has a failed op:areas,activityTypes, orprojects. - 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)
{
"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
undoTokenwhen 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 codeinternal. 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 theundoTokentoPOST /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.", andRetry-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/v1route 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 refusal is HTTP status 429 with the code
- 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_feedbackalso has one. Refer to Rate limits. - A limit of the firewall returns
429. Its body can differ from the normal error format. It can have noRetry-Afterheader. Then, wait 60 seconds and retry. - A limit of the app returns
429with the normal error format and the coderate_limited. It always has aRetry-Afterheader. Retry-Aftergives the window of the limit in seconds:60for a limit for each minute,120for a lock of the app, and3600for the feedback limits. When the limit gives no window, the value is3600.- After a
429, wait and retry. UseRetry-Afterwhen 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
{
"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.
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
codeand the errormessage
Never send your token or your client secret.