# Errors

> Every failure is an RFC 9457 problem with a stable machine-readable code, a human message, the right HTTP status, a request id and field-level errors[].

Source: https://developers.processcart.com/errors

Every error answers `application/problem+json` (RFC 9457) with the same shape:

```json
{
  "type": "https://developers.processcart.com/errors#validation_failed",
  "title": "Validation failed",
  "status": 422,
  "detail": "The request body failed validation.",
  "code": "validation_failed",
  "requestId": "req_1f3c…",
  "errors": [
    { "field": "amount", "code": "unparseable", "message": "Enter a number." }
  ]
}
```

- `code` is the **stable, machine-readable** reason — branch on it, never on `detail`. Every code is listed below and is the anchor its `type` URL points at.
- `type` is this page plus the code; `title` is constant per code; `detail` is a human sentence that may change.
- `status` repeats the HTTP status. The code's default status is listed below; a few codes can appear under more than one status where noted.
- `requestId` is the same value as the `X-Request-Id` header. Quote it to support.
- `errors[]` appears when the problem is field-level: each entry names the `field` (a body member such as `name` or a dotted path such as `target.fieldKey`, a query parameter, a header name, or — for a field value — the field's key, as above), a `code` from the vocabulary below and a `message`. A top-level code can also appear as an `errors[].code` when it concerns one field (`task_completed` on a field of a completed task); it is described under [Error codes](#error-codes).
- Request bodies are strict: a key an operation does not take is refused with `unrecognized_keys` on `body` (on `target` for one inside a file upload's `target`; the message names the key) instead of being dropped. `enabled` in a webhook, schedule or rule body answers `unknown_key` on `enabled` instead, and its message names the operations that turn the resource on and off. A map keyed by field or task keys answers its own code too: `PATCH /v1/runs/{id}/fields` (whose whole body is that map) and a schedule's `presetValues` refuse a key that names no field as `unknown_field` on that key, and a schedule's `assignmentOverrides` a key that names no task as `unknown_task`.
- A **404 `not_found`** means the key may not see the thing, not only that it does not exist: an id that is not a uuid or belongs to another workspace, a task or field key the run's version does not define, a private saved view, a webhook outside the key's allowlist. The answer never says which, so it reveals nothing about other workspaces. A run in Trash is not a 404: it can still be read, and a write to it answers 409 `run_trashed`.
- A **409 `revision_mismatch`** carries one extension member, `currentRevision`: the run's revision now. Read the run again, merge, and retry with `Expected-Revision: <currentRevision>` ([Concurrency](https://developers.processcart.com/guides/concurrency)).
- A request that carries **`If-Match`** is refused with 422 `validation_failed` (`errors[].field: "If-Match"`) on every authenticated operation: the API's concurrency header is `Expected-Revision`. Some hosting layers answer such a request with `412 Precondition Failed` before it reaches the API; both mean "send `Expected-Revision` instead".

Everything under `/api` answers this way — an unknown path is a 404 problem, a wrong verb a 405 problem with an `Allow` header, a non-JSON body a 415. The `code` vocabulary is **open**: new codes may be added without a version bump, so treat an unknown code as its HTTP status would suggest.

## Error codes

| Code | Status | Title |
| --- | --- | --- |
| [`unauthorized`](#unauthorized) | 401 | Authentication required |
| [`invalid_api_key`](#invalid_api_key) | 401 | Invalid API key |
| [`key_revoked`](#key_revoked) | 401 | API key revoked |
| [`insufficient_scope`](#insufficient_scope) | 403 | Insufficient scope |
| [`read_only_key`](#read_only_key) | 403 | Read-only key |
| [`not_allowed_for_key`](#not_allowed_for_key) | 403 | Not allowed for this key |
| [`rate_limited`](#rate_limited) | 429 | Too many requests |
| [`idempotency_key_reused`](#idempotency_key_reused) | 422 | Idempotency key reused |
| [`idempotency_in_progress`](#idempotency_in_progress) | 409 | Request in progress |
| [`validation_failed`](#validation_failed) | 422 | Validation failed |
| [`unsupported_media_type`](#unsupported_media_type) | 415 | Unsupported media type |
| [`payload_too_large`](#payload_too_large) | 413 | Payload too large |
| [`method_not_allowed`](#method_not_allowed) | 405 | Method not allowed |
| [`not_found`](#not_found) | 404 | Not found |
| [`revision_mismatch`](#revision_mismatch) | 409 | Revision mismatch |
| [`run_archived`](#run_archived) | 409 | Run is archived |
| [`run_trashed`](#run_trashed) | 409 | Run is in Trash |
| [`task_locked`](#task_locked) | 409 | Task is locked |
| [`task_hidden`](#task_hidden) | 409 | Task is hidden |
| [`task_completed`](#task_completed) | 409 | Task already completed |
| [`field_not_writable`](#field_not_writable) | 422 | Field is not writable |
| [`required_fields_unsatisfied`](#required_fields_unsatisfied) | 422 | Required fields unsatisfied |
| [`workflow_not_published`](#workflow_not_published) | 409 | Workflow not published |
| [`workflow_not_runnable`](#workflow_not_runnable) | 403 | Workflow not runnable |
| [`approver_only`](#approver_only) | 403 | Approver only |
| [`workflow_trashed`](#workflow_trashed) | 409 | Workflow is in Trash |
| [`wrong_task_kind`](#wrong_task_kind) | 409 | Wrong task kind |
| [`task_not_configured`](#task_not_configured) | 409 | Task not configured |
| [`upload_incomplete`](#upload_incomplete) | 409 | Upload incomplete |
| [`webhook_disabled`](#webhook_disabled) | 409 | Webhook disabled |
| [`delivery_not_terminal`](#delivery_not_terminal) | 409 | Delivery not finished |
| [`schedule_ended`](#schedule_ended) | 409 | Schedule ended |
| [`internal_error`](#internal_error) | 500 | Internal error |
| [`unavailable`](#unavailable) | 503 | Service unavailable |

### unauthorized
**401 Authentication required.** No credential was sent, or `Authorization` and `X-API-Key` carried two different values. Send the key as `Authorization: Bearer <key>`. The response carries `WWW-Authenticate: Bearer realm="processcart"`.

### invalid_api_key
**401 Invalid API key.** The key is not recognized — malformed, or not one this deployment issued. Check for stray whitespace and that the whole secret was copied.

### key_revoked
**401 API key revoked.** The key was revoked in Settings → API, or it was rotated and its 24-hour grace period has ended. Switch to the replacement key.

### insufficient_scope
**403 Insufficient scope.** The key does not hold the scope the operation needs; `detail` names it. Mint a key with that scope (see [Scopes](https://developers.processcart.com/reference/scopes)).

### read_only_key
**403 Read-only key.** A read-only key sent a request other than `GET` / `HEAD`. Read-only keys may only read, whatever scopes they hold.

### not_allowed_for_key
**403 Not allowed for this key.** The workflow (or its folder) is outside the key's allowlist. Also answers: a folder-restricted key on the webhook routes; a webhook whose `workflowIds` is not a non-empty subset of a workflow-restricted key's list; deleting a comment's file through the API; a view write by a key whose creator is no longer a Builder or Admin; an automation rule for every workflow (`workflowId: null`) used by a key with an allowlist.

### rate_limited
**429 Too many requests.** More than 600 requests in the key's current 60-second window. Wait `Retry-After` seconds (`RateLimit-Reset` says the same) and retry.

### idempotency_key_reused
**422 Idempotency key reused.** The same `Idempotency-Key` arrived with a different method, path, body or `Expected-Revision` — or from a different API key. Use a fresh key for every distinct request; nothing was changed.

### idempotency_in_progress
**409 Request in progress.** A request with this `Idempotency-Key` is still running. Retry after `Retry-After` seconds; the stored answer is then replayed.

### validation_failed
**422 Validation failed.** The query, body or a header failed validation — a malformed `Expected-Revision`, or any `If-Match` header (the API refuses it; send `Expected-Revision`). `errors[]` lists every problem with its `field` and a `code` from the vocabulary below; nothing was changed.

### unsupported_media_type
**415 Unsupported media type.** The request body is not `application/json` (the `Content-Type` header decides).

### payload_too_large
**413 Payload too large.** The request body exceeds 1,048,576 bytes, or a file exceeds 25 MB.

### method_not_allowed
**405 Method not allowed.** The path exists but not for this verb; the `Allow` header lists the verbs it supports.

### not_found
**404 Not found.** No such resource in your workspace: an id that is not a uuid, a run or workflow of another workspace, a task or field key the run's version does not define, a private saved view, a workflow or run outside what the key may see. The API never reveals whether the id exists elsewhere.

### revision_mismatch
**409 Revision mismatch.** `Expected-Revision` named a revision the run no longer has. The problem body's `currentRevision` (and its `detail`) carry the current revision — read the run again, merge, and retry with `Expected-Revision: <currentRevision>`.

### run_archived
**409 Run is archived.** The run is archived. Only a status change (`PATCH /v1/runs/{id}` with `status`) is accepted until it is active again.

### run_trashed
**409 Run is in Trash.** The run is in Trash. Restore it (`POST /v1/runs/{id}/restore`) before writing to it.

### task_locked
**409 Task is locked.** The task sits behind an incomplete stop task, or behind an approval that sent it back. Finish the earlier work first.

### task_hidden
**409 Task is hidden.** A visibility rule hides the task right now; it cannot be acted on until the rule's condition changes.

### task_completed
**409 Task already completed.** The task is already done: a field of a completed task was written, or an approval already approved was approved again. Reopen the task first (`/reopen`).

### field_not_writable
**422 Field is not writable.** The field cannot be written through the API: a formula, file or Display Text field, a field a formula computes, or a hidden field carrying a formula. `errors[]` names each such field with the reason as its `code` (`computed`, `file`, `display_text` — or `field_not_writable` itself). Files go through `POST /v1/runs/{id}/files`.

### required_fields_unsatisfied
**422 Required fields unsatisfied.** Completing the task would leave a required field blank, or keep a value its format rules refuse (out of bounds, a failed pattern): `errors[]` lists every offender with `code: "required"`. Starting a run (`POST /v1/runs`) answers it when a required kickoff field is missing: `errors[]` lists each missing one as `required` and, in the same response, every kickoff value its rules refuse as `invalid_value` (refused values alone at start are 422 `validation_failed`).

### workflow_not_published
**409 Workflow not published.** The workflow has no published version yet; publish it in the builder before starting runs, reading its schema, scheduling it (create, resume or run a schedule now, or change a schedule's presets or assignment overrides) or naming its tasks and fields in an automation rule.

### workflow_not_runnable
**403 Workflow not runnable.** The workflow is archived, so a schedule cannot start runs of it: creating a schedule of it, changing a schedule's presets or assignment overrides, resuming one or running one now answers this. Unarchive the workflow first. (A workflow outside the key's allowlist answers 403 `not_allowed_for_key` instead.)

### approver_only
**403 Approver only.** Deciding (or reopening) an approval needs the `approvals:decide` scope; the key does not hold it.

### workflow_trashed
**409 Workflow is in Trash.** The workflow is in Trash: restore it in the app before restoring its runs; before creating, resuming or running a schedule of it, or changing a schedule's presets or assignment overrides; or before creating, changing or turning on an automation rule of it.

### wrong_task_kind
**409 Wrong task kind.** The verb does not fit the task's kind: `approve` / `reject` on a standard task, `complete` on an approval task.

### task_not_configured
**409 Task not configured.** The task cannot be completed through the API as configured — an email task with no send configuration.

### upload_incomplete
**409 Upload incomplete.** Finalize was called before the file's bytes reached the upload URL. PUT the bytes first (see [Tasks, approvals & files](https://developers.processcart.com/guides/tasks-approvals-files#files)).

### webhook_disabled
**409 Webhook disabled.** The webhook is disabled; enable it (`POST /v1/webhooks/{id}/enable`) before sending a test event or redelivering.

### delivery_not_terminal
**409 Delivery not finished.** The delivery is still pending or mid-retry; only a delivered, failed or exhausted delivery can be redelivered.

### schedule_ended
**409 Schedule ended.** The schedule's end date has passed, so it has no future runs to resume; change its recurrence (a later end date, or none) before resuming.

### internal_error
**500 Internal error.** Something failed on our side. Retry once; if it persists, quote the `requestId` to support.

### unavailable
**503 Service unavailable.** A dependency did not answer — the database (the answer then carries `Retry-After`), storage signing a download link, or the encryption key a webhook secret needs. Retry with backoff; the status page shows whether a wider incident is open.


## The `errors[].code` vocabulary

| `errors[].code` | Meaning |
| --- | --- |
| `required` | A required value is missing (a body, a field, a comment, a group list). |
| `invalid_json` | The request body is not valid JSON. |
| `invalid` | A header or body value is malformed (`Expected-Revision`, `Idempotency-Key`, a filter, a webhook body), or a header this API refuses (`If-Match`). |
| `invalid_value` | The value is the right type but not acceptable (an impossible date, an out-of-bounds number, a failing custom rule). |
| `invalid_input` | The input is not acceptable as a whole (a comment body that is empty or too large). |
| `unknown_parameter` | A query parameter this endpoint does not take; the message suggests the camelCase spelling. |
| `unknown_key` | A body key that belongs to another operation: `enabled` on a webhook, schedule or rule body (the message names the operations that turn it on and off). |
| `invalid_cursor` | The cursor is not one this API handed out (or it was altered). |
| `cursor_mismatch` | The cursor belongs to a different filter or sort than this request. |
| `conflicts_with_updated_since` | `updatedSince` forces `updatedAt` ascending; an explicit conflicting `sort` / `order` is refused. |
| `unsupported_sort` | This list cannot be sorted by that key. |
| `viewer_relative` | A viewer-relative filter ("me") has no meaning for an API key. |
| `too_many_rows` | The export would exceed its row cap; narrow the filter. |
| `unknown_field` | No field with this key exists on the run's workflow version. |
| `field_not_writable` | The field cannot be written through this endpoint. |
| `computed` | The field is a formula; its value is computed, never written. |
| `file` | A File field is written through the files endpoints, not set-fields. |
| `too_large` | The stored file is empty or over the 25 MB limit; PUT the bytes again, or upload a smaller file. |
| `display_text` | A Display Text field holds no value. |
| `hidden` | The field is hidden by the workflow's rules right now. |
| `wrong_type` | The JSON type does not match the field (a string where a number belongs, …). |
| `unparseable` | The text could not be parsed in the field's format (a date, a phone number, …). |
| `not_finite` | A number must be finite. |
| `too_long` | The text exceeds the field's maximum length. |
| `not_a_choice` | The value is not one of the field's choices. |
| `not_an_item` | The value names a checklist item the field does not have. |
| `not_a_member` | The user id is not a member of this workspace. |
| `unknown_task` | No task with this key exists on the run. |
| `unknown_label` | A label id is not in the workspace's status-label set. |
| `unknown_workflow` | A workflow id is not in this workspace. |
| `unknown_folder` | No folder with this id is in the workspace. |
| `unknown_group` | No group with this id is in the workspace. |
| `unknown_webhook` | No webhook with this id is in the workspace. |
| `not_a_date_field` | The Key Date field must be date-typed. |
| `invalid_template` | The template JSON does not match the portable template format. |
| `unsupported_version` | The template's `formatVersion` / `engineVersion` is not one this API reads. |
| `publish_gate` | The template fails the publish checks a hand-built workflow must pass. |
| `duplicate_key` | The template repeats a section, task or field key; keys must be unique within a workflow. |
| `limit_reached` | A workspace or per-person maximum is reached (saved views per creator, schedules or enabled rules per workspace); delete or disable one first. |
| `url_policy` | The webhook URL is refused (https only; no private, loopback or own-host addresses). |
| `invalid_type` | The JSON type is wrong for this member (or a required member is missing). |
| `invalid_string` | The string fails its format (a uuid, an instant, a date shape, an email, a pattern). |
| `invalid_enum_value` | The value is not one of the allowed values. |
| `invalid_literal` | The value must be one exact constant. |
| `invalid_union` | The value matches none of the allowed shapes. |
| `invalid_union_discriminator` | The discriminator member names no known variant. |
| `invalid_date` | Not a valid date. |
| `too_small` | Below the minimum (length, count or value). |
| `too_big` | Above the maximum (length, count or value). |
| `not_multiple_of` | The number is not a multiple of the required step. |
| `unrecognized_keys` | The body carries a key this endpoint does not take. |
| `custom` | The value fails a rule this endpoint applies (the message says which). |

## Getting help

Every response — success or failure — carries `X-Request-Id`. The same id is in the request log under Settings → API, so an Admin can see what the key did. Include it when you write to support.
