# Errors

Every v3 error is an RFC 7807 problem-details body. At
`https://openapi.niftypm.com/api/v3` every one of them is served as
`application/json`, the `429` a rate limiter raises included — see
[Rate limits](/rate-limits):

```json
{ "type": "resource_not_found", "title": "Resource not found", "status": 404,
  "detail": "task 'KfS0ha8P' not found or inaccessible", "instance": "/tasks/KfS0ha8P",
  "code": "resource_not_found", "requestId": "1f5622fd-2634-4f55-825d-f9199ce2c940",
  "docsUrl": "https://developers.niftypm.com/docs/errors#resource_not_found" }
```

Branch on `code`, never on `detail` (wording can change). Quote `requestId` when you contact support. Validation errors add `errors: [{ field, code, message }]`; `expand_unknown_relation` adds `available: [...]`. Extra fields like these sit at the **top level** of the body, not nested under a `meta` key. The legacy v1 API uses a different shape, `{ "message": "...", "code": 401 }`, and OAuth endpoints use RFC 6749 `{ "error", "error_description" }`.

## validation_failed

**HTTP 400.** The request body failed validation — a missing required field, wrong type, or out-of-range value.

**Fix:** Check `errors[]` for the specific field(s) and resend with corrected values.

## cursor_invalid

**HTTP 400.** The `?cursor=` value on a paginated LIST isn't a cursor this endpoint issued — malformed, expired, or copied from a different resource or sort.

**Fix:** Drop the cursor and start pagination over, or use the `nextCursor` from the endpoint's own most recent response.

## cursor_sort_mismatch

**HTTP 400.** A paginated request supplied both `?cursor=` and `?sort=`, and the sort doesn't match the one the cursor was minted under.

**Fix:** Keep the same `sort` value across every page of a paginated walk, or drop the cursor and restart.

## pagination_param_unknown

**HTTP 400.** A LIST request included a pagination query parameter the endpoint doesn't recognize.

**Fix:** Remove the unrecognized parameter; valid pagination params are `cursor`, `limit`, `sort`.

## sort_field_invalid

**HTTP 400.** `?sort=` named a field the resource doesn't allow sorting by.

**Fix:** Use one of the resource's allowed sort fields — `field` names the rejected field and `resource` the resource it was requested on.

## resource_unknown

**HTTP 400.** The resource name in the URL path (e.g. `/v3/<name>`) does not match any v3 resource.

**Fix:** Check the resource name against the v3 API reference for a typo.

## resource_misconfigured

**HTTP 400.** The server could not process the request.

**Fix:** Retrying is unlikely to help — report it with the `requestId`.

## search_query_missing

**HTTP 400.** A search endpoint was called without a `?q=` parameter.

**Fix:** Pass a non-empty `q`.

## search_query_too_short

**HTTP 400.** `?q=` is under 3 characters.

**Fix:** Use 3 or more characters.

## search_query_too_long

**HTTP 400.** `?q=` exceeds 200 characters.

**Fix:** Shorten the query.

## search_query_invalid

**HTTP 400.** `?q=` contains a URL, script tag, HTML, or email address — a spam-shield rejection.

**Fix:** Strip the flagged content; `reason` names which check failed (`url` | `script` | `html` | `email`).

## search_timed_out

**HTTP 400.** The search took too long to complete.

**Fix:** Narrow the search (e.g. scope to a project) instead of searching the whole workspace; retrying the identical query will likely time out again.

## identity_rejected

**HTTP 400.** The authenticated identity's type (human, agent, system, OAuth, etc.) isn't allowed to perform this specific operation.

**Fix:** Use a credential of the identity type the operation requires — `detail` names it.

## empty_update

**HTTP 400.** A PATCH body has zero fields to update.

**Fix:** Include at least one field to change.

## project_not_specified

**HTTP 400.** A project-scoped resource was created or listed without identifying a project.

**Fix:** Pass `projectId` in the request body (create) or `?projectId=` (list).

## accessible_project_ids_exceeded

