# Errors

> Every error code the API returns, what it means, and what your integration should do about it.

Source: https://docs.xplantpro.com/docs/errors

A failed request answers with a non-2xx status and this body:

```json
{ "ok": false, "data": null, "error": "Missing scope: write:tasks", "code": "FORBIDDEN" }
```

**Branch on `code`, never on `error`.** Codes are stable; the `error` text is written for people and may be reworded. Several different failures can share a status (a `403` can be a missing scope, the owner's role, or the wrong kind of credential), so the status alone isn't enough either. As a rule of thumb, `402` is about the plan and `403` is about the role or a missing scope.

## Error codes

| Status | Code | Meaning |
| --- | --- | --- |
| `400` | `VALIDATION_ERROR` | The request body is not valid JSON. Or: The body is not valid JSON, and no token was minted. An empty body is not an error: it mints a token with every default. |
| `401` | `UNAUTHORIZED` | The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. |
| `402` | `DEVICE_LIMIT_REACHED` | The workspace has connected every device its plan includes — retire a device it no longer uses, or contact support to raise the limit — or its plan includes no connected devices at all. `error` says which. Nothing was registered. |
| `402` | `FEATURE_NOT_INCLUDED` | Culture line pricing, which includes sell-through, is not included in your plan. Or: Equipment calibration and maintenance records are not included in your plan. Or: A calibration or maintenance record was sent, and equipment calibration is not included in your plan. Or: Culture line pricing is not included in your plan. |
| `402` | `PAID_PLAN_REQUIRED` | The workspace's plan does not include the API. The full API is included with xPlant+ Teams and Enterprise; on Hobby and Pro Lab, keys can connect devices only. Or: The workspace has no paid plan. Connecting devices is included with every paid plan. |
| `402` | `PLAN_LIMIT_REACHED` | The key's owner is on the Free allowance and has created as many explants as it includes. Or: The workspace holds as many plants as its plan includes, or the key's owner is on the Free allowance and has used it. `error` says which limit. |
| `403` | `DEVICE_TOKEN_NOT_ACCEPTED` | A device token was sent; this operation needs a workspace API key. |
| `403` | `DEVICE_TOKEN_WRONG_DEVICE` | A device token was used to write about a device other than its own. |
| `403` | `EXPLANT_WRITE_FORBIDDEN` | The explant was created by someone else, and the key's owner is not a manager or owner of the lab. |
| `403` | `FORBIDDEN` | The key lacks a scope this operation requires, or its owner's current role in the workspace cannot use it — a key never does more than its owner can in xPlant. `error` names the scope, and for a role, the role it needs. Or: The key's owner is below the member role in the workspace. A key never does more than its owner can in xPlant; `error` names the role needed. |
| `403` | `MEDIA_RECIPE_NOT_OWNER` | The key's owner can see the recipe but did not create it. Only a recipe's creator can change it. |
| `403` | `PLANT_WRITE_FORBIDDEN` | The plant was created by someone else, and the key's owner is not a manager or owner of the lab. |
| `403` | `STAGE_WRITE_FORBIDDEN` | The plant or explant was created by someone else, and the key's owner is not a manager or owner of the lab. Nothing was changed. |
| `403` | `TRAINING_REQUIRED` | Your lab requires current training before an SOP is run, and the key's owner has none on this one: it was never recorded, has expired, or was revoked. Nothing is started. Once their training is recorded in xPlant, start the run again. |
| `404` | `NOT_FOUND` | `target_id` does not name a record of that kind in the key's workspace. Or: No file with this id is attached to a record in the key's workspace. Or: `entity_id` names no record of that `entity_type` in the key's workspace. Or: `entity_id` names no record of that `entity_type` in the workspace, or `parent_id` names no comment on it; `error` says which. Or: `product_link_id` names no store product in the key's workspace. Or: `plant_id` names no culture line in the key's workspace. Or: `plant_id` or `explant_id` names no plant or explant in the key's workspace; `error` says which. Or: No record with this id exists in the key's workspace. Or: `device_id` does not name a device in the workspace. Or: `room_id` does not name a growing room in the workspace. Nothing was registered. Or: No device with this id is registered in the key's workspace. Or: No token with this id belongs to this device in the key's workspace. Or: The equipment, or a use's `subject_id`, is not in the key's workspace. Or: The plant or explant named in the scan is not in this workspace. Or: Nothing in this workspace matches the code. Or: No recipe with this id is visible to the key's owner in the workspace: it does not exist there, or it is another member's private recipe. Or: A `device_id` or `room_id` in the request is not in the workspace. Nothing was stored. Or: The SOP, or the plant named in `plant_id`, is not in this workspace. Or: No run with this id exists in the key's workspace, or the run id is not a well-formed id. Or: `stepId` is not the `id` of a step in the version the run follows, or the run follows no recorded version of its SOP and so has no steps to record against. Nothing is recorded. Or: No SOP with this id exists in the key's workspace, or `id` is not a well-formed id. Or: The plant or explant is not in this workspace. Or: The plant or explant, or the growing room named in `room_id`, is not in this workspace. Nothing was changed. Or: No task with this id exists in the key's workspace, or `id` is not a well-formed id. |
| `409` | `CONFLICT` | The task's discussion is locked. Only the task's creator or a lab manager can comment. |
| `409` | `DEVICE_INGEST_DISABLED` | A device in the request is paused or retired, so its readings are refused. `error` names it; nothing was stored. |
| `409` | `DUPLICATE_ENTRY` | Another explant in the workspace already uses this `external_id`, including one that has been deleted. Or: Another plant in the workspace already uses this `external_id`, including one that has been deleted. |
| `409` | `IDEMPOTENCY_IN_FLIGHT` | A request with this `Idempotency-Key` is still being processed; retry after `Retry-After` seconds. |
| `409` | `SOP_RUN_CLOSED` | The run has ended: its status is one of `completed`, `failed`, `cancelled`, `archived`. An ended run's record is final and takes no more evidence. |
| `409` | `SOP_RUN_NOT_EFFECTIVE` | The SOP has no version in force, so there is nothing approved to run. Put a version into force in xPlant, then start the run. |
| `413` | `PAYLOAD_TOO_LARGE` | The image is larger than 10 MB. |
| `415` | `UNSUPPORTED_MEDIA_TYPE` | The file is not a JPEG, PNG, WebP or GIF image. The type is read from the file itself. |
| `422` | `IMAGE_URL_FETCH_FAILED` | `image_url` could not be downloaded: it redirected, answered with an error status, took longer than 10 seconds, or its host could not be found. `error` says which. |
| `422` | `INVALID_CURSOR` | The cursor is malformed, or came from a different list or different filters. Start again without it. |
| `422` | `VALIDATION_ERROR` | Both `cursor` and `offset` were sent; use one. Or: `target` or `target_id` is missing or not valid. Or: The `Idempotency-Key` header is malformed. Or: A field failed validation. `error` names the first one, for example `title: title is required`. Or: `target_id` does not name a record of that kind in the key's workspace. Or: `image_url` is not an `https` address on the public internet. Or: `entity_type` or `entity_id` is missing or malformed. Or: A mentioned user is not an active member of the workspace, or a linked record is not in it. Or: A query parameter is malformed, or `from` is later than `to`. `error` names it. Or: A filter is malformed; `error` names it. Or: `custom_fields` names a field your lab has not defined, or gives a field a value of the wrong kind. `error` names the field. Or: A field failed validation. `error` names the first one, for example `event_type: Invalid enum value`. Or: A field failed validation. `error` names the first one, for example `name: String must contain at least 1 character(s)`. Or: A field failed validation, such as a `name` longer than 120 characters. `error` names it. Or: A query parameter is malformed, `from` is later than `to`, or `offset` was sent without `kind`. `error` names the parameter. Or: `entity` is missing or is not `plant` or `explant`, or `since` is not a valid timestamp. Or: `plant_id` is not a plant in the workspace. Or: `custom_fields` names a field the lab has not set up, gives a field a value that does not fit it, or totals more than 10,000 bytes. `error` names the field. Or: A field failed validation, for example a missing `barcode` or a `scanned_at` that is not a timestamp with a time zone. `error` gives the first problem but does not name the field. Or: `barcode` is missing or blank. `error` names it: `barcode: barcode is required`. Or: `status` is not one of the recipe statuses. Or: A query parameter is malformed. `error` names it. Or: A query parameter is malformed: `device_id` or `room_id` is not a UUID, `since` or `until` is not a timestamp, `since` is later than `until`, or `limit` is not a whole number. `error` names the parameter, for example `device_id: must be a UUID`. Or: A reading failed validation. `error` names the first field, for example `readings.0.value: Expected number, received string`. Or: A field failed validation, for example `sop_id` is missing or is not an id. `error` gives the first problem but does not name the field. Or: A field failed validation — an `event_type` outside the list, or a `recorded_at` that is not a timestamp with a time zone. `error` gives the first problem but does not name the field. Or: A field failed validation — a missing `unit`, a `value` that is not a number, or a `recorded_at` that is not a timestamp with a time zone. `error` gives the first problem but does not name the field. Or: Neither or both of `plant_id` and `explant_id` were sent. Or: A field failed validation, or neither or both of `plant_id` and `explant_id` were sent. `error` names the first problem, for example `stage: stage is required`. Or: `stage` is not in your lab's stage list for a plant, or for an explant — whichever you sent. `error` starts `stage:` and names the list. Nothing was changed. Or: A field failed validation, or neither or both of `plant_id` and `explant_id` were sent. `error` names the first problem, for example `transfer_cycle: Number must be greater than or equal to 1`. Or: `custom_fields` names a field your lab has not defined, gives a field a value of the wrong kind, or was sent for a plant transfer. `error` names the field. |
| `429` | `RATE_LIMIT_EXCEEDED` | Too many requests for this key, device token or workspace. Wait `Retry-After` seconds. |
| `5xx` | `…_FAILED` | Something failed on our side; each endpoint names its own code. Retry later; don't branch on the specific code. |

## What to do about each

| Code | Retry? | What to do |
| --- | --- | --- |
| `VALIDATION_ERROR` | No | Fix the request. The `error` text names the first field that failed, e.g. `title: title is required`. |
| `UNAUTHORIZED` | No | Check the key. It may be mistyped or revoked, or its creator may have left the workspace. |
| `PAID_PLAN_REQUIRED` | No | The workspace's plan doesn't include this call. Teams and Enterprise include the whole API; Hobby and Pro Lab keys can connect devices only; Free has no API access. See [Plans and access](https://docs.xplantpro.com/docs/authentication.md#plans-and-access). |
| `DEVICE_LIMIT_REACHED` | No | Registering this device would go past your plan's device allowance. Retire a device you no longer use, or see [plans](https://www.xplantpro.com/en/subscriptions). |
| `FORBIDDEN` | No | Either the key lacks the scope named in `error` (use a key that has it), or the owner's role is below what the scope needs (the message names the role; see [Plans and access](https://docs.xplantpro.com/docs/authentication.md#plans-and-access)). |
| `DEVICE_TOKEN_NOT_ACCEPTED` | No | This endpoint needs a workspace key. Device tokens only write readings, events and heartbeats. |
| `TRAINING_REQUIRED` | No | The lab requires training for this SOP and the key's owner doesn't have it (or it has expired). Get them trained in xPlant first. |
| `STAGE_WRITE_FORBIDDEN` | No | Only the plant's or explant's creator, or a manager, can move its stage. Use a key owned by one of them. |
| `SOP_RUN_CLOSED` | No | The run has ended; start a new run instead of writing to it. |
| `DEVICE_TOKEN_WRONG_DEVICE` | No | The token belongs to a different device than the `device_id` you sent. |
| `NOT_FOUND` | No | The id doesn't exist in this workspace (or belongs to another one; the two look the same on purpose). |
| `IDEMPOTENCY_IN_FLIGHT` | Yes | The first request with this `Idempotency-Key` is still running. Wait `Retry-After` seconds (1) and send the same request again. |
| `RATE_LIMIT_EXCEEDED` | Yes | Wait the `Retry-After` seconds, then continue. See [Rate limits](https://docs.xplantpro.com/docs/rate-limits.md). |
| `…_FAILED` (5xx) | Yes, later | Something went wrong on our side. Retry with backoff; for a write, reuse the same `Idempotency-Key`. Don't branch on the specific code. |

Each endpoint's page lists the errors it can return.

When you contact support about a failed call, include its `X-Request-Id` response header (`err.requestId` in the SDK). It identifies exactly that request.

## Retrying well

- Retry only `429`, `409 IDEMPOTENCY_IN_FLIGHT`, `5xx` and network failures. Everything else fails the same way the second time.
- Honour `Retry-After`: it's a whole number of seconds.
- Back off exponentially on `5xx` and network errors, and cap the number of attempts.
- Before retrying a write, make sure a repeat is harmless: send an [`Idempotency-Key`](https://docs.xplantpro.com/docs/idempotency.md), or for sensor readings an `external_id` with `recorded_at`.

The JavaScript SDK does all of this when you pass `retry: true` to the client.