**HTTP 400.** A workspace-scope LIST (no `?projectId=` filter) was requested by a caller with access to more than 1000 projects, exceeding the fan-out cap.

**Fix:** Narrow the request with `?projectId=<id>`.

## milestone_dependency_wrong_project

**HTTP 400.** A task's `dependencyId` references a task in a different project than the task being created or updated.

**Fix:** Use a dependency task from the same project.

## milestone_hiddenfor_roles_not_in_team

**HTTP 400.** A milestone's `hiddenFor` array names role IDs that don't belong to the team.

**Fix:** Use role IDs from `GET /v3/roles` for this team.

## favorite_folder_invalid

**HTTP 400.** `favoriteFolderId` doesn't reference a folder the caller owns.

**Fix:** Use a folder ID returned by the caller's own `GET /v3/favorite-folders`, or omit the field.

## expand_too_deep

**HTTP 400.** `?expand=` requested a relation chain deeper than the endpoint's depth limit.

**Fix:** Shorten the expand path — `max` names the limit, `depth` what you asked for, and `path` the offending entry.

## expand_cycle_detected

**HTTP 400.** An `?expand=` path revisits a resource already visited earlier in the same chain.

**Fix:** Remove the relation that creates the cycle.

## expand_unknown_relation

**HTTP 400.** An `?expand=` segment names a relation the resource doesn't have — usually a typo.

**Fix:** Check `available` for the valid relation names on this resource.

## expand_budget_exceeded

**HTTP 400.** The total rows materialized by `?expand=` across the whole request exceeded the per-request budget — protects against fan-out shapes like `?expand=subtasks.subtasks.subtasks`.

**Fix:** Drop the relation named in `relation` and re-issue with a narrower expand; `budget` names the per-request row budget.

## invalid_field_selector

**HTTP 400.** `?fields[resource]=` names a field the resource doesn't have.

**Fix:** Check the field name against the resource's schema.

## invalid_query_param

**HTTP 400.** A known query parameter was given a value outside its allowed set (e.g. `?include_trashed=maybe`).

**Fix:** Use one of the values in `allowed` for the parameter named in `name`.

## move_target_milestone_mismatch

**HTTP 400.** A cross-project task move (`PATCH task.projectId`) also set `milestoneId` to a milestone that lives in a different project than the new target.

**Fix:** Pass a `milestoneId` from the target project, or omit it and let the move default one.

## move_target_taskgroup_mismatch

**HTTP 400.** Same as `move_target_milestone_mismatch` but for `taskGroupId` (v3's name for the status column).

**Fix:** Pass a `taskGroupId` from the target project, or omit it.

## move_target_no_default_milestone

**HTTP 400.** A cross-project task move omitted `milestoneId` and the target project has no non-archived milestone to default to.

**Fix:** Create a milestone in the target project first, or pass an explicit `milestoneId`.

## move_target_no_default_taskgroup

**HTTP 400.** Same as `move_target_no_default_milestone` but for the target project's status column.

**Fix:** Create a status in the target project first, or pass an explicit `taskGroupId`.

## task_no_default_status

**HTTP 400.** A task is being created in a project that has no active status to default the task into.

**Fix:** Create a status in the project first, or pass an explicit `taskGroupId`.

## move_subtask_must_move_with_parent

**HTTP 400.** `task.projectId` was PATCHed directly on a subtask.

**Fix:** Move the parent task instead (the whole subtree moves with it), or clear `parentTaskId` first to promote the subtask to a root task, then move it.

## task_subtask_chain_too_deep

**HTTP 400.** A cross-project task move's subtree (root + descendants) exceeded 200 nodes; the transaction rolled back atomically rather than landing a half-moved tree.

**Fix:** Split the move into smaller per-subtree PATCH calls.

## task_dependency_incomplete

**HTTP 400.** `completed: true` was PATCHed on a task whose `dependency` task isn't complete yet.

**Fix:** Complete the dependency task first, or have a workspace admin enable the team's "Complete Dependant Tasks" setting.

## bulk_too_large

**HTTP 400.** A bulk write named more than 150 items.

**Fix:** Split the request into batches of 150 or fewer.

## batch_empty

**HTTP 400.** An M2M sub-resource request (e.g. `POST /v3/files/:id/labels`) sent a missing or empty `ids` array.

**Fix:** Pass a non-empty `ids: string[]`.

## batch_too_large

**HTTP 400.** An M2M sub-resource request's `ids` array exceeded the endpoint's max.

**Fix:** Split into smaller batches — `max` names the limit and `received` what you sent.

## m2m_relation_unknown

**HTTP 400.** An M2M sub-resource request named a relation the parent resource doesn't declare — usually a typo'd relation segment.

**Fix:** Use one of the relation names the error body lists for this resource.

## m2m_target_not_found

**HTTP 404.** One or more `ids` in an M2M sub-resource request don't exist, are soft-deleted, or belong to a different workspace. The whole operation is rejected rather than partially applied.

**Fix:** Drop the IDs listed in `missing` and retry.

## m2m_not_supported

**HTTP 501.** This relation is not available on this resource.

**Fix:** Retrying won't help; report it.

## unauthorized

**HTTP 401.** The request has no valid authentication credential — no bearer token, or the header is missing or malformed.

**Fix:** Send `Authorization: Bearer <token>` with a valid Personal Access Token or OAuth access token.

## invalid_token

**HTTP 401.** The bearer token isn't found, was revoked, or is otherwise invalid.

**Fix:** Mint a new Personal Access Token or re-run the OAuth flow.

## token_expired

**HTTP 401.** The bearer token's `expiresAt` has passed.

**Fix:** Mint a new token.

## token_owner_inactive

**HTTP 401.** The token's owning team member has been deactivated, or the owning user account has been deleted.

**Fix:** The token can't be reactivated — have an active member mint a new one.

## pat_cap_reached

**HTTP 400.** Creating a new Personal Access Token would exceed the 20-active-PAT-per-user cap.

**Fix:** Revoke an existing token first (`DELETE /personal-access-tokens/{id}`), then create the new one.

## cannot_broaden_scope

**HTTP 400.** Updating a PAT tried to add a scope it didn't already have.

**Fix:** Revoke the token and issue a new one with the broader scope set instead of updating in place.

## cannot_shorten_expiry

**HTTP 400.** Updating a PAT tried to set `expiresAt` earlier than its current value.

**Fix:** Use revoke instead of shortening expiry.

## cannot_create_empty_token

**HTTP 400.** The scopes requested for a new PAT don't intersect with the creating user's actual role permissions — the token would be able to do nothing.

**Fix:** Request only scopes the user's role actually grants.

## permission_denied

**HTTP 403.** The authenticated identity is missing one or more permissions the operation requires.

**Fix:** Check `missingPermissions` in the response and have a workspace admin grant them, or use a credential with the right role/scopes.

## project_access_denied

**HTTP 403.** The authenticated identity doesn't have access to the specific project the request targets.

**Fix:** Use a credential belonging to a team member with access to that project.

## default_label_locked

**HTTP 403.** The request tried to rename or delete a system default label (e.g. the archived-status label).

**Fix:** Default labels can't be mutated — create a new label instead.

## default_role_locked

**HTTP 403.** The request tried to rename, re-permission, or delete a system role (owner, manager, regular, client).

**Fix:** System roles are fixed — create a custom role instead.

## permission_exceeds_caller_grant

**HTTP 403.** A caller tried to set a role's `permissions` to include a permission they don't themselves currently hold.

**Fix:** Only grant permissions the acting identity already has.

## resource_not_found

**HTTP 404.** The requested resource ID doesn't exist, is soft-deleted, or isn't accessible to the caller.

**Fix:** Check the ID and the caller's access to that resource.

## conflict

**HTTP 409.** The generic catch-all for a state collision that doesn't have its own dedicated code — typically a business-rule invariant the current row state violates.

**Fix:** Re-read the resource's current state and adjust the request accordingly; `detail` names the specific conflict.

## duplicate_label

**HTTP 409.** A create or rename set a label name that already exists (case aside) among the team's active labels.

**Fix:** Use a different name.

## duplicate_role

**HTTP 409.** A create or rename set a role name that already exists in the team.

**Fix:** Use a different name.

## role_in_use

**HTTP 409.** A role delete was requested while the role is still assigned to one or more active team members.

**Fix:** Reassign those members to a different role first.

## regular_role_missing

**HTTP 409.** A role delete needs to reassign its former (removed) holders to the team's "regular" role, but the team has none.

**Fix:** Create a `regular` role in the team, or leave this role in place.

## stale_write

**HTTP 409.** An update included `expectedUpdatedAt` and the row's actual `updated_at` no longer matches — another writer committed after the read this update was based on.

**Fix:** Re-read the resource, re-derive your change from the current values, and retry with the fresh `updatedAt`. Do not reuse the value in `detail`.

## idempotency_key_reuse

**HTTP 422.** An `Idempotency-Key` was reused with a request body whose content fingerprint differs from the first use of that key.

**Fix:** Use a fresh `Idempotency-Key` for the new body.

## idempotency_in_progress

**HTTP 409.** A retry with the same `Idempotency-Key` arrived while the first request with that key was still being processed.

**Fix:** Back off briefly and retry — the first request's result will apply.

## name_contains_url

**HTTP 400.** A free-text name field (workspace, project, portfolio, label, etc.) contains a URL.

**Fix:** Remove the URL from the name.

## name_contains_email

**HTTP 400.** A free-text name field contains an email address.

**Fix:** Remove the email address from the name.

## name_contains_html

**HTTP 400.** A free-text name field contains HTML markup.

**Fix:** Remove the markup — use plain text.

## name_contains_script

**HTTP 400.** A free-text name field contains script-like markup.

**Fix:** Remove the script-like content.

## team_storage_quota_exceeded

**HTTP 413.** Uploading a file would put the team over its storage quota (`storage_per_user` × billable members, in GiB).

**Fix:** Free up space or upgrade the plan — `usage`/`limit`/`requested` in the body show the numbers.

## rate_limited

**HTTP 429.** The caller exceeded the API rate limit.

**Fix:** Back off and retry after `retryAfter` seconds (also in the `Retry-After` header); `scope` names which limit you hit.

## free_plan_write_cap_exceeded

**HTTP 429.** A free-plan team's programmatic caller (agent, or a PAT/OAuth-bearer human) hit the per-team, per-resource, per-day create cap.

**Fix:** Wait for the daily UTC reset, or upgrade the plan — the cap doesn't apply to paid teams. `limit`, `used` and `resource` describe the cap you hit.

## quota_exceeded

**HTTP 429.** The team has spent its monthly allowance for a metered dimension (automation runs, API/MCP calls, AI generations, form responses).

**Fix:** Wait until `resetsAt`, or have the billing owner raise the plan's limit for `dimension`. The same numbers arrive as the `X-Quota-Limit`, `X-Quota-Remaining` and `X-Quota-Reset` headers.

## not_implemented

**HTTP 501.** This operation is not available on this resource.

**Fix:** This operation isn't available for this resource — check the API reference for what it does support.

## create_not_durable

**HTTP 503.** The create could not be confirmed.

**Fix:** Retry with the same `Idempotency-Key`; it's safe either way — a true miss gets a fresh create, a false alarm replays the already-committed result.

## internal_error

**HTTP 500.** The fallback for an unexpected server-side error that doesn't map to any other code.

**Fix:** Retry once; if it persists, report it with the `requestId`.

## actor_resolution_failed

**HTTP 500.** The server could not resolve the acting identity.

**Fix:** Not caller-fixable — report it with the `requestId`.

## response_schema_violation

**HTTP 500.** The write was not applied.

**Fix:** Retry once with the same `Idempotency-Key`; if it persists, report it with the `requestId`.

## route_not_found

**HTTP 404.** No v3 route matches the method and path.

**Fix:** Check the method and the path against the v3 API reference.
