{
  "openapi": "3.1.0",
  "info": {
    "title": "xPlant API",
    "version": "1",
    "description": "The xPlant REST API. Every request is scoped to the calling key's workspace. Responses are `{ ok: true, data }` on success and `{ ok: false, data: null, error, code }` on failure; branch on `code`.",
    "contact": {
      "name": "xPlant support",
      "email": "support@xplantpro.com"
    }
  },
  "externalDocs": {
    "description": "Guides and examples",
    "url": "https://github.com/shmaplex/xplant_os"
  },
  "servers": [
    {
      "url": "https://app.xplantpro.com"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "x-scopes": [
    {
      "id": "read:workspace",
      "label": "Read workspace",
      "description": "Read workspace and lab settings",
      "category": "Workspace"
    },
    {
      "id": "read:plants",
      "label": "Read plants",
      "description": "Read plant records",
      "category": "Plants"
    },
    {
      "id": "write:plants",
      "label": "Write plants",
      "description": "Create and update plant records",
      "category": "Plants"
    },
    {
      "id": "read:explants",
      "label": "Read explants",
      "description": "Read explant records",
      "category": "Explants"
    },
    {
      "id": "write:explants",
      "label": "Write explants",
      "description": "Create and update explant records",
      "category": "Explants"
    },
    {
      "id": "read:contaminations",
      "label": "Read contaminations",
      "description": "Read contamination logs",
      "category": "Contaminations"
    },
    {
      "id": "write:contaminations",
      "label": "Write contaminations",
      "description": "Submit contamination observations",
      "category": "Contaminations"
    },
    {
      "id": "read:tasks",
      "label": "Read tasks",
      "description": "Read scheduled tasks and due work",
      "category": "Tasks"
    },
    {
      "id": "write:tasks",
      "label": "Write tasks",
      "description": "Create, update, complete, and reopen tasks",
      "category": "Tasks"
    },
    {
      "id": "write:demand",
      "label": "Write demand signals",
      "description": "Push sales/order demand numbers per genus for task prioritization",
      "category": "Tasks"
    },
    {
      "id": "read:comments",
      "label": "Read comments",
      "description": "Read notes and comments on lab records",
      "category": "Comments"
    },
    {
      "id": "write:comments",
      "label": "Write comments",
      "description": "Add notes and comments to lab records",
      "category": "Comments"
    },
    {
      "id": "read:assets",
      "label": "Read media",
      "description": "Read photos and media attached to lab records",
      "category": "Media"
    },
    {
      "id": "write:assets",
      "label": "Write media",
      "description": "Attach photos and media to lab records",
      "category": "Media"
    },
    {
      "id": "read:media_recipes",
      "label": "Read media recipes",
      "description": "Read media recipe data",
      "category": "Media"
    },
    {
      "id": "write:media_recipes",
      "label": "Write media recipes",
      "description": "Create and update media recipes",
      "category": "Media"
    },
    {
      "id": "read:transfers",
      "label": "Read transfers",
      "description": "Read transfer and stage history for plants and explants",
      "category": "Transfers"
    },
    {
      "id": "write:transfers",
      "label": "Write transfers",
      "description": "Record transfers and advance the stage of plants and explants",
      "category": "Transfers"
    },
    {
      "id": "read:sops",
      "label": "Read SOPs",
      "description": "Read SOP templates and steps",
      "category": "SOPs"
    },
    {
      "id": "read:sop_runs",
      "label": "Read SOP run history",
      "description": "Read SOP execution history: who ran which step, when",
      "category": "SOPs"
    },
    {
      "id": "write:sop_runs",
      "label": "Write SOP runs",
      "description": "Create and advance SOP run sessions",
      "category": "SOPs"
    },
    {
      "id": "write:sop_steps",
      "label": "Write SOP step evidence",
      "description": "Post confirmations, scans and measurements against a step of an SOP run",
      "category": "SOPs"
    },
    {
      "id": "read:labels",
      "label": "Read labels",
      "description": "Resolve QR/barcode label codes to entity records",
      "category": "Labels"
    },
    {
      "id": "write:label_scans",
      "label": "Write label scans",
      "description": "Submit label scan events",
      "category": "Labels"
    },
    {
      "id": "read:devices",
      "label": "Read devices",
      "description": "List registered devices and their status",
      "category": "Devices"
    },
    {
      "id": "write:devices",
      "label": "Write devices",
      "description": "Register devices, send heartbeats, and manage device tokens",
      "category": "Devices"
    },
    {
      "id": "read:sensor_readings",
      "label": "Read sensor readings",
      "description": "Query historical sensor readings",
      "category": "Devices"
    },
    {
      "id": "write:sensor_readings",
      "label": "Write sensor readings",
      "description": "Submit environmental sensor readings (temperature, humidity, etc.)",
      "category": "Devices"
    },
    {
      "id": "write:device_events",
      "label": "Write device events",
      "description": "Submit device status events",
      "category": "Devices"
    },
    {
      "id": "read:equipment",
      "label": "Read equipment",
      "description": "Read the lab equipment library and its maintenance history",
      "category": "Equipment"
    },
    {
      "id": "write:equipment_events",
      "label": "Write equipment events",
      "description": "Record that a piece of equipment was used, calibrated, serviced or faulted",
      "category": "Equipment"
    },
    {
      "id": "read:pricing",
      "label": "Read culture line pricing",
      "description": "Read culture line prices, pricing tiers, and price history",
      "category": "Commercial"
    },
    {
      "id": "read:commerce",
      "label": "Read sell-through",
      "description": "Read store order lines and sell-through summaries",
      "category": "Commercial"
    },
    {
      "id": "read:events",
      "label": "Read change history",
      "description": "Read plant and explant edit history (audit events)",
      "category": "History"
    }
  ],
  "tags": [
    {
      "name": "Account",
      "description": "Who the calling key is, and the workspace it belongs to."
    },
    {
      "name": "Change history",
      "description": "What happened to plant and explant records, oldest first, for incremental sync."
    },
    {
      "name": "Comments",
      "description": "Notes and comments on lab records."
    },
    {
      "name": "Contamination",
      "description": "Contamination observations on plants and explants."
    },
    {
      "name": "Demand signals",
      "description": "Sales and order demand per genus, used to prioritise the task queue."
    },
    {
      "name": "Devices",
      "description": "Register devices, send heartbeats and manage device tokens."
    },
    {
      "name": "Equipment",
      "description": "The lab equipment library and its use and maintenance history."
    },
    {
      "name": "Explants",
      "description": "Explant and culture records, with their vessel and plantlet counts."
    },
    {
      "name": "Labels",
      "description": "Resolve a scanned QR code or barcode to the record it identifies, and record that a scan happened."
    },
    {
      "name": "Media files",
      "description": "Photos and files attached to lab records."
    },
    {
      "name": "Media recipes",
      "description": "Culture media recipes and their components."
    },
    {
      "name": "Plants",
      "description": "Plant records in the workspace."
    },
    {
      "name": "Pricing",
      "description": "Culture line prices, pricing tiers and price history."
    },
    {
      "name": "Sell-through",
      "description": "Store order lines and sell-through summaries."
    },
    {
      "name": "Sensor readings",
      "description": "Environmental readings such as temperature and humidity."
    },
    {
      "name": "SOP runs",
      "description": "Run a procedure step by step and record evidence against each step."
    },
    {
      "name": "SOPs",
      "description": "Standard operating procedures and their steps."
    },
    {
      "name": "Stages",
      "description": "The stage history of plants and explants."
    },
    {
      "name": "Tasks",
      "description": "The lab's task queue: create, schedule, prioritise, assign and complete work."
    },
    {
      "name": "Transfers",
      "description": "Transfers of plants and explants onto fresh media."
    }
  ],
  "paths": {
    "/api/v1/assets": {
      "get": {
        "operationId": "listAssets",
        "tags": [
          "Media files"
        ],
        "summary": "List a record's photos and files",
        "description": "The photos and files attached to one plant, explant, contamination log or SOP, newest first. Each carries a `view_url`: a link to the file itself that works for 15 minutes. List again for fresh links, and never store one.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "read:assets"
        ],
        "x-idempotent": false,
        "x-cursor": true,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "name": "target",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "plant",
                "explant",
                "contamination",
                "sop"
              ]
            },
            "description": "The kind of record whose files to list: `plant`, `explant`, `contamination`, `sop`."
          },
          {
            "name": "target_id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The id of that record."
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Asset"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/PageMeta"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": [
                    {
                      "id": "8c2f1a6e-4b3d-4f7a-9e21-5d6c7b8a9f10",
                      "target": "explant",
                      "target_id": "3f9a2c1e-7b4d-4e8f-a6c5-1d2e3f4a5b6c",
                      "kind": "photo",
                      "file_name": "vessel-12-week-3.png",
                      "content_type": "image/png",
                      "caption": "Callus forming at the cut edge",
                      "captured_at": null,
                      "uploaded_by": "0d7e4b9a-6f21-4c3e-8a5b-2e9f1c7d4a60",
                      "created_at": "2026-09-25T14:20:11.000Z",
                      "view_url": "https://files.example.com/vessel-12-week-3.png?signature=4f9c2e",
                      "view_url_expires_at": "2026-09-25T14:35:11.000Z"
                    }
                  ],
                  "meta": {
                    "next_cursor": null
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "`NOT_FOUND` — `target_id` does not name a record of that kind in the key's workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "NOT_FOUND"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "`target_id` does not name a record of that kind in the key's workspace."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "422": {
            "description": "`VALIDATION_ERROR` — Both `cursor` and `offset` were sent; use one.\n\n`INVALID_CURSOR` — The cursor is malformed, or came from a different list or different filters. Start again without it.\n\n`VALIDATION_ERROR` — `target` or `target_id` is missing or not valid.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR",
                            "INVALID_CURSOR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "Both `cursor` and `offset` were sent; use one."
              },
              {
                "code": "INVALID_CURSOR",
                "when": "The cursor is malformed, or came from a different list or different filters. Start again without it."
              },
              {
                "code": "VALIDATION_ERROR",
                "when": "`target` or `target_id` is missing or not valid."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`ASSET_QUERY_FAILED` — The files could not be read. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "ASSET_QUERY_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "ASSET_QUERY_FAILED",
                "when": "The files could not be read. Retry later."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createAsset",
        "tags": [
          "Media files"
        ],
        "summary": "Attach an image to a record",
        "description": "Attaches an image to a plant, explant, contamination log or SOP in the workspace. It appears on that record in xPlant exactly like a photo added in the app. Send the image as `image_url` or as `image_base64` — exactly one.\n\n**Limits.** JPEG, PNG, WebP or GIF, up to 10 MB. The type is read from the image itself, not from its name. A request body can be at most about 4.5 MB, and base64 makes a file about a third larger, so send `image_base64` for images up to about 3 MB and `image_url` for anything larger. A request body over that size is refused with `413` before it reaches the API.\n\n**`image_url`.** xPlant downloads the image once, over `https`, from the public internet. Addresses on private or local networks are refused, redirects are not followed, and the download must finish within 10 seconds.\n\nThe response's `view_url` works for 15 minutes. A retry with the same `Idempotency-Key` returns the first response, with a fresh `view_url`.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "write:assets"
        ],
        "x-idempotent": true,
        "x-cursor": false,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "target": {
                    "type": "string",
                    "enum": [
                      "plant",
                      "explant",
                      "contamination",
                      "sop"
                    ],
                    "description": "The kind of record to attach the image to: `plant`, `explant`, `contamination`, `sop`."
                  },
                  "target_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "The id of that record. It must be in the key's workspace."
                  },
                  "image_url": {
                    "type": "string",
                    "format": "uri",
                    "maxLength": 2048,
                    "description": "An `https` address xPlant downloads the image from, up to 10 MB. It must be reachable from the public internet: private and local network addresses are refused, and redirects are not followed, so send the image's final address. The download must finish within 10 seconds. Use this for anything larger than about 3 MB."
                  },
                  "image_base64": {
                    "type": "string",
                    "minLength": 1,
                    "description": "The image itself, base64-encoded, without a `data:` prefix. A request body can be at most about 4.5 MB and base64 makes a file about a third larger, so this suits images up to about 3 MB. Send anything larger, up to the 10 MB limit, as `image_url`."
                  },
                  "filename": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200,
                    "description": "The name to show for the file, for example `leaf-sample-3.jpg`. Defaults to the last part of `image_url`, or `photo` for an inline image."
                  },
                  "caption": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 500,
                    "description": "A short note stored with the image and shown beside it in xPlant."
                  }
                },
                "required": [
                  "target",
                  "target_id"
                ]
              },
              "example": {
                "target": "explant",
                "target_id": "3f9a2c1e-7b4d-4e8f-a6c5-1d2e3f4a5b6c",
                "image_base64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==",
                "filename": "vessel-12-week-3.png",
                "caption": "Callus forming at the cut edge"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/Asset"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": {
                    "id": "8c2f1a6e-4b3d-4f7a-9e21-5d6c7b8a9f10",
                    "target": "explant",
                    "target_id": "3f9a2c1e-7b4d-4e8f-a6c5-1d2e3f4a5b6c",
                    "kind": "photo",
                    "file_name": "vessel-12-week-3.png",
                    "content_type": "image/png",
                    "caption": "Callus forming at the cut edge",
                    "captured_at": null,
                    "uploaded_by": "0d7e4b9a-6f21-4c3e-8a5b-2e9f1c7d4a60",
                    "created_at": "2026-09-25T14:20:11.000Z",
                    "view_url": "https://files.example.com/vessel-12-week-3.png?signature=4f9c2e",
                    "view_url_expires_at": "2026-09-25T14:35:11.000Z"
                  }
                }
              }
            },
            "headers": {
              "Idempotent-Replay": {
                "$ref": "#/components/headers/IdempotentReplay"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "400": {
            "description": "`VALIDATION_ERROR` — The request body is not valid JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "The request body is not valid JSON."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "409": {
            "description": "`IDEMPOTENCY_IN_FLIGHT` — A request with this `Idempotency-Key` is still being processed; retry after `Retry-After` seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "IDEMPOTENCY_IN_FLIGHT"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "IDEMPOTENCY_IN_FLIGHT",
                "when": "A request with this `Idempotency-Key` is still being processed; retry after `Retry-After` seconds."
              }
            ],
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "413": {
            "description": "`PAYLOAD_TOO_LARGE` — The image is larger than 10 MB.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAYLOAD_TOO_LARGE"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAYLOAD_TOO_LARGE",
                "when": "The image is larger than 10 MB."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "415": {
            "description": "`UNSUPPORTED_MEDIA_TYPE` — The file is not a JPEG, PNG, WebP or GIF image. The type is read from the file itself.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNSUPPORTED_MEDIA_TYPE"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "UNSUPPORTED_MEDIA_TYPE",
                "when": "The file is not a JPEG, PNG, WebP or GIF image. The type is read from the file itself."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "422": {
            "description": "`VALIDATION_ERROR` — The `Idempotency-Key` header is malformed.\n\n`VALIDATION_ERROR` — A field failed validation. `error` names the first one, for example `title: title is required`.\n\n`VALIDATION_ERROR` — `target_id` does not name a record of that kind in the key's workspace.\n\n`VALIDATION_ERROR` — `image_url` is not an `https` address on the public internet.\n\n`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR",
                            "IMAGE_URL_FETCH_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "The `Idempotency-Key` header is malformed."
              },
              {
                "code": "VALIDATION_ERROR",
                "when": "A field failed validation. `error` names the first one, for example `title: title is required`."
              },
              {
                "code": "VALIDATION_ERROR",
                "when": "`target_id` does not name a record of that kind in the key's workspace."
              },
              {
                "code": "VALIDATION_ERROR",
                "when": "`image_url` is not an `https` address on the public internet."
              },
              {
                "code": "IMAGE_URL_FETCH_FAILED",
                "when": "`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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`ASSET_UPLOAD_FAILED` — The image could not be stored. Retry with the same `Idempotency-Key`.\n\n`ASSET_INSERT_FAILED` — The image could not be saved to the record. Retry with the same `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "ASSET_UPLOAD_FAILED",
                            "ASSET_INSERT_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "ASSET_UPLOAD_FAILED",
                "when": "The image could not be stored. Retry with the same `Idempotency-Key`."
              },
              {
                "code": "ASSET_INSERT_FAILED",
                "when": "The image could not be saved to the record. Retry with the same `Idempotency-Key`."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      }
    },
    "/api/v1/assets/{id}": {
      "get": {
        "operationId": "getAsset",
        "tags": [
          "Media files"
        ],
        "summary": "Get a photo or file",
        "description": "One photo or file, with a `view_url` that works for 15 minutes from this response. Fetch it again whenever you need the file; never store the link.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "read:assets"
        ],
        "x-idempotent": false,
        "x-cursor": false,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The record's id."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/Asset"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": {
                    "id": "8c2f1a6e-4b3d-4f7a-9e21-5d6c7b8a9f10",
                    "target": "explant",
                    "target_id": "3f9a2c1e-7b4d-4e8f-a6c5-1d2e3f4a5b6c",
                    "kind": "photo",
                    "file_name": "vessel-12-week-3.png",
                    "content_type": "image/png",
                    "caption": "Callus forming at the cut edge",
                    "captured_at": null,
                    "uploaded_by": "0d7e4b9a-6f21-4c3e-8a5b-2e9f1c7d4a60",
                    "created_at": "2026-09-25T14:20:11.000Z",
                    "view_url": "https://files.example.com/vessel-12-week-3.png?signature=4f9c2e",
                    "view_url_expires_at": "2026-09-25T14:35:11.000Z"
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "`NOT_FOUND` — No file with this id is attached to a record in the key's workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "NOT_FOUND"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "No file with this id is attached to a record in the key's workspace."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`ASSET_QUERY_FAILED` — The file could not be read. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "ASSET_QUERY_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "ASSET_QUERY_FAILED",
                "when": "The file could not be read. Retry later."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      }
    },
    "/api/v1/comments": {
      "get": {
        "operationId": "listComments",
        "tags": [
          "Comments"
        ],
        "summary": "List comments on a record",
        "description": "The discussion on one plant, explant, contamination log, task, media recipe or SOP, oldest first. Replies carry `parent_id`. A deleted comment keeps its place, with its text removed, so the replies to it still read in order.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "read:comments"
        ],
        "x-idempotent": false,
        "x-cursor": true,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "name": "entity_type",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "plant",
                "explant",
                "contamination",
                "task",
                "media_recipe",
                "sop"
              ]
            },
            "description": "The kind of record whose comments to list."
          },
          {
            "name": "entity_id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The id of the record whose comments to list."
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Comment"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/PageMeta"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": [
                    {
                      "id": "9c4e2a71-3d5b-4f86-b1e0-7a2d6c8f3e45",
                      "entity_type": "explant",
                      "entity_id": "6d2f9a14-8b3e-4c71-a5d0-1f7e3b8c2a96",
                      "parent_id": null,
                      "body": "@sam the Alocasia line on shelf 2 is ready to go onto fresh medium. See #task:Subculture-Alocasia-line.",
                      "status": "active",
                      "is_pinned": false,
                      "author": {
                        "id": "0d7e4b9a-6f21-4c3e-8a5b-2e9f1c7d4a60",
                        "name": "Sam Okafor"
                      },
                      "mentioned_user_ids": [
                        "4a8f1c63-2e7d-4b95-8c10-5d3e9f7a2b68"
                      ],
                      "references": [
                        {
                          "entity_type": "task",
                          "entity_id": "5b0f6f3e-2c1a-4d8e-9f47-0a6c3e1b7d22",
                          "label": "Subculture Alocasia line"
                        }
                      ],
                      "created_at": "2026-09-25T09:15:40.000Z",
                      "updated_at": "2026-09-25T09:15:40.000Z",
                      "edited_at": null
                    }
                  ],
                  "meta": {
                    "next_cursor": null
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "`NOT_FOUND` — `entity_id` names no record of that `entity_type` in the key's workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "NOT_FOUND"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "`entity_id` names no record of that `entity_type` in the key's workspace."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "422": {
            "description": "`VALIDATION_ERROR` — Both `cursor` and `offset` were sent; use one.\n\n`INVALID_CURSOR` — The cursor is malformed, or came from a different list or different filters. Start again without it.\n\n`VALIDATION_ERROR` — `entity_type` or `entity_id` is missing or malformed.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR",
                            "INVALID_CURSOR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "Both `cursor` and `offset` were sent; use one."
              },
              {
                "code": "INVALID_CURSOR",
                "when": "The cursor is malformed, or came from a different list or different filters. Start again without it."
              },
              {
                "code": "VALIDATION_ERROR",
                "when": "`entity_type` or `entity_id` is missing or malformed."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`COMMENT_QUERY_FAILED` — The comments could not be read. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "COMMENT_QUERY_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "COMMENT_QUERY_FAILED",
                "when": "The comments could not be read. Retry later."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createComment",
        "tags": [
          "Comments"
        ],
        "summary": "Comment on a record",
        "description": "Adds a comment to a plant, explant, contamination log, task, media recipe or SOP, as the key's owner. It is the same comment the app makes: members it names are notified, the author of the comment it replies to is told, the people following a task hear about it, and it appears in the record's activity.\n\nThe key's owner needs the member role or above in the workspace, as in the app.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "write:comments"
        ],
        "x-idempotent": true,
        "x-cursor": false,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "entity_type": {
                    "type": "string",
                    "enum": [
                      "plant",
                      "explant",
                      "contamination",
                      "task",
                      "media_recipe",
                      "sop"
                    ],
                    "description": "The kind of record to comment on."
                  },
                  "entity_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "The id of the record to comment on."
                  },
                  "body": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 5000,
                    "description": "The comment, in Markdown. Stored exactly as sent. To mention someone, write `@` and their username in the text and add their user id to `mentioned_user_ids`. To link a record, write `#plant:` (or `#explant:`, `#contamination:`, `#task:`, `#media:`, `#sop:`) followed by its name, and list it in `references`."
                  },
                  "parent_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Reply to this comment. It must be on the same record."
                  },
                  "mentioned_user_ids": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "maxItems": 25,
                    "description": "Workspace members this comment names. Each is notified, exactly as when someone is mentioned in the app. Every id must be an active member of the workspace."
                  },
                  "references": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "entity_type": {
                          "type": "string",
                          "enum": [
                            "plant",
                            "explant",
                            "contamination",
                            "task",
                            "media_recipe",
                            "sop"
                          ],
                          "description": "The kind of record the comment links to."
                        },
                        "entity_id": {
                          "type": "string",
                          "format": "uuid",
                          "description": "The linked record's id."
                        },
                        "label": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 200,
                          "description": "The record's name as it follows `#type:` in the body, where spaces are written as hyphens; either spelling is accepted here. The link is drawn over the matching text."
                        }
                      },
                      "required": [
                        "entity_type",
                        "entity_id",
                        "label"
                      ]
                    },
                    "maxItems": 25,
                    "description": "Records in this workspace that the body links to."
                  }
                },
                "required": [
                  "entity_type",
                  "entity_id",
                  "body"
                ]
              },
              "example": {
                "entity_type": "explant",
                "entity_id": "6d2f9a14-8b3e-4c71-a5d0-1f7e3b8c2a96",
                "body": "@sam the Alocasia line on shelf 2 is ready to go onto fresh medium. See #task:Subculture-Alocasia-line.",
                "mentioned_user_ids": [
                  "4a8f1c63-2e7d-4b95-8c10-5d3e9f7a2b68"
                ],
                "references": [
                  {
                    "entity_type": "task",
                    "entity_id": "5b0f6f3e-2c1a-4d8e-9f47-0a6c3e1b7d22",
                    "label": "Subculture Alocasia line"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/Comment"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": {
                    "id": "9c4e2a71-3d5b-4f86-b1e0-7a2d6c8f3e45",
                    "entity_type": "explant",
                    "entity_id": "6d2f9a14-8b3e-4c71-a5d0-1f7e3b8c2a96",
                    "parent_id": null,
                    "body": "@sam the Alocasia line on shelf 2 is ready to go onto fresh medium. See #task:Subculture-Alocasia-line.",
                    "status": "active",
                    "is_pinned": false,
                    "author": {
                      "id": "0d7e4b9a-6f21-4c3e-8a5b-2e9f1c7d4a60",
                      "name": "Sam Okafor"
                    },
                    "mentioned_user_ids": [
                      "4a8f1c63-2e7d-4b95-8c10-5d3e9f7a2b68"
                    ],
                    "references": [
                      {
                        "entity_type": "task",
                        "entity_id": "5b0f6f3e-2c1a-4d8e-9f47-0a6c3e1b7d22",
                        "label": "Subculture Alocasia line"
                      }
                    ],
                    "created_at": "2026-09-25T09:15:40.000Z",
                    "updated_at": "2026-09-25T09:15:40.000Z",
                    "edited_at": null
                  }
                }
              }
            },
            "headers": {
              "Idempotent-Replay": {
                "$ref": "#/components/headers/IdempotentReplay"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "400": {
            "description": "`VALIDATION_ERROR` — The request body is not valid JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "The request body is not valid JSON."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.\n\n`FORBIDDEN` — 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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              },
              {
                "code": "FORBIDDEN",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "`NOT_FOUND` — `entity_id` names no record of that `entity_type` in the workspace, or `parent_id` names no comment on it; `error` says which.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "NOT_FOUND"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "`entity_id` names no record of that `entity_type` in the workspace, or `parent_id` names no comment on it; `error` says which."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "409": {
            "description": "`IDEMPOTENCY_IN_FLIGHT` — A request with this `Idempotency-Key` is still being processed; retry after `Retry-After` seconds.\n\n`CONFLICT` — The task's discussion is locked. Only the task's creator or a lab manager can comment.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "IDEMPOTENCY_IN_FLIGHT",
                            "CONFLICT"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "IDEMPOTENCY_IN_FLIGHT",
                "when": "A request with this `Idempotency-Key` is still being processed; retry after `Retry-After` seconds."
              },
              {
                "code": "CONFLICT",
                "when": "The task's discussion is locked. Only the task's creator or a lab manager can comment."
              }
            ],
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "422": {
            "description": "`VALIDATION_ERROR` — The `Idempotency-Key` header is malformed.\n\n`VALIDATION_ERROR` — A field failed validation. `error` names the first one, for example `title: title is required`.\n\n`VALIDATION_ERROR` — A mentioned user is not an active member of the workspace, or a linked record is not in it.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "The `Idempotency-Key` header is malformed."
              },
              {
                "code": "VALIDATION_ERROR",
                "when": "A field failed validation. `error` names the first one, for example `title: title is required`."
              },
              {
                "code": "VALIDATION_ERROR",
                "when": "A mentioned user is not an active member of the workspace, or a linked record is not in it."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`COMMENT_CREATE_FAILED` — The comment could not be saved. Retry with the same `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "COMMENT_CREATE_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "COMMENT_CREATE_FAILED",
                "when": "The comment could not be saved. Retry with the same `Idempotency-Key`."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      }
    },
    "/api/v1/commerce/order-lines": {
      "get": {
        "operationId": "listOrderLines",
        "tags": [
          "Sell-through"
        ],
        "summary": "List store order lines",
        "description": "Your store's sales, line by line, most recent first: which product sold, the culture line it is matched to, how many, at what unit price, and when. Filter with `from` and `to` for a period and `product_link_id` for one product.\n\nOrder lines carry no buyer details — no names, email or postal addresses — and leave out your store's order numbers. For totals, use the sell-through summary rather than adding these lines up: amounts are exact decimal text, and lines in different currencies must never be added together.\n\nNeeds culture line pricing, which includes sell-through, in the workspace's plan; without it the answer is `FEATURE_NOT_INCLUDED`.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "read:commerce"
        ],
        "x-idempotent": false,
        "x-cursor": true,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Only sales at or after this time, as an ISO 8601 timestamp."
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Only sales at or before this time, as an ISO 8601 timestamp."
          },
          {
            "name": "product_link_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Only lines for this store product, by the `product_link_id` an order line carries."
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/OrderLine"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/PageMeta"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": [
                    {
                      "id": "9b3d5f7a-1c2e-4a4b-8d6f-0e2a4c6e8b1d",
                      "product_link_id": "f2a4c6e8-0b1d-4e3f-a5b7-c9d1e3f5a7b9",
                      "plant_id": "6d1b3f8a-4c2e-4a97-b5d0-8e7f2a1c9b43",
                      "store_product_id": "prod-5521",
                      "store_variant_id": "var-5521-4in",
                      "quantity": 3,
                      "unit_price": {
                        "amount": "18.50",
                        "currency": "USD"
                      },
                      "occurred_at": "2026-09-21T15:02:44.000Z"
                    }
                  ],
                  "meta": {
                    "next_cursor": null
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.\n\n`FEATURE_NOT_INCLUDED` — Culture line pricing, which includes sell-through, is not included in your plan.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED",
                            "FEATURE_NOT_INCLUDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              },
              {
                "code": "FEATURE_NOT_INCLUDED",
                "when": "Culture line pricing, which includes sell-through, is not included in your plan."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "`NOT_FOUND` — `product_link_id` names no store product in the key's workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "NOT_FOUND"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "`product_link_id` names no store product in the key's workspace."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "422": {
            "description": "`VALIDATION_ERROR` — Both `cursor` and `offset` were sent; use one.\n\n`INVALID_CURSOR` — The cursor is malformed, or came from a different list or different filters. Start again without it.\n\n`VALIDATION_ERROR` — A query parameter is malformed, or `from` is later than `to`. `error` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR",
                            "INVALID_CURSOR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "Both `cursor` and `offset` were sent; use one."
              },
              {
                "code": "INVALID_CURSOR",
                "when": "The cursor is malformed, or came from a different list or different filters. Start again without it."
              },
              {
                "code": "VALIDATION_ERROR",
                "when": "A query parameter is malformed, or `from` is later than `to`. `error` names it."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`FEATURE_CHECK_FAILED` — Your plan could not be checked. Retry later.\n\n`SELL_THROUGH_EVENT_QUERY_FAILED` — The sales could not be read. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FEATURE_CHECK_FAILED",
                            "SELL_THROUGH_EVENT_QUERY_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FEATURE_CHECK_FAILED",
                "when": "Your plan could not be checked. Retry later."
              },
              {
                "code": "SELL_THROUGH_EVENT_QUERY_FAILED",
                "when": "The sales could not be read. Retry later."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      }
    },
    "/api/v1/commerce/sell-through": {
      "get": {
        "operationId": "getSellThrough",
        "tags": [
          "Sell-through"
        ],
        "summary": "Summarize sell-through",
        "description": "Units sold and revenue per culture line over a period, highest units first. Filter with `from` and `to` for the period and `plant_id` for one culture line.\n\nEach group is one culture line in one currency. There is no combined total across currencies: groups in different currencies are separate figures and must not be added together. Revenue is exact decimal text, totalled from the recorded sales without floating-point rounding.\n\nPages by `offset`. Needs culture line pricing, which includes sell-through, in the workspace's plan; without it the answer is `FEATURE_NOT_INCLUDED`.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "read:commerce"
        ],
        "x-idempotent": false,
        "x-cursor": false,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Only sales at or after this time, as an ISO 8601 timestamp."
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Only sales at or before this time, as an ISO 8601 timestamp."
          },
          {
            "name": "plant_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Only this culture line's sales. A culture line is identified by its plant id."
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SellThroughGroup"
                      }
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": [
                    {
                      "plant_id": "6d1b3f8a-4c2e-4a97-b5d0-8e7f2a1c9b43",
                      "currency": "USD",
                      "units": 42,
                      "revenue": {
                        "amount": "777.00",
                        "currency": "USD"
                      },
                      "order_line_count": 17,
                      "priced_line_count": 17,
                      "first_occurred_at": "2026-07-02T10:11:00.000Z",
                      "last_occurred_at": "2026-09-21T15:02:44.000Z"
                    },
                    {
                      "plant_id": "6d1b3f8a-4c2e-4a97-b5d0-8e7f2a1c9b43",
                      "currency": "EUR",
                      "units": 6,
                      "revenue": {
                        "amount": "96.00",
                        "currency": "EUR"
                      },
                      "order_line_count": 2,
                      "priced_line_count": 2,
                      "first_occurred_at": "2026-08-14T08:30:00.000Z",
                      "last_occurred_at": "2026-09-03T12:45:10.000Z"
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.\n\n`FEATURE_NOT_INCLUDED` — Culture line pricing, which includes sell-through, is not included in your plan.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED",
                            "FEATURE_NOT_INCLUDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              },
              {
                "code": "FEATURE_NOT_INCLUDED",
                "when": "Culture line pricing, which includes sell-through, is not included in your plan."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "`NOT_FOUND` — `plant_id` names no culture line in the key's workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "NOT_FOUND"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "`plant_id` names no culture line in the key's workspace."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "422": {
            "description": "`VALIDATION_ERROR` — A query parameter is malformed, or `from` is later than `to`. `error` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "A query parameter is malformed, or `from` is later than `to`. `error` names it."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`FEATURE_CHECK_FAILED` — Your plan could not be checked. Retry later.\n\n`SELL_THROUGH_EVENT_QUERY_FAILED` — The sales could not be read. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FEATURE_CHECK_FAILED",
                            "SELL_THROUGH_EVENT_QUERY_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FEATURE_CHECK_FAILED",
                "when": "Your plan could not be checked. Retry later."
              },
              {
                "code": "SELL_THROUGH_EVENT_QUERY_FAILED",
                "when": "The sales could not be read. Retry later."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      }
    },
    "/api/v1/contaminations": {
      "get": {
        "operationId": "listContaminations",
        "tags": [
          "Contamination"
        ],
        "summary": "List contaminations",
        "description": "The workspace's contamination logs, newest first. Narrow to one plant or explant, to one status, or to what was logged since a point in time. Withdrawn logs are never listed.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "read:contaminations"
        ],
        "x-idempotent": false,
        "x-cursor": true,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "name": "plant_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Only contaminations linked to this plant."
          },
          {
            "name": "explant_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Only contaminations linked to this explant."
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "resolved",
                "quarantined",
                "archived",
                "under investigation"
              ]
            },
            "description": "Only contaminations in this state."
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Only contaminations logged at or after this ISO 8601 timestamp. Keep the newest `created_at` you have received and send it back to fetch only what is new."
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Contamination"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/PageMeta"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": [
                    {
                      "id": "8a3c1e57-4b2d-4f60-9c81-2d7e5b9a0f14",
                      "workspace_id": "3f9d2b61-7c4e-4a85-b0d3-6e1f8a2c5b97",
                      "type": "bacteria",
                      "type_other": null,
                      "issue": "Cloudy halo around the base of the Phalaenopsis explant",
                      "description": null,
                      "notes": "Noticed at the weekly check on shelf 3.",
                      "severity": "medium",
                      "status": "active",
                      "observed_at": "2026-09-24T08:30:00.000Z",
                      "resolved_at": null,
                      "vessels_affected": 2,
                      "plants_affected": null,
                      "affected_vessel_markings": "P-07 / P-09",
                      "custom_fields": {
                        "hood": "Hood 2"
                      },
                      "plant_ids": [],
                      "explant_ids": [
                        "6d2f9a14-8b3e-4c71-a5d0-1f7e3b8c2a96"
                      ],
                      "logged_by": "0d7e4b9a-6f21-4c3e-8a5b-2e9f1c7d4a60",
                      "created_at": "2026-09-24T08:41:12.000Z",
                      "updated_at": "2026-09-24T08:41:12.000Z"
                    }
                  ],
                  "meta": {
                    "next_cursor": null
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "`NOT_FOUND` — `plant_id` or `explant_id` names no plant or explant in the key's workspace; `error` says which.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "NOT_FOUND"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "`plant_id` or `explant_id` names no plant or explant in the key's workspace; `error` says which."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "422": {
            "description": "`VALIDATION_ERROR` — Both `cursor` and `offset` were sent; use one.\n\n`INVALID_CURSOR` — The cursor is malformed, or came from a different list or different filters. Start again without it.\n\n`VALIDATION_ERROR` — A filter is malformed; `error` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR",
                            "INVALID_CURSOR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "Both `cursor` and `offset` were sent; use one."
              },
              {
                "code": "INVALID_CURSOR",
                "when": "The cursor is malformed, or came from a different list or different filters. Start again without it."
              },
              {
                "code": "VALIDATION_ERROR",
                "when": "A filter is malformed; `error` names it."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`CONTAMINATION_QUERY_FAILED` — The logs could not be read. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "CONTAMINATION_QUERY_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "CONTAMINATION_QUERY_FAILED",
                "when": "The logs could not be read. Retry later."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createContamination",
        "tags": [
          "Contamination"
        ],
        "summary": "Log a contamination",
        "description": "Records a contamination seen on one plant or explant, as the key's owner. It is the same record the app makes: it appears in the plant's or explant's history, and for an explant the stage and room the culture was in when it was seen are recorded with it, which is what lets the app show that room's conditions over the week before.\n\nSend `observed_at` when logging something seen earlier; it defaults to now. Fill in the fields your lab has added to contaminations with `custom_fields`; they are checked against your lab's field list exactly as the app's form checks them.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "write:contaminations"
        ],
        "x-idempotent": true,
        "x-cursor": false,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "plant_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "The plant the contamination was seen on. Send exactly one of `plant_id` or `explant_id`."
                  },
                  "explant_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "The explant the contamination was seen on. Send exactly one of `plant_id` or `explant_id`."
                  },
                  "type": {
                    "type": "string",
                    "enum": [
                      "mold",
                      "bacteria",
                      "hyperhydricity",
                      "phenolic",
                      "algae",
                      "yeast",
                      "endophytic",
                      "viral",
                      "fungal",
                      "physiological",
                      "contaminated_media",
                      "damage",
                      "insect",
                      "other"
                    ],
                    "description": "What was seen. Use `other` and describe it in `type_other` when nothing in the list fits."
                  },
                  "type_other": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200,
                    "description": "What the contamination is, in your own words. Required when `type` is `other`, ignored otherwise."
                  },
                  "issue": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 300,
                    "description": "A short summary of what was seen, as it should read in a list."
                  },
                  "description": {
                    "type": "string",
                    "maxLength": 5000
                  },
                  "notes": {
                    "type": "string",
                    "maxLength": 5000
                  },
                  "severity": {
                    "type": "string",
                    "enum": [
                      "very low",
                      "low",
                      "medium",
                      "high",
                      "critical"
                    ],
                    "default": "low"
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "active",
                      "resolved",
                      "quarantined",
                      "archived",
                      "under investigation"
                    ],
                    "default": "active",
                    "description": "Where the contamination stands. A new observation is `active`; send another state only when recording one that has already been dealt with."
                  },
                  "suspected_source": {
                    "type": "string",
                    "enum": [
                      "airborne",
                      "cross",
                      "media",
                      "observed",
                      "tool",
                      "transferred",
                      "unknown"
                    ],
                    "description": "Where you think it came from, if you have a view. Leave it out rather than guess."
                  },
                  "observed_at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "When it was seen, as an ISO 8601 timestamp. Defaults to now. For an explant, the stage and room it was in at that moment are recorded with the observation."
                  },
                  "vessels_affected": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 1000000,
                    "description": "How many vessels it reached. Leave it out if nobody counted: `0` means counted and none were affected."
                  },
                  "plants_affected": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 1000000,
                    "description": "How many plants it reached. Leave it out if nobody counted: `0` means counted and none were affected."
                  },
                  "affected_vessel_markings": {
                    "type": "string",
                    "maxLength": 500,
                    "description": "Which vessels, as written on them — for example `B-12 / B-14`."
                  },
                  "custom_fields": {
                    "type": [
                      "object",
                      "null"
                    ],
                    "additionalProperties": {
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "number"
                        },
                        {
                          "type": "boolean"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "description": "Values for the fields your lab has added to contaminations, keyed by field key — for example `{ \"hood\": \"Hood 2\" }`. Each value must suit its field: text, a number, `true` or `false`, a date (stored as `YYYY-MM-DD`), or one of a list field's options. `null` leaves a field empty. A key your lab has not defined is refused."
                  }
                },
                "required": [
                  "type",
                  "issue"
                ]
              },
              "example": {
                "explant_id": "6d2f9a14-8b3e-4c71-a5d0-1f7e3b8c2a96",
                "type": "bacteria",
                "issue": "Cloudy halo around the base of the Phalaenopsis explant",
                "notes": "Noticed at the weekly check on shelf 3.",
                "severity": "medium",
                "observed_at": "2026-09-24T08:30:00Z",
                "vessels_affected": 2,
                "affected_vessel_markings": "P-07 / P-09",
                "custom_fields": {
                  "hood": "Hood 2"
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/Contamination"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": {
                    "id": "8a3c1e57-4b2d-4f60-9c81-2d7e5b9a0f14",
                    "workspace_id": "3f9d2b61-7c4e-4a85-b0d3-6e1f8a2c5b97",
                    "type": "bacteria",
                    "type_other": null,
                    "issue": "Cloudy halo around the base of the Phalaenopsis explant",
                    "description": null,
                    "notes": "Noticed at the weekly check on shelf 3.",
                    "severity": "medium",
                    "status": "active",
                    "observed_at": "2026-09-24T08:30:00.000Z",
                    "resolved_at": null,
                    "vessels_affected": 2,
                    "plants_affected": null,
                    "affected_vessel_markings": "P-07 / P-09",
                    "custom_fields": {
                      "hood": "Hood 2"
                    },
                    "plant_ids": [],
                    "explant_ids": [
                      "6d2f9a14-8b3e-4c71-a5d0-1f7e3b8c2a96"
                    ],
                    "logged_by": "0d7e4b9a-6f21-4c3e-8a5b-2e9f1c7d4a60",
                    "created_at": "2026-09-24T08:41:12.000Z",
                    "updated_at": "2026-09-24T08:41:12.000Z"
                  }
                }
              }
            },
            "headers": {
              "Idempotent-Replay": {
                "$ref": "#/components/headers/IdempotentReplay"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "400": {
            "description": "`VALIDATION_ERROR` — The request body is not valid JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "The request body is not valid JSON."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.\n\n`FORBIDDEN` — 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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              },
              {
                "code": "FORBIDDEN",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "`NOT_FOUND` — `plant_id` or `explant_id` names no plant or explant in the key's workspace; `error` says which.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "NOT_FOUND"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "`plant_id` or `explant_id` names no plant or explant in the key's workspace; `error` says which."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "409": {
            "description": "`IDEMPOTENCY_IN_FLIGHT` — A request with this `Idempotency-Key` is still being processed; retry after `Retry-After` seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "IDEMPOTENCY_IN_FLIGHT"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "IDEMPOTENCY_IN_FLIGHT",
                "when": "A request with this `Idempotency-Key` is still being processed; retry after `Retry-After` seconds."
              }
            ],
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "422": {
            "description": "`VALIDATION_ERROR` — The `Idempotency-Key` header is malformed.\n\n`VALIDATION_ERROR` — A field failed validation. `error` names the first one, for example `title: title is required`.\n\n`VALIDATION_ERROR` — `custom_fields` names a field your lab has not defined, or gives a field a value of the wrong kind. `error` names the field.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "The `Idempotency-Key` header is malformed."
              },
              {
                "code": "VALIDATION_ERROR",
                "when": "A field failed validation. `error` names the first one, for example `title: title is required`."
              },
              {
                "code": "VALIDATION_ERROR",
                "when": "`custom_fields` names a field your lab has not defined, or gives a field a value of the wrong kind. `error` names the field."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`CONTAMINATION_CREATE_FAILED` — The log could not be saved, and nothing was kept. Retry with the same `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "CONTAMINATION_CREATE_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "CONTAMINATION_CREATE_FAILED",
                "when": "The log could not be saved, and nothing was kept. Retry with the same `Idempotency-Key`."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      }
    },
    "/api/v1/contaminations/{id}": {
      "get": {
        "operationId": "getContamination",
        "tags": [
          "Contamination"
        ],
        "summary": "Get a contamination",
        "description": "One contamination log, with the plants and explants it is linked to.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "read:contaminations"
        ],
        "x-idempotent": false,
        "x-cursor": false,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The record's id."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/Contamination"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": {
                    "id": "8a3c1e57-4b2d-4f60-9c81-2d7e5b9a0f14",
                    "workspace_id": "3f9d2b61-7c4e-4a85-b0d3-6e1f8a2c5b97",
                    "type": "bacteria",
                    "type_other": null,
                    "issue": "Cloudy halo around the base of the Phalaenopsis explant",
                    "description": null,
                    "notes": "Noticed at the weekly check on shelf 3.",
                    "severity": "medium",
                    "status": "active",
                    "observed_at": "2026-09-24T08:30:00.000Z",
                    "resolved_at": null,
                    "vessels_affected": 2,
                    "plants_affected": null,
                    "affected_vessel_markings": "P-07 / P-09",
                    "custom_fields": {
                      "hood": "Hood 2"
                    },
                    "plant_ids": [],
                    "explant_ids": [
                      "6d2f9a14-8b3e-4c71-a5d0-1f7e3b8c2a96"
                    ],
                    "logged_by": "0d7e4b9a-6f21-4c3e-8a5b-2e9f1c7d4a60",
                    "created_at": "2026-09-24T08:41:12.000Z",
                    "updated_at": "2026-09-24T08:41:12.000Z"
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "`NOT_FOUND` — No record with this id exists in the key's workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "NOT_FOUND"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "No record with this id exists in the key's workspace."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`CONTAMINATION_QUERY_FAILED` — The log could not be read. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "CONTAMINATION_QUERY_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "CONTAMINATION_QUERY_FAILED",
                "when": "The log could not be read. Retry later."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      }
    },
    "/api/v1/device-events": {
      "post": {
        "operationId": "createDeviceEvent",
        "tags": [
          "Devices"
        ],
        "summary": "Record a device event",
        "description": "Records something that happened on a device — an alert, an error, a firmware update or a configuration change — with whatever detail you put in `payload`. Send it with the device's own token, which may only write about that device, or with a workspace key.\n\nEvents are not deduplicated: a request retried after a dropped response is recorded twice.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "deviceToken": []
          }
        ],
        "x-required-scopes": [
          "write:device_events"
        ],
        "x-idempotent": false,
        "x-cursor": false,
        "x-device-token": true,
        "x-documented": true,
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "device_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "The device the event is about."
                  },
                  "event_type": {
                    "type": "string",
                    "enum": [
                      "heartbeat",
                      "alert",
                      "firmware_update",
                      "config_change",
                      "error",
                      "other"
                    ],
                    "description": "What kind of event this is."
                  },
                  "payload": {
                    "type": "object",
                    "additionalProperties": {
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "number"
                        },
                        {
                          "type": "boolean"
                        },
                        {
                          "type": "null"
                        },
                        {
                          "type": "array",
                          "items": {}
                        },
                        {
                          "type": "object",
                          "additionalProperties": {}
                        }
                      ]
                    },
                    "default": {},
                    "description": "Any JSON object describing the event."
                  },
                  "occurred_at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "When it happened, as an ISO 8601 timestamp in UTC ending in `Z`. Defaults to the time the request arrives."
                  }
                },
                "required": [
                  "device_id",
                  "event_type"
                ]
              },
              "example": {
                "device_id": "8e3b1f52-6c0d-4a7e-9b21-5f4d8c2a7e13",
                "event_type": "alert",
                "payload": {
                  "reason": "humidity_high",
                  "humidity_percent": 94
                },
                "occurred_at": "2026-09-25T13:58:00.000Z"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/DeviceEvent"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": {
                    "id": "c7d2a9e4-1b6f-4c3a-8e5d-0f9b2a7c4e61",
                    "device_id": "8e3b1f52-6c0d-4a7e-9b21-5f4d8c2a7e13",
                    "team_id": "5d2e8b17-9c4a-4e3f-b6d0-1a7c9e2f4b83",
                    "event_type": "alert",
                    "payload": {
                      "reason": "humidity_high",
                      "humidity_percent": 94
                    },
                    "occurred_at": "2026-09-25T13:58:00.000Z",
                    "created_at": "2026-09-25T13:58:01.000Z"
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "400": {
            "description": "`VALIDATION_ERROR` — The request body is not valid JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "The request body is not valid JSON."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`PAID_PLAN_REQUIRED` — The workspace has no paid plan. Connecting devices is included with every paid plan.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "The workspace has no paid plan. Connecting devices is included with every paid plan."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_WRONG_DEVICE` — A device token was used to write about a device other than its own.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_WRONG_DEVICE"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_WRONG_DEVICE",
                "when": "A device token was used to write about a device other than its own."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "`NOT_FOUND` — `device_id` does not name a device in the workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "NOT_FOUND"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "`device_id` does not name a device in the workspace."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "422": {
            "description": "`VALIDATION_ERROR` — A field failed validation. `error` names the first one, for example `event_type: Invalid enum value`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "A field failed validation. `error` names the first one, for example `event_type: Invalid enum value`."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`DEVICE_EVENT_CREATE_FAILED` — The event could not be saved. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "DEVICE_EVENT_CREATE_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "DEVICE_EVENT_CREATE_FAILED",
                "when": "The event could not be saved. Retry later."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      }
    },
    "/api/v1/devices": {
      "get": {
        "operationId": "listDevices",
        "tags": [
          "Devices"
        ],
        "summary": "List devices",
        "description": "Every device registered to the workspace, newest first, a page at a time. Pages hold up to 200 devices by default, so most workspaces receive every device in the first answer; follow `meta.next_cursor` until it comes back `null` to be sure you have them all.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "read:devices"
        ],
        "x-idempotent": false,
        "x-cursor": true,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size. Values above 200 are capped at 200.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 200
            }
          },
          {
            "$ref": "#/components/parameters/Offset"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Device"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/PageMeta"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": [
                    {
                      "id": "8e3b1f52-6c0d-4a7e-9b21-5f4d8c2a7e13",
                      "name": "Shelf 3 climate probe",
                      "type": "sensor",
                      "hardware": "Raspberry Pi 4",
                      "status": "active",
                      "firmware_version": "1.4.2",
                      "room_id": "2c6f9a41-7d3e-4b8a-a1c5-9e0d4f7b3a26",
                      "last_seen_at": "2026-09-25T14:05:00.000Z",
                      "metadata": {
                        "shelf": 3
                      },
                      "created_at": "2026-09-01T09:30:00.000Z",
                      "updated_at": "2026-09-25T14:05:00.000Z"
                    }
                  ],
                  "meta": {
                    "next_cursor": null
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`PAID_PLAN_REQUIRED` — The workspace has no paid plan. Connecting devices is included with every paid plan.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "The workspace has no paid plan. Connecting devices is included with every paid plan."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "422": {
            "description": "`VALIDATION_ERROR` — Both `cursor` and `offset` were sent; use one.\n\n`INVALID_CURSOR` — The cursor is malformed, or came from a different list or different filters. Start again without it.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR",
                            "INVALID_CURSOR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "Both `cursor` and `offset` were sent; use one."
              },
              {
                "code": "INVALID_CURSOR",
                "when": "The cursor is malformed, or came from a different list or different filters. Start again without it."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`DEVICE_QUERY_FAILED` — The devices could not be read. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "DEVICE_QUERY_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "DEVICE_QUERY_FAILED",
                "when": "The devices could not be read. Retry later."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createDevice",
        "tags": [
          "Devices"
        ],
        "summary": "Register a device",
        "description": "Adds a sensor, controller or gateway to the workspace so it can send heartbeats, readings and events. Then mint a device token for it and put that token on the device, rather than a workspace key.\n\nIf the workspace has already connected every device its plan includes, or its plan includes none, the request is refused with `DEVICE_LIMIT_REACHED`. A retired device does not count. Send an `Idempotency-Key` so a retried request cannot register the same device twice.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "write:devices"
        ],
        "x-idempotent": true,
        "x-cursor": false,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 100,
                    "description": "What the lab calls the device, as it appears in the device list."
                  },
                  "type": {
                    "type": "string",
                    "enum": [
                      "sensor",
                      "controller",
                      "gateway"
                    ],
                    "default": "sensor",
                    "description": "What kind of device this is."
                  },
                  "hardware": {
                    "type": "string",
                    "maxLength": 100,
                    "description": "The board or model, for example `Raspberry Pi 4`."
                  },
                  "firmware_version": {
                    "type": "string",
                    "maxLength": 50,
                    "description": "The firmware or software version the device runs."
                  },
                  "room_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "The growing room the device sits in. Must be a room in the workspace."
                  },
                  "metadata": {
                    "type": "object",
                    "additionalProperties": {
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "number"
                        },
                        {
                          "type": "boolean"
                        },
                        {
                          "type": "null"
                        },
                        {
                          "type": "array",
                          "items": {}
                        },
                        {
                          "type": "object",
                          "additionalProperties": {}
                        }
                      ]
                    },
                    "default": {},
                    "description": "Any JSON object you want kept with the device."
                  }
                },
                "required": [
                  "name"
                ]
              },
              "example": {
                "name": "Shelf 3 climate probe",
                "type": "sensor",
                "hardware": "Raspberry Pi 4",
                "firmware_version": "1.4.2",
                "room_id": "2c6f9a41-7d3e-4b8a-a1c5-9e0d4f7b3a26",
                "metadata": {
                  "shelf": 3
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/Device"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": {
                    "id": "8e3b1f52-6c0d-4a7e-9b21-5f4d8c2a7e13",
                    "name": "Shelf 3 climate probe",
                    "type": "sensor",
                    "hardware": "Raspberry Pi 4",
                    "status": "active",
                    "firmware_version": "1.4.2",
                    "room_id": "2c6f9a41-7d3e-4b8a-a1c5-9e0d4f7b3a26",
                    "last_seen_at": null,
                    "metadata": {
                      "shelf": 3
                    },
                    "created_at": "2026-09-01T09:30:00.000Z",
                    "updated_at": "2026-09-01T09:30:00.000Z"
                  }
                }
              }
            },
            "headers": {
              "Idempotent-Replay": {
                "$ref": "#/components/headers/IdempotentReplay"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "400": {
            "description": "`VALIDATION_ERROR` — The request body is not valid JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "The request body is not valid JSON."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`PAID_PLAN_REQUIRED` — The workspace has no paid plan. Connecting devices is included with every paid plan.\n\n`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED",
                            "DEVICE_LIMIT_REACHED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "The workspace has no paid plan. Connecting devices is included with every paid plan."
              },
              {
                "code": "DEVICE_LIMIT_REACHED",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "`NOT_FOUND` — `room_id` does not name a growing room in the workspace. Nothing was registered.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "NOT_FOUND"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "`room_id` does not name a growing room in the workspace. Nothing was registered."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "409": {
            "description": "`IDEMPOTENCY_IN_FLIGHT` — A request with this `Idempotency-Key` is still being processed; retry after `Retry-After` seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "IDEMPOTENCY_IN_FLIGHT"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "IDEMPOTENCY_IN_FLIGHT",
                "when": "A request with this `Idempotency-Key` is still being processed; retry after `Retry-After` seconds."
              }
            ],
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "422": {
            "description": "`VALIDATION_ERROR` — The `Idempotency-Key` header is malformed.\n\n`VALIDATION_ERROR` — A field failed validation. `error` names the first one, for example `name: String must contain at least 1 character(s)`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "The `Idempotency-Key` header is malformed."
              },
              {
                "code": "VALIDATION_ERROR",
                "when": "A field failed validation. `error` names the first one, for example `name: String must contain at least 1 character(s)`."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`DEVICE_CREATE_FAILED` — The device could not be saved. Retry with the same `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "DEVICE_CREATE_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "DEVICE_CREATE_FAILED",
                "when": "The device could not be saved. Retry with the same `Idempotency-Key`."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "503": {
            "description": "`DEVICE_LIMIT_UNAVAILABLE` — The workspace's device allowance could not be checked, so nothing was registered. Retry shortly.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "DEVICE_LIMIT_UNAVAILABLE"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "DEVICE_LIMIT_UNAVAILABLE",
                "when": "The workspace's device allowance could not be checked, so nothing was registered. Retry shortly."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      }
    },
    "/api/v1/devices/{deviceId}/heartbeat": {
      "post": {
        "operationId": "sendHeartbeat",
        "tags": [
          "Devices"
        ],
        "summary": "Send a heartbeat",
        "description": "Tells xPlant the device is alive and stamps its last-seen time. xPlant shows a device as offline once 15 minutes pass without hearing from it, so send a heartbeat every minute or two. The request has no body.\n\nA repeated heartbeat does no harm, so a retry needs no `Idempotency-Key`. A device token may send heartbeats only for its own device.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "deviceToken": []
          }
        ],
        "x-required-scopes": [
          "write:devices"
        ],
        "x-idempotent": false,
        "x-cursor": false,
        "x-device-token": true,
        "x-documented": true,
        "parameters": [
          {
            "name": "deviceId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The device's id."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/Heartbeat"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": {
                    "received_at": "2026-09-25T14:05:00.000Z"
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`PAID_PLAN_REQUIRED` — The workspace has no paid plan. Connecting devices is included with every paid plan.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "The workspace has no paid plan. Connecting devices is included with every paid plan."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_WRONG_DEVICE` — A device token was used to write about a device other than its own.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_WRONG_DEVICE"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_WRONG_DEVICE",
                "when": "A device token was used to write about a device other than its own."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "`NOT_FOUND` — No device with this id is registered in the key's workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "NOT_FOUND"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "No device with this id is registered in the key's workspace."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`DEVICE_UPDATE_FAILED` — The heartbeat could not be recorded. Retry, or send the next one on schedule.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "DEVICE_UPDATE_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "DEVICE_UPDATE_FAILED",
                "when": "The heartbeat could not be recorded. Retry, or send the next one on schedule."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      }
    },
    "/api/v1/devices/{deviceId}/tokens": {
      "get": {
        "operationId": "listDeviceTokens",
        "tags": [
          "Devices"
        ],
        "summary": "List a device's tokens",
        "description": "Every token minted for the device, active and revoked, newest first. Each carries its prefix, name, status and last use — what you need to tell tokens apart and decide which to revoke. The secret itself is never returned after the token is minted.\n\nPages hold up to 200 tokens by default; follow `meta.next_cursor` until it comes back `null` to be sure you have them all.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "read:devices"
        ],
        "x-idempotent": false,
        "x-cursor": true,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "name": "deviceId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The device's id."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size. Values above 200 are capped at 200.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 200
            }
          },
          {
            "$ref": "#/components/parameters/Offset"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/DeviceToken"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/PageMeta"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": [
                    {
                      "id": "4f8a2c6e-9d1b-4e7a-b3c5-6a0d8f2e4b19",
                      "deviceId": "8e3b1f52-6c0d-4a7e-9b21-5f4d8c2a7e13",
                      "name": "Grow room Pi",
                      "prefix": "xpd_live_0123456789a",
                      "status": "active",
                      "lastUsedAt": "2026-09-25T14:05:00.000Z",
                      "revokedAt": null,
                      "createdAt": "2026-09-01T09:40:00.000Z"
                    }
                  ],
                  "meta": {
                    "next_cursor": null
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`PAID_PLAN_REQUIRED` — The workspace has no paid plan. Connecting devices is included with every paid plan.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "The workspace has no paid plan. Connecting devices is included with every paid plan."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "`NOT_FOUND` — No device with this id is registered in the key's workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "NOT_FOUND"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "No device with this id is registered in the key's workspace."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "422": {
            "description": "`VALIDATION_ERROR` — Both `cursor` and `offset` were sent; use one.\n\n`INVALID_CURSOR` — The cursor is malformed, or came from a different list or different filters. Start again without it.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR",
                            "INVALID_CURSOR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "Both `cursor` and `offset` were sent; use one."
              },
              {
                "code": "INVALID_CURSOR",
                "when": "The cursor is malformed, or came from a different list or different filters. Start again without it."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`DEVICE_TOKEN_QUERY_FAILED` — The tokens could not be read. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "DEVICE_TOKEN_QUERY_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "DEVICE_TOKEN_QUERY_FAILED",
                "when": "The tokens could not be read. Retry later."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createDeviceToken",
        "tags": [
          "Devices"
        ],
        "summary": "Mint a device token",
        "description": "Creates a credential for one device. Put it on the device in place of a workspace key: it can send that device's heartbeats, readings and events, and nothing else.\n\n**The secret is in `token`, and this is the only response that contains it.** Store it on the device straight away. If it is lost, mint another and revoke the old one.\n\nMint tokens with a workspace key — a device token cannot create tokens. A retried request mints a second token rather than returning the first, so revoke whichever one you do not keep. Every field is optional; send `{}` or no body at all.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "write:devices"
        ],
        "x-idempotent": false,
        "x-cursor": false,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "name": "deviceId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The device's id."
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 120,
                    "description": "A label to tell the device's tokens apart, for example `Grow room Pi`."
                  },
                  "environment": {
                    "type": "string",
                    "enum": [
                      "production",
                      "development"
                    ],
                    "default": "production",
                    "description": "`production` mints an `xpd_live_` token and `development` an `xpd_dev_` one. Both act on the same workspace."
                  }
                }
              },
              "example": {
                "name": "Grow room Pi"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/DeviceTokenCreated"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": {
                    "id": "4f8a2c6e-9d1b-4e7a-b3c5-6a0d8f2e4b19",
                    "deviceId": "8e3b1f52-6c0d-4a7e-9b21-5f4d8c2a7e13",
                    "name": "Grow room Pi",
                    "prefix": "xpd_live_0123456789a",
                    "token": "xpd_live_0123456789abcdef0123456789abcdef0123456789abcdef",
                    "createdAt": "2026-09-01T09:40:00.000Z"
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`PAID_PLAN_REQUIRED` — The workspace has no paid plan. Connecting devices is included with every paid plan.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "The workspace has no paid plan. Connecting devices is included with every paid plan."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "`NOT_FOUND` — No device with this id is registered in the key's workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "NOT_FOUND"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "No device with this id is registered in the key's workspace."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "422": {
            "description": "`VALIDATION_ERROR` — A field failed validation, such as a `name` longer than 120 characters. `error` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "A field failed validation, such as a `name` longer than 120 characters. `error` names it."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`DEVICE_TOKEN_CREATE_FAILED` — The token could not be created, and none was minted. Retry.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "DEVICE_TOKEN_CREATE_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "DEVICE_TOKEN_CREATE_FAILED",
                "when": "The token could not be created, and none was minted. Retry."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      }
    },
    "/api/v1/devices/{deviceId}/tokens/{tokenId}": {
      "delete": {
        "operationId": "revokeDeviceToken",
        "tags": [
          "Devices"
        ],
        "summary": "Revoke a device token",
        "description": "Stops one device token working: the next request made with it is refused as unauthorized. The token stays in the device's token list, marked `revoked` with the time it was revoked, so the list remains a complete history of the device's credentials.\n\nRevoking a token that is already revoked is not an error — you get the token back as it stands, with its original revocation time. Taking a device out of service revokes all of its tokens at once.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "write:devices"
        ],
        "x-idempotent": false,
        "x-cursor": false,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "name": "deviceId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The device's id."
          },
          {
            "name": "tokenId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The token's id, as the token list returns it."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/DeviceToken"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": {
                    "id": "4f8a2c6e-9d1b-4e7a-b3c5-6a0d8f2e4b19",
                    "deviceId": "8e3b1f52-6c0d-4a7e-9b21-5f4d8c2a7e13",
                    "name": "Grow room Pi",
                    "prefix": "xpd_live_0123456789a",
                    "status": "revoked",
                    "lastUsedAt": "2026-09-25T14:05:00.000Z",
                    "revokedAt": "2026-09-25T15:00:00.000Z",
                    "createdAt": "2026-09-01T09:40:00.000Z"
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`PAID_PLAN_REQUIRED` — The workspace has no paid plan. Connecting devices is included with every paid plan.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "The workspace has no paid plan. Connecting devices is included with every paid plan."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "`NOT_FOUND` — No token with this id belongs to this device in the key's workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "NOT_FOUND"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "No token with this id belongs to this device in the key's workspace."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`DEVICE_TOKEN_REVOKE_FAILED` — The token could not be revoked and may still work. Retry.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "DEVICE_TOKEN_REVOKE_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "DEVICE_TOKEN_REVOKE_FAILED",
                "when": "The token could not be revoked and may still work. Retry."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      }
    },
    "/api/v1/equipment": {
      "get": {
        "operationId": "listEquipment",
        "tags": [
          "Equipment"
        ],
        "summary": "List equipment",
        "description": "The workspace's equipment library, newest first, with each item's calibration and maintenance due dates. Filter by `category` and `status`; with neither you get every item, archived ones included.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "read:equipment"
        ],
        "x-idempotent": false,
        "x-cursor": true,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "name": "category",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "balance_scale",
                "autoclave_pressure_cooker",
                "laminar_flow_hood",
                "still_air_box",
                "incubator",
                "light_rack",
                "fridge_freezer",
                "ph_ec_meter",
                "microscope",
                "label_printer",
                "other"
              ]
            },
            "description": "Only equipment of this type: `balance_scale`, `autoclave_pressure_cooker`, `laminar_flow_hood`, `still_air_box`, `incubator`, `light_rack`, `fridge_freezer`, `ph_ec_meter`, `microscope`, `label_printer`, `other`."
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "archived"
              ]
            },
            "description": "`active`, or `archived` for equipment the lab has retired. Leave it out to get both."
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Equipment"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/PageMeta"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": [
                    {
                      "id": "8c2f4e61-5a3b-4d7e-9b10-3e6a2c9d4f18",
                      "name": "Autoclave 2",
                      "category": "autoclave_pressure_cooker",
                      "status": "active",
                      "manufacturer": "Example Instruments",
                      "model": "AC-40",
                      "serial_number": "AC40-00172",
                      "location": "Media room",
                      "purchase_date": "2025-03-14",
                      "vendor_url": null,
                      "notes": null,
                      "last_calibrated_at": "2026-06-02T08:30:00.000Z",
                      "next_calibration_due_at": "2026-12-02",
                      "last_maintenance_at": null,
                      "next_maintenance_due_at": null,
                      "created_at": "2025-03-20T10:04:11.000Z",
                      "updated_at": "2026-06-02T08:31:40.000Z"
                    }
                  ],
                  "meta": {
                    "next_cursor": null
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "422": {
            "description": "`VALIDATION_ERROR` — Both `cursor` and `offset` were sent; use one.\n\n`INVALID_CURSOR` — The cursor is malformed, or came from a different list or different filters. Start again without it.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR",
                            "INVALID_CURSOR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "Both `cursor` and `offset` were sent; use one."
              },
              {
                "code": "INVALID_CURSOR",
                "when": "The cursor is malformed, or came from a different list or different filters. Start again without it."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`LAB_EQUIPMENT_QUERY_FAILED` — The equipment could not be read. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "LAB_EQUIPMENT_QUERY_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "LAB_EQUIPMENT_QUERY_FAILED",
                "when": "The equipment could not be read. Retry later."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      }
    },
    "/api/v1/equipment/{id}": {
      "get": {
        "operationId": "getEquipment",
        "tags": [
          "Equipment"
        ],
        "summary": "Get equipment",
        "description": "One piece of equipment, with its calibration and maintenance due dates.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "read:equipment"
        ],
        "x-idempotent": false,
        "x-cursor": false,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The record's id."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/Equipment"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": {
                    "id": "8c2f4e61-5a3b-4d7e-9b10-3e6a2c9d4f18",
                    "name": "Autoclave 2",
                    "category": "autoclave_pressure_cooker",
                    "status": "active",
                    "manufacturer": "Example Instruments",
                    "model": "AC-40",
                    "serial_number": "AC40-00172",
                    "location": "Media room",
                    "purchase_date": "2025-03-14",
                    "vendor_url": null,
                    "notes": null,
                    "last_calibrated_at": "2026-06-02T08:30:00.000Z",
                    "next_calibration_due_at": "2026-12-02",
                    "last_maintenance_at": null,
                    "next_maintenance_due_at": null,
                    "created_at": "2025-03-20T10:04:11.000Z",
                    "updated_at": "2026-06-02T08:31:40.000Z"
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "`NOT_FOUND` — No record with this id exists in the key's workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "NOT_FOUND"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "No record with this id exists in the key's workspace."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`LAB_EQUIPMENT_QUERY_FAILED` — The equipment could not be read. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "LAB_EQUIPMENT_QUERY_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "LAB_EQUIPMENT_QUERY_FAILED",
                "when": "The equipment could not be read. Retry later."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      }
    },
    "/api/v1/equipment/{id}/events": {
      "get": {
        "operationId": "listEquipmentEvents",
        "tags": [
          "Equipment"
        ],
        "summary": "List equipment events",
        "description": "A piece of equipment's history, newest first: what it was used on, and its calibration and preventive maintenance records. Each event has the fields of its kind; the others are null.\n\nWithout `kind` you get every kind in one list, which pages by `cursor` only. Send `kind` to read one kind, which pages by `cursor` or `offset`.\n\nThis history is part of equipment calibration, which not every plan includes.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "read:equipment"
        ],
        "x-idempotent": false,
        "x-cursor": true,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The record's id."
          },
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "used",
                "calibration",
                "preventive_maintenance"
              ]
            },
            "description": "Only this kind of event: `used`, `calibration` or `preventive_maintenance`. Without it you get every kind, newest first."
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Only events at or after this time, as an ISO 8601 timestamp."
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Only events at or before this time, as an ISO 8601 timestamp."
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/EquipmentEvent"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/PageMeta"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": [
                    {
                      "id": "3f7a9c20-8e41-4b5d-a6c2-1d9e0b7f5a34",
                      "equipment_id": "8c2f4e61-5a3b-4d7e-9b10-3e6a2c9d4f18",
                      "kind": "used",
                      "occurred_at": "2026-09-24T07:15:00.000Z",
                      "outcome": null,
                      "subject_type": "media_batch",
                      "subject_id": "b1e6d8a4-2c7f-4e93-8a05-6f4c3d2b1a90",
                      "subject_label": "MS half strength, batch 14",
                      "performed_by_name": null,
                      "provider": null,
                      "as_found_condition": null,
                      "as_left_condition": null,
                      "result_summary": null,
                      "certificate_number": null,
                      "certificate_url": null,
                      "next_due_at": null,
                      "notes": "121 °C for 20 minutes"
                    },
                    {
                      "id": "c5d2e8f1-7a3b-4c69-9e14-0b8a6d4f2e57",
                      "equipment_id": "8c2f4e61-5a3b-4d7e-9b10-3e6a2c9d4f18",
                      "kind": "calibration",
                      "occurred_at": "2026-06-02T08:30:00.000Z",
                      "outcome": "pass",
                      "subject_type": null,
                      "subject_id": null,
                      "subject_label": null,
                      "performed_by_name": "Example Calibration Services",
                      "provider": "Example Calibration Services",
                      "as_found_condition": "Within tolerance",
                      "as_left_condition": "Within tolerance",
                      "result_summary": "Temperature and pressure checked at three points.",
                      "certificate_number": "ECS-2026-0412",
                      "certificate_url": null,
                      "next_due_at": "2026-12-02",
                      "notes": null
                    }
                  ],
                  "meta": {
                    "next_cursor": null
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.\n\n`FEATURE_NOT_INCLUDED` — Equipment calibration and maintenance records are not included in your plan.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED",
                            "FEATURE_NOT_INCLUDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              },
              {
                "code": "FEATURE_NOT_INCLUDED",
                "when": "Equipment calibration and maintenance records are not included in your plan."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "`NOT_FOUND` — No record with this id exists in the key's workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "NOT_FOUND"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "No record with this id exists in the key's workspace."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "422": {
            "description": "`VALIDATION_ERROR` — Both `cursor` and `offset` were sent; use one.\n\n`INVALID_CURSOR` — The cursor is malformed, or came from a different list or different filters. Start again without it.\n\n`VALIDATION_ERROR` — A query parameter is malformed, `from` is later than `to`, or `offset` was sent without `kind`. `error` names the parameter.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR",
                            "INVALID_CURSOR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "Both `cursor` and `offset` were sent; use one."
              },
              {
                "code": "INVALID_CURSOR",
                "when": "The cursor is malformed, or came from a different list or different filters. Start again without it."
              },
              {
                "code": "VALIDATION_ERROR",
                "when": "A query parameter is malformed, `from` is later than `to`, or `offset` was sent without `kind`. `error` names the parameter."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`LAB_EQUIPMENT_QUERY_FAILED` — The equipment could not be read. Retry later.\n\n`FEATURE_CHECK_FAILED` — Your plan could not be checked. Retry later.\n\n`EQUIPMENT_EVENT_QUERY_FAILED` — The history could not be read. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "LAB_EQUIPMENT_QUERY_FAILED",
                            "FEATURE_CHECK_FAILED",
                            "EQUIPMENT_EVENT_QUERY_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "LAB_EQUIPMENT_QUERY_FAILED",
                "when": "The equipment could not be read. Retry later."
              },
              {
                "code": "FEATURE_CHECK_FAILED",
                "when": "Your plan could not be checked. Retry later."
              },
              {
                "code": "EQUIPMENT_EVENT_QUERY_FAILED",
                "when": "The history could not be read. Retry later."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createEquipmentEvent",
        "tags": [
          "Equipment"
        ],
        "summary": "Record an equipment event",
        "description": "Records that a piece of equipment was used, calibrated or serviced — from the instrument itself, or from any system that knows. Events are history: there is no update or delete, and a correction is another event.\n\n`used` records what the equipment was used on; `subject_id` must be a record in the same workspace. `calibration` and `preventive_maintenance` record maintenance with an `outcome`, and are part of equipment calibration, which not every plan includes.\n\nRecording an event does not move the equipment's calibration or maintenance due dates; those follow the lab's schedules in xPlant.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "write:equipment_events"
        ],
        "x-idempotent": true,
        "x-cursor": false,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The record's id."
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "object",
                    "properties": {
                      "kind": {
                        "type": "string",
                        "const": "used",
                        "description": "The equipment was used."
                      },
                      "subject_type": {
                        "type": "string",
                        "enum": [
                          "sop_log",
                          "media_batch",
                          "plant_transfer",
                          "explant_transfer",
                          "contamination_log"
                        ],
                        "description": "What it was used on: `sop_log`, `media_batch`, `plant_transfer`, `explant_transfer`, `contamination_log`."
                      },
                      "subject_id": {
                        "type": "string",
                        "format": "uuid",
                        "description": "The id of that record. It must belong to the key's workspace."
                      },
                      "subject_label": {
                        "type": "string",
                        "maxLength": 200,
                        "description": "A readable name for the record, kept with the event."
                      },
                      "used_at": {
                        "type": "string",
                        "format": "date-time",
                        "description": "When it was used, as an ISO 8601 timestamp in UTC (ending in `Z`). Defaults to now."
                      },
                      "notes": {
                        "type": "string",
                        "maxLength": 2000
                      }
                    },
                    "required": [
                      "kind",
                      "subject_type",
                      "subject_id"
                    ]
                  },
                  {
                    "type": "object",
                    "properties": {
                      "kind": {
                        "type": "string",
                        "enum": [
                          "calibration",
                          "preventive_maintenance"
                        ],
                        "description": "`calibration`, or `preventive_maintenance` for a service."
                      },
                      "outcome": {
                        "type": "string",
                        "enum": [
                          "pass",
                          "pass_after_adjustment",
                          "out_of_tolerance",
                          "fail",
                          "not_performed"
                        ],
                        "default": "pass",
                        "description": "How it went: `pass`, `pass_after_adjustment`, `out_of_tolerance`, `fail`, `not_performed`. `out_of_tolerance` means the instrument was found outside its specification before any adjustment."
                      },
                      "performed_at": {
                        "type": "string",
                        "format": "date-time",
                        "description": "When the work was done, as an ISO 8601 timestamp in UTC (ending in `Z`). Defaults to now."
                      },
                      "performed_by_name": {
                        "type": "string",
                        "maxLength": 200,
                        "description": "Who did the work — a person or a service company."
                      },
                      "result_summary": {
                        "type": "string",
                        "maxLength": 2000
                      },
                      "notes": {
                        "type": "string",
                        "maxLength": 2000
                      }
                    },
                    "required": [
                      "kind"
                    ]
                  }
                ],
                "discriminator": {
                  "propertyName": "kind"
                }
              },
              "example": {
                "kind": "calibration",
                "outcome": "pass",
                "performed_at": "2026-06-02T08:30:00Z",
                "performed_by_name": "Example Calibration Services",
                "result_summary": "Temperature and pressure checked at three points."
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/EquipmentEventReceipt"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": {
                    "id": "c5d2e8f1-7a3b-4c69-9e14-0b8a6d4f2e57",
                    "equipmentId": "8c2f4e61-5a3b-4d7e-9b10-3e6a2c9d4f18",
                    "kind": "calibration",
                    "recordedAt": "2026-06-02T08:30:00+00:00"
                  }
                }
              }
            },
            "headers": {
              "Idempotent-Replay": {
                "$ref": "#/components/headers/IdempotentReplay"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "400": {
            "description": "`VALIDATION_ERROR` — The request body is not valid JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "The request body is not valid JSON."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.\n\n`FEATURE_NOT_INCLUDED` — A calibration or maintenance record was sent, and equipment calibration is not included in your plan.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED",
                            "FEATURE_NOT_INCLUDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              },
              {
                "code": "FEATURE_NOT_INCLUDED",
                "when": "A calibration or maintenance record was sent, and equipment calibration is not included in your plan."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "`NOT_FOUND` — The equipment, or a use's `subject_id`, is not in the key's workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "NOT_FOUND"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "The equipment, or a use's `subject_id`, is not in the key's workspace."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "409": {
            "description": "`IDEMPOTENCY_IN_FLIGHT` — A request with this `Idempotency-Key` is still being processed; retry after `Retry-After` seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "IDEMPOTENCY_IN_FLIGHT"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "IDEMPOTENCY_IN_FLIGHT",
                "when": "A request with this `Idempotency-Key` is still being processed; retry after `Retry-After` seconds."
              }
            ],
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "422": {
            "description": "`VALIDATION_ERROR` — The `Idempotency-Key` header is malformed.\n\n`VALIDATION_ERROR` — A field failed validation. `error` names the first one, for example `title: title is required`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "The `Idempotency-Key` header is malformed."
              },
              {
                "code": "VALIDATION_ERROR",
                "when": "A field failed validation. `error` names the first one, for example `title: title is required`."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`FEATURE_CHECK_FAILED` — Your plan could not be checked. Retry later.\n\n`EQUIPMENT_EVENT_CREATE_FAILED` — The event could not be saved. Retry with the same `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FEATURE_CHECK_FAILED",
                            "EQUIPMENT_EVENT_CREATE_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FEATURE_CHECK_FAILED",
                "when": "Your plan could not be checked. Retry later."
              },
              {
                "code": "EQUIPMENT_EVENT_CREATE_FAILED",
                "when": "The event could not be saved. Retry with the same `Idempotency-Key`."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      }
    },
    "/api/v1/events": {
      "get": {
        "operationId": "listChangeEvents",
        "tags": [
          "Change history"
        ],
        "summary": "List change history",
        "description": "What has happened to the workspace's plants or explants, oldest first: transfers, stage changes, contamination records, observations, printed labels.\n\nUse it to keep your own system in step with xPlant without downloading everything again:\n\n1. The first time, call with `entity=plant` and follow `meta.next_cursor` until it comes back `null`. Do the same with `entity=explant`.\n2. Save the newest `created_at` you received for each.\n3. Next time, pass that value as `since` and follow the cursor again from the first page.\n\nSeveral events can share one `created_at` — a batch import records many at once. The cursor keeps them in a fixed order, so a walk neither skips nor repeats one; still walk a `since` window to its last page before you move `since` forward, and skip any `id` you already hold.\n\nThe history is written by work done in xPlant. Transfers and stage moves recorded through this API's own endpoints do not add events here; their responses already carry what was saved.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "read:events"
        ],
        "x-idempotent": false,
        "x-cursor": true,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "name": "entity",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "plant",
                "explant"
              ]
            },
            "description": "Which history to read. Required: plant and explant histories are separate, so sync each with its own calls."
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Only events recorded after this moment, as an ISO 8601 timestamp such as `2026-09-24T00:00:00Z`. Pass the newest `created_at` you already hold."
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ChangeEvent"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/PageMeta"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": [
                    {
                      "id": "4b3a2918-0716-4f5e-8d4c-3b2a19087f6e",
                      "entity_type": "explant",
                      "entity_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
                      "stage_id": "6e5d4c3b-2a19-4807-96a5-b4c3d2e1f0a9",
                      "event_type": "transfer",
                      "event_time": "2026-09-24T00:00:00+00:00",
                      "recorded_by": "0d7e4b9a-6f21-4c3e-8a5b-2e9f1c7d4a60",
                      "payload": {
                        "transfer_id": "c7d8e9f0-a1b2-4c3d-8e4f-5a6b7c8d9e0f",
                        "transfer_cycle": 5,
                        "from_location": "Growth room 1, shelf B2",
                        "to_location": "Growth room 1, shelf C1",
                        "observed_vessel_count": 12,
                        "plantlet_count": 48
                      },
                      "created_at": "2026-09-24T09:12:45.118Z"
                    }
                  ],
                  "meta": {
                    "next_cursor": null
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "422": {
            "description": "`VALIDATION_ERROR` — Both `cursor` and `offset` were sent; use one.\n\n`INVALID_CURSOR` — The cursor is malformed, or came from a different list or different filters. Start again without it.\n\n`VALIDATION_ERROR` — `entity` is missing or is not `plant` or `explant`, or `since` is not a valid timestamp.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR",
                            "INVALID_CURSOR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "Both `cursor` and `offset` were sent; use one."
              },
              {
                "code": "INVALID_CURSOR",
                "when": "The cursor is malformed, or came from a different list or different filters. Start again without it."
              },
              {
                "code": "VALIDATION_ERROR",
                "when": "`entity` is missing or is not `plant` or `explant`, or `since` is not a valid timestamp."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`EVENT_QUERY_FAILED` — The history could not be read. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "EVENT_QUERY_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "EVENT_QUERY_FAILED",
                "when": "The history could not be read. Retry later."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      }
    },
    "/api/v1/explants": {
      "get": {
        "operationId": "listExplants",
        "tags": [
          "Explants"
        ],
        "summary": "List explants",
        "description": "The workspace's explants, newest first, a page at a time. Pass `externalId` to find the one culture filed under your own identifier instead; the answer is still a list, with that explant or nothing, and `meta.next_cursor` is always null.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "read:explants"
        ],
        "x-idempotent": false,
        "x-cursor": true,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "name": "externalId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Your own identifier for a culture, matched ignoring case against the identifier you gave it, then its batch number, then its label. Returns that one explant, or an empty list; paging does not apply."
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Explant"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/PageMeta"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": [
                    {
                      "id": "b7e4a2c9-1f3d-4e8b-8a6c-5d2f9e1b7c34",
                      "label": "Zebrina line A",
                      "external_id": "LINE-0412-A",
                      "status": "establishing",
                      "plant_id": "8c3f2d1a-6b4e-4f7a-9d2c-1e5b7a9c3f60",
                      "workspace_id": "3a9e7c21-5d4b-4c8f-a1e6-9b2d0f4c7e13",
                      "initial_count": null,
                      "current_count": null,
                      "created_at": "2026-09-25T10:05:00.000Z",
                      "custom_fields": {
                        "medium_lot": "MS-2609-03",
                        "cytokinin_added": true
                      }
                    }
                  ],
                  "meta": {
                    "next_cursor": null
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "422": {
            "description": "`VALIDATION_ERROR` — Both `cursor` and `offset` were sent; use one.\n\n`INVALID_CURSOR` — The cursor is malformed, or came from a different list or different filters. Start again without it.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR",
                            "INVALID_CURSOR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "Both `cursor` and `offset` were sent; use one."
              },
              {
                "code": "INVALID_CURSOR",
                "when": "The cursor is malformed, or came from a different list or different filters. Start again without it."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`EXPLANT_QUERY_FAILED` — The explants could not be read. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "EXPLANT_QUERY_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "EXPLANT_QUERY_FAILED",
                "when": "The explants could not be read. Retry later."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createExplant",
        "tags": [
          "Explants"
        ],
        "summary": "Create an explant",
        "description": "Adds an explant to the workspace, recorded as created by the key's owner. Link it to the plant it was taken from with `plant_id`.\n\nGive it your own identifier in `external_id` to find it again with `GET /api/v1/explants?externalId=`. Fill in the lab's own fields with `custom_fields`: each value is checked against its field, and the whole request is refused if one does not fit.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "write:explants"
        ],
        "x-idempotent": true,
        "x-cursor": false,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "label": {
                    "type": "string",
                    "maxLength": 200,
                    "minLength": 1,
                    "description": "The culture's label — the name the lab knows it by."
                  },
                  "plant_id": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "uuid",
                    "description": "The plant the explant was taken from. It must be a plant in the same workspace."
                  },
                  "batch_number": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 100
                  },
                  "notes": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 5000
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "active",
                      "establishing",
                      "growing",
                      "needs_subculture",
                      "quarantined",
                      "senescing",
                      "discarded",
                      "retired",
                      "lost"
                    ],
                    "description": "Where the culture stands. New explants default to `active`."
                  },
                  "external_id": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 100,
                    "minLength": 1,
                    "description": "Your own identifier for this culture, such as a batch or line code from your own records. It must be unique in the workspace, ignoring case, and it is how `GET /api/v1/explants?externalId=` finds the explant again. Send `null` to clear it."
                  },
                  "custom_fields": {
                    "type": "object",
                    "additionalProperties": {
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "number"
                        },
                        {
                          "type": "boolean"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "description": "Values for the lab's own fields, keyed by each field's key as set up in the lab's settings. Each value must fit its field: text, a number, `true` or `false`, a date, or one of a pick-list's options (a date is stored as `YYYY-MM-DD`). A key the lab has not set up is refused, and the values may total at most 10,000 bytes. Send `null` as a value to leave a field empty."
                  }
                },
                "required": [
                  "label"
                ]
              },
              "example": {
                "label": "Zebrina line A",
                "plant_id": "8c3f2d1a-6b4e-4f7a-9d2c-1e5b7a9c3f60",
                "status": "establishing",
                "external_id": "LINE-0412-A",
                "custom_fields": {
                  "medium_lot": "MS-2609-03",
                  "cytokinin_added": true
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/Explant"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": {
                    "id": "b7e4a2c9-1f3d-4e8b-8a6c-5d2f9e1b7c34",
                    "label": "Zebrina line A",
                    "external_id": "LINE-0412-A",
                    "status": "establishing",
                    "plant_id": "8c3f2d1a-6b4e-4f7a-9d2c-1e5b7a9c3f60",
                    "workspace_id": "3a9e7c21-5d4b-4c8f-a1e6-9b2d0f4c7e13",
                    "initial_count": null,
                    "current_count": null,
                    "created_at": "2026-09-25T10:05:00.000Z",
                    "custom_fields": {
                      "medium_lot": "MS-2609-03",
                      "cytokinin_added": true
                    }
                  }
                }
              }
            },
            "headers": {
              "Idempotent-Replay": {
                "$ref": "#/components/headers/IdempotentReplay"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "400": {
            "description": "`VALIDATION_ERROR` — The request body is not valid JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "The request body is not valid JSON."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.\n\n`PLAN_LIMIT_REACHED` — The key's owner has created as many explants as their plan allows.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED",
                            "PLAN_LIMIT_REACHED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              },
              {
                "code": "PLAN_LIMIT_REACHED",
                "when": "The key's owner has created as many explants as their plan allows."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "409": {
            "description": "`IDEMPOTENCY_IN_FLIGHT` — A request with this `Idempotency-Key` is still being processed; retry after `Retry-After` seconds.\n\n`DUPLICATE_ENTRY` — Another explant in the workspace already uses this `external_id`, including one that has been deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "IDEMPOTENCY_IN_FLIGHT",
                            "DUPLICATE_ENTRY"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "IDEMPOTENCY_IN_FLIGHT",
                "when": "A request with this `Idempotency-Key` is still being processed; retry after `Retry-After` seconds."
              },
              {
                "code": "DUPLICATE_ENTRY",
                "when": "Another explant in the workspace already uses this `external_id`, including one that has been deleted."
              }
            ],
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "422": {
            "description": "`VALIDATION_ERROR` — The `Idempotency-Key` header is malformed.\n\n`VALIDATION_ERROR` — A field failed validation. `error` names the first one, for example `title: title is required`.\n\n`VALIDATION_ERROR` — `plant_id` is not a plant in the workspace.\n\n`VALIDATION_ERROR` — `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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "The `Idempotency-Key` header is malformed."
              },
              {
                "code": "VALIDATION_ERROR",
                "when": "A field failed validation. `error` names the first one, for example `title: title is required`."
              },
              {
                "code": "VALIDATION_ERROR",
                "when": "`plant_id` is not a plant in the workspace."
              },
              {
                "code": "VALIDATION_ERROR",
                "when": "`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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`EXPLANT_CREATE_FAILED` — The explant could not be saved. Retry with the same `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "EXPLANT_CREATE_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "EXPLANT_CREATE_FAILED",
                "when": "The explant could not be saved. Retry with the same `Idempotency-Key`."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      }
    },
    "/api/v1/explants/{id}": {
      "get": {
        "operationId": "getExplant",
        "tags": [
          "Explants"
        ],
        "summary": "Get an explant",
        "description": "One explant, with the lab's own fields.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "read:explants"
        ],
        "x-idempotent": false,
        "x-cursor": false,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The record's id."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/Explant"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": {
                    "id": "b7e4a2c9-1f3d-4e8b-8a6c-5d2f9e1b7c34",
                    "label": "Zebrina line A",
                    "external_id": "LINE-0412-A",
                    "status": "establishing",
                    "plant_id": "8c3f2d1a-6b4e-4f7a-9d2c-1e5b7a9c3f60",
                    "workspace_id": "3a9e7c21-5d4b-4c8f-a1e6-9b2d0f4c7e13",
                    "initial_count": null,
                    "current_count": null,
                    "created_at": "2026-09-25T10:05:00.000Z",
                    "custom_fields": {
                      "medium_lot": "MS-2609-03",
                      "cytokinin_added": true
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "`NOT_FOUND` — No record with this id exists in the key's workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "NOT_FOUND"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "No record with this id exists in the key's workspace."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`EXPLANT_QUERY_FAILED` — The explant could not be read. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "EXPLANT_QUERY_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "EXPLANT_QUERY_FAILED",
                "when": "The explant could not be read. Retry later."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updateExplant",
        "tags": [
          "Explants"
        ],
        "summary": "Update an explant",
        "description": "Changes only the fields you send. Send `null` to clear an optional one.\n\n`custom_fields` changes only the keys you name: the explant's other values stay as they are, and `null` clears a field.\n\nA key can change the explants its owner created. Changing a teammate's explant needs the key's owner to be a manager or owner of the lab.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "write:explants"
        ],
        "x-idempotent": false,
        "x-cursor": false,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The record's id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "label": {
                    "type": "string",
                    "maxLength": 200,
                    "minLength": 1
                  },
                  "batch_number": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 100
                  },
                  "notes": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 5000
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "active",
                      "establishing",
                      "growing",
                      "needs_subculture",
                      "quarantined",
                      "senescing",
                      "discarded",
                      "retired",
                      "lost"
                    ],
                    "description": "Where the culture stands. New explants default to `active`."
                  },
                  "external_id": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 100,
                    "minLength": 1,
                    "description": "Your own identifier for this culture, such as a batch or line code from your own records. It must be unique in the workspace, ignoring case, and it is how `GET /api/v1/explants?externalId=` finds the explant again. Send `null` to clear it."
                  },
                  "custom_fields": {
                    "type": "object",
                    "additionalProperties": {
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "number"
                        },
                        {
                          "type": "boolean"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "description": "Values for the lab's own fields, keyed by each field's key. Only the keys you send change; the explant's other values stay as they are. Send `null` as a value to clear that field. Each value must fit its field, a key the lab has not set up is refused, and the record's values together may total at most 10,000 bytes."
                  }
                }
              },
              "example": {
                "status": "growing",
                "custom_fields": {
                  "cytokinin_added": false
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/Explant"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": {
                    "id": "b7e4a2c9-1f3d-4e8b-8a6c-5d2f9e1b7c34",
                    "label": "Zebrina line A",
                    "external_id": "LINE-0412-A",
                    "status": "growing",
                    "plant_id": "8c3f2d1a-6b4e-4f7a-9d2c-1e5b7a9c3f60",
                    "workspace_id": "3a9e7c21-5d4b-4c8f-a1e6-9b2d0f4c7e13",
                    "initial_count": null,
                    "current_count": null,
                    "created_at": "2026-09-25T10:05:00.000Z",
                    "custom_fields": {
                      "medium_lot": "MS-2609-03",
                      "cytokinin_added": false
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "400": {
            "description": "`VALIDATION_ERROR` — The request body is not valid JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "The request body is not valid JSON."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.\n\n`EXPLANT_WRITE_FORBIDDEN` — The explant was created by someone else, and the key's owner is not a manager or owner of the lab.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED",
                            "EXPLANT_WRITE_FORBIDDEN"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              },
              {
                "code": "EXPLANT_WRITE_FORBIDDEN",
                "when": "The explant was created by someone else, and the key's owner is not a manager or owner of the lab."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "`NOT_FOUND` — No record with this id exists in the key's workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "NOT_FOUND"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "No record with this id exists in the key's workspace."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "409": {
            "description": "`DUPLICATE_ENTRY` — Another explant in the workspace already uses this `external_id`, including one that has been deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "DUPLICATE_ENTRY"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "DUPLICATE_ENTRY",
                "when": "Another explant in the workspace already uses this `external_id`, including one that has been deleted."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "422": {
            "description": "`VALIDATION_ERROR` — A field failed validation. `error` names the first one, for example `title: title is required`.\n\n`VALIDATION_ERROR` — `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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "A field failed validation. `error` names the first one, for example `title: title is required`."
              },
              {
                "code": "VALIDATION_ERROR",
                "when": "`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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`EXPLANT_UPDATE_FAILED` — The change could not be saved. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "EXPLANT_UPDATE_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "EXPLANT_UPDATE_FAILED",
                "when": "The change could not be saved. Retry later."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      }
    },
    "/api/v1/label-scans": {
      "post": {
        "operationId": "createLabelScan",
        "tags": [
          "Labels"
        ],
        "summary": "Record a label scan",
        "description": "Records that a code was scanned — where, and when. Resolving a label only reads; this writes the visit down.\n\nSend `plant_id` or `explant_id` when you already know what the code belongs to, for example from resolving it first; the scan is then marked `resolved`. A scan that matched nothing is still worth recording.\n\nScans are append-only: a correction is another scan. Send an `Idempotency-Key` so a scanner retrying on a patchy connection records one visit rather than two.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "write:label_scans"
        ],
        "x-idempotent": true,
        "x-cursor": false,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "barcode": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 512,
                    "description": "The code exactly as the scanner read it."
                  },
                  "plant_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "The plant the code belongs to, when you already know it — for example from resolving the label first. It must be in this workspace."
                  },
                  "explant_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "The explant the code belongs to, when you already know it. It must be in this workspace."
                  },
                  "context": {
                    "type": "string",
                    "maxLength": 120,
                    "description": "Where the scan happened, in your own words — a bench, a shelf, a growth room door."
                  },
                  "scanned_at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "When the scan happened, in UTC with a trailing `Z`, such as `2026-09-25T08:30:00Z`. A timestamp with an offset is refused. Defaults to when xPlant receives it."
                  }
                },
                "required": [
                  "barcode"
                ]
              },
              "example": {
                "barcode": "LINE-0412",
                "explant_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
                "context": "Growth room 1, shelf C1",
                "scanned_at": "2026-09-25T08:30:00Z"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/LabelScan"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": {
                    "id": "d4c3b2a1-f6e5-4d7c-8b9a-0f1e2d3c4b5a",
                    "barcode": "LINE-0412",
                    "resolved": true,
                    "plantId": null,
                    "explantId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
                    "context": "Growth room 1, shelf C1",
                    "scannedAt": "2026-09-25T08:30:00+00:00"
                  }
                }
              }
            },
            "headers": {
              "Idempotent-Replay": {
                "$ref": "#/components/headers/IdempotentReplay"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "400": {
            "description": "`VALIDATION_ERROR` — The request body is not valid JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "The request body is not valid JSON."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "`NOT_FOUND` — The plant or explant named in the scan is not in this workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "NOT_FOUND"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "The plant or explant named in the scan is not in this workspace."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "409": {
            "description": "`IDEMPOTENCY_IN_FLIGHT` — A request with this `Idempotency-Key` is still being processed; retry after `Retry-After` seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "IDEMPOTENCY_IN_FLIGHT"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "IDEMPOTENCY_IN_FLIGHT",
                "when": "A request with this `Idempotency-Key` is still being processed; retry after `Retry-After` seconds."
              }
            ],
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "422": {
            "description": "`VALIDATION_ERROR` — The `Idempotency-Key` header is malformed.\n\n`VALIDATION_ERROR` — A field failed validation, for example a missing `barcode` or a `scanned_at` that is not a UTC timestamp. `error` gives the first problem but does not name the field.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "The `Idempotency-Key` header is malformed."
              },
              {
                "code": "VALIDATION_ERROR",
                "when": "A field failed validation, for example a missing `barcode` or a `scanned_at` that is not a UTC timestamp. `error` gives the first problem but does not name the field."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`LABEL_SCAN_CREATE_FAILED` — The scan could not be saved. Retry with the same `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "LABEL_SCAN_CREATE_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "LABEL_SCAN_CREATE_FAILED",
                "when": "The scan could not be saved. Retry with the same `Idempotency-Key`."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      }
    },
    "/api/v1/labels/resolve": {
      "get": {
        "operationId": "resolveLabel",
        "tags": [
          "Labels"
        ],
        "summary": "Resolve a label",
        "description": "Turns a scanned or typed code into the record it identifies, for scanners and bench hardware. xPlant checks, in order: plant labels, explant labels, container labels, then your own plant and explant identifiers — so a code your lab writes on the jar itself, such as `LINE-0412`, resolves too.\n\nA container code answers with the container and, in `contents`, the items stored at its location.\n\nLooking a code up leaves no trace. To record that someone was at the shelf, also call Record a label scan.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "read:labels"
        ],
        "x-idempotent": false,
        "x-cursor": false,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "name": "barcode",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The code exactly as scanned or typed. A plant, explant or container label must match exactly; your own identifiers match ignoring case."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/LabelResolution"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": {
                    "barcode": "LINE-0412",
                    "record_type": "explant",
                    "record_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
                    "display_name": "Alocasia LINE-0412",
                    "url": "/dashboard/explants/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "400": {
            "description": "`VALIDATION_ERROR` — `barcode` is missing or blank.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "`barcode` is missing or blank."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "`NOT_FOUND` — Nothing in this workspace matches the code.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "NOT_FOUND"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "Nothing in this workspace matches the code."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`LINK_QUERY_FAILED` — The code could not be looked up. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "LINK_QUERY_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "LINK_QUERY_FAILED",
                "when": "The code could not be looked up. Retry later."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      }
    },
    "/api/v1/me": {
      "get": {
        "operationId": "getMe",
        "tags": [
          "Account"
        ],
        "summary": "Get the calling key",
        "description": "Who the calling key is: its name, the workspace it acts in, the scopes it holds, and which of them it can use right now. Make this the first call an integration makes: it confirms the key works and tells you in one answer what it may do, instead of one `403` at a time.\n\nA key never does more than its owner can in xPlant. `effectiveScopes` drops any scope above the owner's current role, and on a plan that includes connected devices only (`apiAccess: \"devices\"`) it keeps just the device scopes.\n\nIt needs no scope, and it returns nothing about the workspace beyond its id.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [],
        "x-idempotent": false,
        "x-cursor": false,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/Me"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": {
                    "key": {
                      "id": "3f6a2c1e-9b7d-4e21-8c5a-1d0e7f9b2a64",
                      "name": "Bench station 3",
                      "prefix": "xpk_live_4f1c9e2ab70",
                      "environment": "production",
                      "status": "active",
                      "lastUsedAt": "2026-09-24T17:02:11.000Z",
                      "createdAt": "2026-08-30T09:15:00.000Z"
                    },
                    "scopes": [
                      "read:plants",
                      "read:transfers",
                      "write:transfers"
                    ],
                    "effectiveScopes": [
                      "read:plants",
                      "read:transfers",
                      "write:transfers"
                    ],
                    "role": "member",
                    "apiAccess": "full",
                    "workspace": {
                      "id": "8c2d4f6a-1b3e-4a5c-9d7e-0f1a2b3c4d5e"
                    },
                    "user": {
                      "id": "0d7e4b9a-6f21-4c3e-8a5b-2e9f1c7d4a60"
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/media-recipes": {
      "get": {
        "operationId": "listMediaRecipes",
        "tags": [
          "Media recipes"
        ],
        "summary": "List media recipes",
        "description": "The workspace's culture media recipes that the key's owner can see — their own, and those shared with the workspace or published — newest first, each with its components. Another member's private recipe is not listed. Filter by `status`.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "read:media_recipes"
        ],
        "x-idempotent": false,
        "x-cursor": true,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "archived",
                "deprecated",
                "draft",
                "published"
              ]
            },
            "description": "Only recipes with this status."
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/MediaRecipe"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/PageMeta"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": [
                    {
                      "id": "a3d5f7b9-1c2e-4f6a-8b0d-2e4f6a8c0e1f",
                      "title": "MS + 1 mg/L BAP",
                      "status": "active",
                      "origin": "user",
                      "visibility": "team",
                      "is_public": false,
                      "notes": "Shoot multiplication medium for aroid cultures.",
                      "ph_target": 5.8,
                      "sterilization_notes": "Autoclave for 20 minutes at 121 °C.",
                      "storage_notes": "Keep poured vessels at 4 °C and use within two weeks.",
                      "usage_notes": null,
                      "components": [
                        {
                          "id": "c1a2b3c4-d5e6-4f70-8a9b-0c1d2e3f4a5b",
                          "name": "MS basal salts",
                          "qty": "4.4",
                          "unit": "g/L",
                          "concentration": null
                        },
                        {
                          "id": "d2b3c4d5-e6f7-4a81-9b0c-1d2e3f4a5b6c",
                          "name": "Sucrose",
                          "qty": "30",
                          "unit": "g/L",
                          "concentration": null
                        },
                        {
                          "id": "e3c4d5e6-f7a8-4b92-8c1d-2e3f4a5b6c7d",
                          "name": "6-BAP",
                          "qty": "1",
                          "unit": "mg/L",
                          "concentration": null
                        },
                        {
                          "id": "f4d5e6f7-a8b9-4ca3-9d2e-3f4a5b6c7d8e",
                          "name": "Agar",
                          "qty": "7",
                          "unit": "g/L",
                          "concentration": null
                        }
                      ],
                      "version": 1,
                      "created_by": "0d7e4b9a-6f21-4c3e-8a5b-2e9f1c7d4a60",
                      "created_at": "2026-09-25T14:12:03.000Z"
                    }
                  ],
                  "meta": {
                    "next_cursor": null
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "422": {
            "description": "`VALIDATION_ERROR` — Both `cursor` and `offset` were sent; use one.\n\n`INVALID_CURSOR` — The cursor is malformed, or came from a different list or different filters. Start again without it.\n\n`VALIDATION_ERROR` — `status` is not one of the recipe statuses.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR",
                            "INVALID_CURSOR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "Both `cursor` and `offset` were sent; use one."
              },
              {
                "code": "INVALID_CURSOR",
                "when": "The cursor is malformed, or came from a different list or different filters. Start again without it."
              },
              {
                "code": "VALIDATION_ERROR",
                "when": "`status` is not one of the recipe statuses."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`MEDIA_RECIPE_QUERY_FAILED` — The recipes could not be read. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "MEDIA_RECIPE_QUERY_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "MEDIA_RECIPE_QUERY_FAILED",
                "when": "The recipes could not be read. Retry later."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createMediaRecipe",
        "tags": [
          "Media recipes"
        ],
        "summary": "Create a media recipe",
        "description": "Creates a recipe in the workspace with the key's owner as its creator. It starts at version 1 and is `private` to its creator unless `visibility` is `team`.\n\nA component sent without an `id` is given one; keep the ids from the response to update the recipe later.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "write:media_recipes"
        ],
        "x-idempotent": true,
        "x-cursor": false,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200,
                    "description": "The recipe's name, for example `MS + 1 mg/L BAP`."
                  },
                  "components": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 64,
                          "description": "Your id for this component. Kept as given; when left out, one is assigned and returned, so an update can send it back."
                        },
                        "name": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 200,
                          "description": "The ingredient, for example `Sucrose` or `6-BAP`."
                        },
                        "qty": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 50,
                          "description": "The amount as the recipe writes it, for example `30` or `0.5`."
                        },
                        "unit": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 20,
                          "description": "The unit of `qty`, for example `g/L` or `mg/L`."
                        },
                        "concentration": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 50,
                          "description": "A concentration, when the recipe states one apart from the amount."
                        }
                      },
                      "required": [
                        "name",
                        "qty"
                      ]
                    },
                    "minItems": 1,
                    "maxItems": 100,
                    "description": "The ingredients, in order. At least one."
                  },
                  "notes": {
                    "type": "string",
                    "maxLength": 500,
                    "description": "Free-text notes, up to 500 characters."
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "active",
                      "archived",
                      "deprecated",
                      "draft",
                      "published"
                    ],
                    "default": "active",
                    "description": "One of `active`, `archived`, `deprecated`, `draft`, `published`."
                  },
                  "visibility": {
                    "type": "string",
                    "enum": [
                      "private",
                      "team"
                    ],
                    "default": "private",
                    "description": "Who in the workspace can see the recipe. `private`: only the key's owner, who is recorded as its creator. `team`: everyone in the workspace."
                  },
                  "is_public": {
                    "type": "boolean",
                    "default": false,
                    "description": "Publish the recipe to xPlant's public recipe library, where anyone can read it. A `draft` recipe stays out of the library either way."
                  },
                  "origin": {
                    "type": "string",
                    "enum": [
                      "user",
                      "imported"
                    ],
                    "default": "user",
                    "description": "`imported` for a recipe brought in from another system; `user` otherwise."
                  },
                  "ph_target": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "minimum": 0,
                    "maximum": 14,
                    "description": "Target pH, 0 to 14. Stored to two decimal places."
                  },
                  "sterilization_notes": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 500,
                    "description": "How to sterilize the medium, up to 500 characters."
                  },
                  "storage_notes": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 500,
                    "description": "How to store the prepared medium, up to 500 characters."
                  },
                  "usage_notes": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 500,
                    "description": "How to use the medium, up to 500 characters."
                  }
                },
                "required": [
                  "title",
                  "components"
                ]
              },
              "example": {
                "title": "MS + 1 mg/L BAP",
                "components": [
                  {
                    "name": "MS basal salts",
                    "qty": "4.4",
                    "unit": "g/L"
                  },
                  {
                    "name": "Sucrose",
                    "qty": "30",
                    "unit": "g/L"
                  },
                  {
                    "name": "6-BAP",
                    "qty": "1",
                    "unit": "mg/L"
                  },
                  {
                    "name": "Agar",
                    "qty": "7",
                    "unit": "g/L"
                  }
                ],
                "notes": "Shoot multiplication medium for aroid cultures.",
                "visibility": "team",
                "ph_target": 5.8,
                "sterilization_notes": "Autoclave for 20 minutes at 121 °C.",
                "storage_notes": "Keep poured vessels at 4 °C and use within two weeks."
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/MediaRecipe"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": {
                    "id": "a3d5f7b9-1c2e-4f6a-8b0d-2e4f6a8c0e1f",
                    "title": "MS + 1 mg/L BAP",
                    "status": "active",
                    "origin": "user",
                    "visibility": "team",
                    "is_public": false,
                    "notes": "Shoot multiplication medium for aroid cultures.",
                    "ph_target": 5.8,
                    "sterilization_notes": "Autoclave for 20 minutes at 121 °C.",
                    "storage_notes": "Keep poured vessels at 4 °C and use within two weeks.",
                    "usage_notes": null,
                    "components": [
                      {
                        "id": "c1a2b3c4-d5e6-4f70-8a9b-0c1d2e3f4a5b",
                        "name": "MS basal salts",
                        "qty": "4.4",
                        "unit": "g/L",
                        "concentration": null
                      },
                      {
                        "id": "d2b3c4d5-e6f7-4a81-9b0c-1d2e3f4a5b6c",
                        "name": "Sucrose",
                        "qty": "30",
                        "unit": "g/L",
                        "concentration": null
                      },
                      {
                        "id": "e3c4d5e6-f7a8-4b92-8c1d-2e3f4a5b6c7d",
                        "name": "6-BAP",
                        "qty": "1",
                        "unit": "mg/L",
                        "concentration": null
                      },
                      {
                        "id": "f4d5e6f7-a8b9-4ca3-9d2e-3f4a5b6c7d8e",
                        "name": "Agar",
                        "qty": "7",
                        "unit": "g/L",
                        "concentration": null
                      }
                    ],
                    "version": 1,
                    "created_by": "0d7e4b9a-6f21-4c3e-8a5b-2e9f1c7d4a60",
                    "created_at": "2026-09-25T14:12:03.000Z"
                  }
                }
              }
            },
            "headers": {
              "Idempotent-Replay": {
                "$ref": "#/components/headers/IdempotentReplay"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "400": {
            "description": "`VALIDATION_ERROR` — The request body is not valid JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "The request body is not valid JSON."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "409": {
            "description": "`IDEMPOTENCY_IN_FLIGHT` — A request with this `Idempotency-Key` is still being processed; retry after `Retry-After` seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "IDEMPOTENCY_IN_FLIGHT"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "IDEMPOTENCY_IN_FLIGHT",
                "when": "A request with this `Idempotency-Key` is still being processed; retry after `Retry-After` seconds."
              }
            ],
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "422": {
            "description": "`VALIDATION_ERROR` — The `Idempotency-Key` header is malformed.\n\n`VALIDATION_ERROR` — A field failed validation. `error` names the first one, for example `title: title is required`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "The `Idempotency-Key` header is malformed."
              },
              {
                "code": "VALIDATION_ERROR",
                "when": "A field failed validation. `error` names the first one, for example `title: title is required`."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`MEDIA_RECIPE_CREATE_FAILED` — The recipe could not be saved. Retry with the same `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "MEDIA_RECIPE_CREATE_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "MEDIA_RECIPE_CREATE_FAILED",
                "when": "The recipe could not be saved. Retry with the same `Idempotency-Key`."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      }
    },
    "/api/v1/media-recipes/{id}": {
      "get": {
        "operationId": "getMediaRecipe",
        "tags": [
          "Media recipes"
        ],
        "summary": "Get a media recipe",
        "description": "One recipe with its components and current version.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "read:media_recipes"
        ],
        "x-idempotent": false,
        "x-cursor": false,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The record's id."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/MediaRecipe"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": {
                    "id": "a3d5f7b9-1c2e-4f6a-8b0d-2e4f6a8c0e1f",
                    "title": "MS + 1 mg/L BAP",
                    "status": "active",
                    "origin": "user",
                    "visibility": "team",
                    "is_public": false,
                    "notes": "Shoot multiplication medium for aroid cultures.",
                    "ph_target": 5.8,
                    "sterilization_notes": "Autoclave for 20 minutes at 121 °C.",
                    "storage_notes": "Keep poured vessels at 4 °C and use within two weeks.",
                    "usage_notes": null,
                    "components": [
                      {
                        "id": "c1a2b3c4-d5e6-4f70-8a9b-0c1d2e3f4a5b",
                        "name": "MS basal salts",
                        "qty": "4.4",
                        "unit": "g/L",
                        "concentration": null
                      },
                      {
                        "id": "d2b3c4d5-e6f7-4a81-9b0c-1d2e3f4a5b6c",
                        "name": "Sucrose",
                        "qty": "30",
                        "unit": "g/L",
                        "concentration": null
                      },
                      {
                        "id": "e3c4d5e6-f7a8-4b92-8c1d-2e3f4a5b6c7d",
                        "name": "6-BAP",
                        "qty": "1",
                        "unit": "mg/L",
                        "concentration": null
                      },
                      {
                        "id": "f4d5e6f7-a8b9-4ca3-9d2e-3f4a5b6c7d8e",
                        "name": "Agar",
                        "qty": "7",
                        "unit": "g/L",
                        "concentration": null
                      }
                    ],
                    "version": 1,
                    "created_by": "0d7e4b9a-6f21-4c3e-8a5b-2e9f1c7d4a60",
                    "created_at": "2026-09-25T14:12:03.000Z"
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "`NOT_FOUND` — 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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "NOT_FOUND"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`MEDIA_RECIPE_QUERY_FAILED` — The recipe could not be read. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "MEDIA_RECIPE_QUERY_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "MEDIA_RECIPE_QUERY_FAILED",
                "when": "The recipe could not be read. Retry later."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updateMediaRecipe",
        "tags": [
          "Media recipes"
        ],
        "summary": "Update a media recipe",
        "description": "Changes only the fields you send. Only the recipe's creator can change it.\n\nA change to the formulation — the title, notes, components, pH or preparation notes — records a new version and raises `version`. Earlier versions are kept, and records already linked to the recipe keep the version they were linked to. Changing `status`, `visibility` or `is_public` does not create a version.\n\n`components` replaces the whole list, so send every component to keep, with its `id`.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "write:media_recipes"
        ],
        "x-idempotent": false,
        "x-cursor": false,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The record's id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200,
                    "description": "The recipe's name."
                  },
                  "components": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 64,
                          "description": "Your id for this component. Kept as given; when left out, one is assigned and returned, so an update can send it back."
                        },
                        "name": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 200,
                          "description": "The ingredient, for example `Sucrose` or `6-BAP`."
                        },
                        "qty": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 50,
                          "description": "The amount as the recipe writes it, for example `30` or `0.5`."
                        },
                        "unit": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 20,
                          "description": "The unit of `qty`, for example `g/L` or `mg/L`."
                        },
                        "concentration": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 50,
                          "description": "A concentration, when the recipe states one apart from the amount."
                        }
                      },
                      "required": [
                        "name",
                        "qty"
                      ]
                    },
                    "minItems": 1,
                    "maxItems": 100,
                    "description": "The complete ingredient list. It replaces the current one, so send every component to keep, with its `id`."
                  },
                  "notes": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 500,
                    "description": "Free-text notes, up to 500 characters. `null` clears them."
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "active",
                      "archived",
                      "deprecated",
                      "draft",
                      "published"
                    ],
                    "description": "One of `active`, `archived`, `deprecated`, `draft`, `published`."
                  },
                  "visibility": {
                    "type": "string",
                    "enum": [
                      "private",
                      "team"
                    ],
                    "description": "Who in the workspace can see the recipe. `private`: only the key's owner, who is recorded as its creator. `team`: everyone in the workspace."
                  },
                  "is_public": {
                    "type": "boolean",
                    "description": "Publish the recipe to xPlant's public recipe library, where anyone can read it. A `draft` recipe stays out of the library either way."
                  },
                  "ph_target": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "minimum": 0,
                    "maximum": 14,
                    "description": "Target pH, 0 to 14. Stored to two decimal places. `null` clears it."
                  },
                  "sterilization_notes": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 500,
                    "description": "How to sterilize the medium, up to 500 characters."
                  },
                  "storage_notes": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 500,
                    "description": "How to store the prepared medium, up to 500 characters."
                  },
                  "usage_notes": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 500,
                    "description": "How to use the medium, up to 500 characters."
                  }
                }
              },
              "example": {
                "title": "MS + 0.5 mg/L BAP",
                "components": [
                  {
                    "id": "c1a2b3c4-d5e6-4f70-8a9b-0c1d2e3f4a5b",
                    "name": "MS basal salts",
                    "qty": "4.4",
                    "unit": "g/L"
                  },
                  {
                    "id": "d2b3c4d5-e6f7-4a81-9b0c-1d2e3f4a5b6c",
                    "name": "Sucrose",
                    "qty": "30",
                    "unit": "g/L"
                  },
                  {
                    "id": "e3c4d5e6-f7a8-4b92-8c1d-2e3f4a5b6c7d",
                    "name": "6-BAP",
                    "qty": "0.5",
                    "unit": "mg/L"
                  },
                  {
                    "id": "f4d5e6f7-a8b9-4ca3-9d2e-3f4a5b6c7d8e",
                    "name": "Agar",
                    "qty": "7",
                    "unit": "g/L"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/MediaRecipe"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": {
                    "id": "a3d5f7b9-1c2e-4f6a-8b0d-2e4f6a8c0e1f",
                    "title": "MS + 0.5 mg/L BAP",
                    "status": "active",
                    "origin": "user",
                    "visibility": "team",
                    "is_public": false,
                    "notes": "Shoot multiplication medium for aroid cultures.",
                    "ph_target": 5.8,
                    "sterilization_notes": "Autoclave for 20 minutes at 121 °C.",
                    "storage_notes": "Keep poured vessels at 4 °C and use within two weeks.",
                    "usage_notes": null,
                    "components": [
                      {
                        "id": "c1a2b3c4-d5e6-4f70-8a9b-0c1d2e3f4a5b",
                        "name": "MS basal salts",
                        "qty": "4.4",
                        "unit": "g/L",
                        "concentration": null
                      },
                      {
                        "id": "d2b3c4d5-e6f7-4a81-9b0c-1d2e3f4a5b6c",
                        "name": "Sucrose",
                        "qty": "30",
                        "unit": "g/L",
                        "concentration": null
                      },
                      {
                        "id": "e3c4d5e6-f7a8-4b92-8c1d-2e3f4a5b6c7d",
                        "name": "6-BAP",
                        "qty": "0.5",
                        "unit": "mg/L",
                        "concentration": null
                      },
                      {
                        "id": "f4d5e6f7-a8b9-4ca3-9d2e-3f4a5b6c7d8e",
                        "name": "Agar",
                        "qty": "7",
                        "unit": "g/L",
                        "concentration": null
                      }
                    ],
                    "version": 2,
                    "created_by": "0d7e4b9a-6f21-4c3e-8a5b-2e9f1c7d4a60",
                    "created_at": "2026-09-25T14:12:03.000Z"
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "400": {
            "description": "`VALIDATION_ERROR` — The request body is not valid JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "The request body is not valid JSON."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.\n\n`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED",
                            "MEDIA_RECIPE_NOT_OWNER"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              },
              {
                "code": "MEDIA_RECIPE_NOT_OWNER",
                "when": "The key's owner can see the recipe but did not create it. Only a recipe's creator can change it."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "`NOT_FOUND` — 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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "NOT_FOUND"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "422": {
            "description": "`VALIDATION_ERROR` — A field failed validation. `error` names the first one, for example `title: title is required`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "A field failed validation. `error` names the first one, for example `title: title is required`."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`MEDIA_RECIPE_UPDATE_FAILED` — The change could not be saved. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "MEDIA_RECIPE_UPDATE_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "MEDIA_RECIPE_UPDATE_FAILED",
                "when": "The change could not be saved. Retry later."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      }
    },
    "/api/v1/plants": {
      "get": {
        "operationId": "listPlants",
        "tags": [
          "Plants"
        ],
        "summary": "List plants",
        "description": "The workspace's plants, newest first, a page at a time. Pass `externalId` to find the one plant filed under your own identifier instead; the answer is still a list, with that plant or nothing, and `meta.next_cursor` is always null.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "read:plants"
        ],
        "x-idempotent": false,
        "x-cursor": true,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "name": "externalId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Your own identifier for a plant, matched ignoring case. Returns that one plant, or an empty list; paging does not apply."
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Plant"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/PageMeta"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": [
                    {
                      "id": "8c3f2d1a-6b4e-4f7a-9d2c-1e5b7a9c3f60",
                      "name": "Zebra Alocasia",
                      "species": "Alocasia zebrina",
                      "status": "active",
                      "workspace_id": "3a9e7c21-5d4b-4c8f-a1e6-9b2d0f4c7e13",
                      "created_at": "2026-09-25T09:30:00.000Z",
                      "external_id": "LINE-0412",
                      "custom_fields": {
                        "mother_stock_tray": "B4",
                        "weeks_in_culture": 12
                      }
                    }
                  ],
                  "meta": {
                    "next_cursor": null
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "422": {
            "description": "`VALIDATION_ERROR` — Both `cursor` and `offset` were sent; use one.\n\n`INVALID_CURSOR` — The cursor is malformed, or came from a different list or different filters. Start again without it.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR",
                            "INVALID_CURSOR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "Both `cursor` and `offset` were sent; use one."
              },
              {
                "code": "INVALID_CURSOR",
                "when": "The cursor is malformed, or came from a different list or different filters. Start again without it."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`PLANT_QUERY_FAILED` — The plants could not be read. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PLANT_QUERY_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PLANT_QUERY_FAILED",
                "when": "The plants could not be read. Retry later."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createPlant",
        "tags": [
          "Plants"
        ],
        "summary": "Create a plant",
        "description": "Adds a plant to the workspace, recorded as created by the key's owner, and starts it in `initial_stage`.\n\nGive it your own identifier in `external_id` to find it again with `GET /api/v1/plants?externalId=`. Fill in the lab's own fields with `custom_fields`: each value is checked against its field, and the whole request is refused if one does not fit.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "write:plants"
        ],
        "x-idempotent": true,
        "x-cursor": false,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "species": {
                    "type": "string",
                    "maxLength": 200,
                    "minLength": 1,
                    "description": "The plant's species, for example `Alocasia zebrina`."
                  },
                  "common_name": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 200,
                    "description": "The name the lab calls the plant. Without one, the plant is shown by its species."
                  },
                  "genus": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 100
                  },
                  "family": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 100
                  },
                  "cultivar": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 200
                  },
                  "source": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 200,
                    "description": "Where the plant came from — a supplier, a nursery or another collection."
                  },
                  "notes": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 5000
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "active",
                      "dormant",
                      "harvested",
                      "contaminated",
                      "failed",
                      "in_culture",
                      "ready_for_transfer",
                      "quarantined",
                      "archived"
                    ],
                    "description": "Where the plant stands in the lab. New plants default to `active`."
                  },
                  "initial_stage": {
                    "type": "string",
                    "enum": [
                      "Mother Block",
                      "Acclimation",
                      "Production",
                      "Cold Storage",
                      "Quarantine",
                      "Propagation",
                      "Hardening Off",
                      "Greenhouse",
                      "Field",
                      "Discarded"
                    ],
                    "description": "The stage the plant starts in. Defaults to `Mother Block`."
                  },
                  "external_id": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 100,
                    "minLength": 1,
                    "description": "Your own identifier for the plant, such as a line or accession code from your own records. It must be unique in the workspace, ignoring case, and it is how `GET /api/v1/plants?externalId=` finds the plant again. Send `null` to clear it."
                  },
                  "custom_fields": {
                    "type": "object",
                    "additionalProperties": {
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "number"
                        },
                        {
                          "type": "boolean"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "description": "Values for the lab's own fields, keyed by each field's key as set up in the lab's settings. Each value must fit its field: text, a number, `true` or `false`, a date, or one of a pick-list's options (a date is stored as `YYYY-MM-DD`). A key the lab has not set up is refused, and the values may total at most 10,000 bytes. Send `null` as a value to leave a field empty."
                  }
                },
                "required": [
                  "species"
                ]
              },
              "example": {
                "species": "Alocasia zebrina",
                "common_name": "Zebra Alocasia",
                "genus": "Alocasia",
                "source": "Stock line from the lab's own collection",
                "initial_stage": "Mother Block",
                "external_id": "LINE-0412",
                "custom_fields": {
                  "mother_stock_tray": "B4",
                  "weeks_in_culture": 12
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/Plant"
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "warning": {
                          "type": "string",
                          "description": "Sent only when the plant was saved but its first stage could not be set. Record one with `POST /api/v1/stages`."
                        }
                      },
                      "required": [
                        "warning"
                      ]
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": {
                    "id": "8c3f2d1a-6b4e-4f7a-9d2c-1e5b7a9c3f60",
                    "name": "Zebra Alocasia",
                    "species": "Alocasia zebrina",
                    "status": "active",
                    "workspace_id": "3a9e7c21-5d4b-4c8f-a1e6-9b2d0f4c7e13",
                    "created_at": "2026-09-25T09:30:00.000Z",
                    "external_id": "LINE-0412",
                    "custom_fields": {
                      "mother_stock_tray": "B4",
                      "weeks_in_culture": 12
                    }
                  }
                }
              }
            },
            "headers": {
              "Idempotent-Replay": {
                "$ref": "#/components/headers/IdempotentReplay"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "400": {
            "description": "`VALIDATION_ERROR` — The request body is not valid JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "The request body is not valid JSON."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.\n\n`PLAN_LIMIT_REACHED` — The key's owner has created as many plants as their plan allows. `error` says which limit.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED",
                            "PLAN_LIMIT_REACHED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              },
              {
                "code": "PLAN_LIMIT_REACHED",
                "when": "The key's owner has created as many plants as their plan allows. `error` says which limit."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "409": {
            "description": "`IDEMPOTENCY_IN_FLIGHT` — A request with this `Idempotency-Key` is still being processed; retry after `Retry-After` seconds.\n\n`DUPLICATE_ENTRY` — Another plant in the workspace already uses this `external_id`, including one that has been deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "IDEMPOTENCY_IN_FLIGHT",
                            "DUPLICATE_ENTRY"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "IDEMPOTENCY_IN_FLIGHT",
                "when": "A request with this `Idempotency-Key` is still being processed; retry after `Retry-After` seconds."
              },
              {
                "code": "DUPLICATE_ENTRY",
                "when": "Another plant in the workspace already uses this `external_id`, including one that has been deleted."
              }
            ],
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "422": {
            "description": "`VALIDATION_ERROR` — The `Idempotency-Key` header is malformed.\n\n`VALIDATION_ERROR` — A field failed validation. `error` names the first one, for example `title: title is required`.\n\n`VALIDATION_ERROR` — `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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "The `Idempotency-Key` header is malformed."
              },
              {
                "code": "VALIDATION_ERROR",
                "when": "A field failed validation. `error` names the first one, for example `title: title is required`."
              },
              {
                "code": "VALIDATION_ERROR",
                "when": "`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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`PLANT_CREATE_FAILED` — The plant could not be saved. Retry with the same `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PLANT_CREATE_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PLANT_CREATE_FAILED",
                "when": "The plant could not be saved. Retry with the same `Idempotency-Key`."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      }
    },
    "/api/v1/plants/{id}": {
      "get": {
        "operationId": "getPlant",
        "tags": [
          "Plants"
        ],
        "summary": "Get a plant",
        "description": "One plant, with the lab's own fields.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "read:plants"
        ],
        "x-idempotent": false,
        "x-cursor": false,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The record's id."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/Plant"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": {
                    "id": "8c3f2d1a-6b4e-4f7a-9d2c-1e5b7a9c3f60",
                    "name": "Zebra Alocasia",
                    "species": "Alocasia zebrina",
                    "status": "active",
                    "workspace_id": "3a9e7c21-5d4b-4c8f-a1e6-9b2d0f4c7e13",
                    "created_at": "2026-09-25T09:30:00.000Z",
                    "external_id": "LINE-0412",
                    "custom_fields": {
                      "mother_stock_tray": "B4",
                      "weeks_in_culture": 12
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "`NOT_FOUND` — No record with this id exists in the key's workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "NOT_FOUND"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "No record with this id exists in the key's workspace."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`PLANT_QUERY_FAILED` — The plant could not be read. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PLANT_QUERY_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PLANT_QUERY_FAILED",
                "when": "The plant could not be read. Retry later."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updatePlant",
        "tags": [
          "Plants"
        ],
        "summary": "Update a plant",
        "description": "Changes only the fields you send. Send `null` to clear an optional one.\n\n`custom_fields` changes only the keys you name: the plant's other values stay as they are, and `null` clears a field.\n\nA key can change the plants its owner created. Changing a teammate's plant needs the key's owner to be a manager or owner of the lab.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "write:plants"
        ],
        "x-idempotent": false,
        "x-cursor": false,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The record's id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "species": {
                    "type": "string",
                    "maxLength": 200,
                    "minLength": 1
                  },
                  "common_name": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 200
                  },
                  "genus": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 100
                  },
                  "family": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 100
                  },
                  "cultivar": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 200
                  },
                  "source": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 200
                  },
                  "notes": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 5000
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "active",
                      "dormant",
                      "harvested",
                      "contaminated",
                      "failed",
                      "in_culture",
                      "ready_for_transfer",
                      "quarantined",
                      "archived"
                    ],
                    "description": "Where the plant stands in the lab. New plants default to `active`."
                  },
                  "external_id": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 100,
                    "minLength": 1,
                    "description": "Your own identifier for the plant, such as a line or accession code from your own records. It must be unique in the workspace, ignoring case, and it is how `GET /api/v1/plants?externalId=` finds the plant again. Send `null` to clear it."
                  },
                  "custom_fields": {
                    "type": "object",
                    "additionalProperties": {
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "number"
                        },
                        {
                          "type": "boolean"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "description": "Values for the lab's own fields, keyed by each field's key. Only the keys you send change; the plant's other values stay as they are. Send `null` as a value to clear that field. Each value must fit its field, a key the lab has not set up is refused, and the record's values together may total at most 10,000 bytes."
                  }
                }
              },
              "example": {
                "status": "ready_for_transfer",
                "custom_fields": {
                  "weeks_in_culture": 13
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/Plant"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": {
                    "id": "8c3f2d1a-6b4e-4f7a-9d2c-1e5b7a9c3f60",
                    "name": "Zebra Alocasia",
                    "species": "Alocasia zebrina",
                    "status": "ready_for_transfer",
                    "workspace_id": "3a9e7c21-5d4b-4c8f-a1e6-9b2d0f4c7e13",
                    "created_at": "2026-09-25T09:30:00.000Z",
                    "external_id": "LINE-0412",
                    "custom_fields": {
                      "mother_stock_tray": "B4",
                      "weeks_in_culture": 13
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "400": {
            "description": "`VALIDATION_ERROR` — The request body is not valid JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "The request body is not valid JSON."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.\n\n`PLANT_WRITE_FORBIDDEN` — The plant was created by someone else, and the key's owner is not a manager or owner of the lab.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED",
                            "PLANT_WRITE_FORBIDDEN"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              },
              {
                "code": "PLANT_WRITE_FORBIDDEN",
                "when": "The plant was created by someone else, and the key's owner is not a manager or owner of the lab."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "`NOT_FOUND` — No record with this id exists in the key's workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "NOT_FOUND"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "No record with this id exists in the key's workspace."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "409": {
            "description": "`DUPLICATE_ENTRY` — Another plant in the workspace already uses this `external_id`, including one that has been deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "DUPLICATE_ENTRY"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "DUPLICATE_ENTRY",
                "when": "Another plant in the workspace already uses this `external_id`, including one that has been deleted."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "422": {
            "description": "`VALIDATION_ERROR` — A field failed validation. `error` names the first one, for example `title: title is required`.\n\n`VALIDATION_ERROR` — `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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "A field failed validation. `error` names the first one, for example `title: title is required`."
              },
              {
                "code": "VALIDATION_ERROR",
                "when": "`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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`PLANT_UPDATE_FAILED` — The change could not be saved. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PLANT_UPDATE_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PLANT_UPDATE_FAILED",
                "when": "The change could not be saved. Retry later."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      }
    },
    "/api/v1/pricing/culture-lines": {
      "get": {
        "operationId": "listCultureLinePrices",
        "tags": [
          "Pricing"
        ],
        "summary": "List culture line prices",
        "description": "Each priced culture line's current list and wholesale price and its pricing tier, newest first by when the line was first priced. Filter to one culture line with `plant_id`, or to one tier with `pricing_tier`.\n\nEvery amount is exact decimal text with its currency. Never add amounts in different currencies.\n\nNeeds culture line pricing in the workspace's plan; without it the answer is `FEATURE_NOT_INCLUDED`.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "read:pricing"
        ],
        "x-idempotent": false,
        "x-cursor": true,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "name": "plant_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Only this culture line's price. A culture line is identified by its plant id."
          },
          {
            "name": "pricing_tier",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 40
            },
            "description": "Only culture lines in this pricing tier, spelled exactly as your lab names it."
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CultureLinePrice"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/PageMeta"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": [
                    {
                      "id": "a4c8e2f6-1b3d-4f5a-9c7e-2d4b6f8a0c12",
                      "plant_id": "6d1b3f8a-4c2e-4a97-b5d0-8e7f2a1c9b43",
                      "list_price": {
                        "amount": "18.50",
                        "currency": "USD"
                      },
                      "wholesale_price": {
                        "amount": "11.00",
                        "currency": "USD"
                      },
                      "previous_list_price": {
                        "amount": "16.00",
                        "currency": "USD"
                      },
                      "pricing_tier": "B",
                      "tier_score": "72.500",
                      "price_source": "manual",
                      "price_source_at": "2026-09-10T09:12:00.000Z",
                      "notes": null,
                      "created_at": "2026-02-03T11:40:22.000Z",
                      "updated_at": "2026-09-10T09:12:00.000Z"
                    }
                  ],
                  "meta": {
                    "next_cursor": null
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.\n\n`FEATURE_NOT_INCLUDED` — Culture line pricing is not included in your plan.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED",
                            "FEATURE_NOT_INCLUDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              },
              {
                "code": "FEATURE_NOT_INCLUDED",
                "when": "Culture line pricing is not included in your plan."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "`NOT_FOUND` — `plant_id` names no culture line in the key's workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "NOT_FOUND"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "`plant_id` names no culture line in the key's workspace."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "422": {
            "description": "`VALIDATION_ERROR` — Both `cursor` and `offset` were sent; use one.\n\n`INVALID_CURSOR` — The cursor is malformed, or came from a different list or different filters. Start again without it.\n\n`VALIDATION_ERROR` — A query parameter is malformed. `error` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR",
                            "INVALID_CURSOR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "Both `cursor` and `offset` were sent; use one."
              },
              {
                "code": "INVALID_CURSOR",
                "when": "The cursor is malformed, or came from a different list or different filters. Start again without it."
              },
              {
                "code": "VALIDATION_ERROR",
                "when": "A query parameter is malformed. `error` names it."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`FEATURE_CHECK_FAILED` — Your plan could not be checked. Retry later.\n\n`CULTURE_LINE_PRICE_QUERY_FAILED` — The prices could not be read. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FEATURE_CHECK_FAILED",
                            "CULTURE_LINE_PRICE_QUERY_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FEATURE_CHECK_FAILED",
                "when": "Your plan could not be checked. Retry later."
              },
              {
                "code": "CULTURE_LINE_PRICE_QUERY_FAILED",
                "when": "The prices could not be read. Retry later."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      }
    },
    "/api/v1/pricing/events": {
      "get": {
        "operationId": "listPriceEvents",
        "tags": [
          "Pricing"
        ],
        "summary": "List price changes",
        "description": "Every change to a culture line's price, newest first: what it moved from, what it moved to, when, who or what moved it. Filter with `plant_id` for one culture line and `from` and `to` for a period.\n\nEach amount carries its own currency, and a previous price may be in a different currency from the new one.\n\nNeeds culture line pricing in the workspace's plan; without it the answer is `FEATURE_NOT_INCLUDED`.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "read:pricing"
        ],
        "x-idempotent": false,
        "x-cursor": true,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "name": "plant_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Only changes to this culture line's price (a plant id)."
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Only changes made at or after this time, as an ISO 8601 timestamp."
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Only changes made at or before this time, as an ISO 8601 timestamp."
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PriceEvent"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/PageMeta"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": [
                    {
                      "id": "e7b1d3f5-9a2c-4e6b-8d0f-3a5c7e9b1d24",
                      "plant_id": "6d1b3f8a-4c2e-4a97-b5d0-8e7f2a1c9b43",
                      "list_price": {
                        "amount": "18.50",
                        "currency": "USD"
                      },
                      "previous_list_price": {
                        "amount": "16.00",
                        "currency": "USD"
                      },
                      "price_source": "manual",
                      "changed_by": "0d7e4b9a-6f21-4c3e-8a5b-2e9f1c7d4a60",
                      "changed_at": "2026-09-10T09:12:00.000Z"
                    }
                  ],
                  "meta": {
                    "next_cursor": null
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.\n\n`FEATURE_NOT_INCLUDED` — Culture line pricing is not included in your plan.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED",
                            "FEATURE_NOT_INCLUDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              },
              {
                "code": "FEATURE_NOT_INCLUDED",
                "when": "Culture line pricing is not included in your plan."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "`NOT_FOUND` — `plant_id` names no culture line in the key's workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "NOT_FOUND"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "`plant_id` names no culture line in the key's workspace."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "422": {
            "description": "`VALIDATION_ERROR` — Both `cursor` and `offset` were sent; use one.\n\n`INVALID_CURSOR` — The cursor is malformed, or came from a different list or different filters. Start again without it.\n\n`VALIDATION_ERROR` — A query parameter is malformed, or `from` is later than `to`. `error` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR",
                            "INVALID_CURSOR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "Both `cursor` and `offset` were sent; use one."
              },
              {
                "code": "INVALID_CURSOR",
                "when": "The cursor is malformed, or came from a different list or different filters. Start again without it."
              },
              {
                "code": "VALIDATION_ERROR",
                "when": "A query parameter is malformed, or `from` is later than `to`. `error` names it."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`FEATURE_CHECK_FAILED` — Your plan could not be checked. Retry later.\n\n`CULTURE_LINE_PRICE_QUERY_FAILED` — The prices could not be read. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FEATURE_CHECK_FAILED",
                            "CULTURE_LINE_PRICE_QUERY_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FEATURE_CHECK_FAILED",
                "when": "Your plan could not be checked. Retry later."
              },
              {
                "code": "CULTURE_LINE_PRICE_QUERY_FAILED",
                "when": "The prices could not be read. Retry later."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      }
    },
    "/api/v1/sensor-readings": {
      "get": {
        "operationId": "listSensorReadings",
        "tags": [
          "Sensor readings"
        ],
        "summary": "List sensor readings",
        "description": "The workspace's environmental readings, newest first, a page at a time. Narrow them to one device, one growing room or one kind of measurement, and use `since` and `until` to fetch only the readings taken within a window.\n\nPages are larger here than on other lists: 100 readings by default and up to 1,000. To read further back than one page reaches, follow `meta.next_cursor` with the same filters until it comes back `null`. Readings taken at the same moment keep a fixed order, so none is skipped or repeated.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "read:sensor_readings"
        ],
        "x-idempotent": false,
        "x-cursor": true,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "name": "device_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Only readings from this device."
          },
          {
            "name": "room_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Only readings for this growing room."
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "temperature",
                "humidity",
                "ph",
                "co2",
                "light",
                "other"
              ]
            },
            "description": "Only this kind of reading."
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Only readings taken at or after this time, as an ISO 8601 timestamp."
          },
          {
            "name": "until",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Only readings taken at or before this time, as an ISO 8601 timestamp. Must not be earlier than `since`."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size. Values above 1,000 are capped at 1,000.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            }
          },
          {
            "$ref": "#/components/parameters/Offset"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SensorReading"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/PageMeta"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": [
                    {
                      "id": "f1a7c3e9-2b4d-4f6a-8c0e-3d5b7a9c1e24",
                      "device_id": "8e3b1f52-6c0d-4a7e-9b21-5f4d8c2a7e13",
                      "team_id": "5d2e8b17-9c4a-4e3f-b6d0-1a7c9e2f4b83",
                      "room_id": "2c6f9a41-7d3e-4b8a-a1c5-9e0d4f7b3a26",
                      "type": "temperature",
                      "value": 24.1,
                      "unit": "°C",
                      "recorded_at": "2026-09-25T14:00:00.000Z",
                      "notes": null,
                      "created_at": "2026-09-25T14:00:31.000Z"
                    }
                  ],
                  "meta": {
                    "next_cursor": null
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "422": {
            "description": "`VALIDATION_ERROR` — Both `cursor` and `offset` were sent; use one.\n\n`INVALID_CURSOR` — The cursor is malformed, or came from a different list or different filters. Start again without it.\n\n`VALIDATION_ERROR` — 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`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR",
                            "INVALID_CURSOR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "Both `cursor` and `offset` were sent; use one."
              },
              {
                "code": "INVALID_CURSOR",
                "when": "The cursor is malformed, or came from a different list or different filters. Start again without it."
              },
              {
                "code": "VALIDATION_ERROR",
                "when": "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`."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`READINGS_QUERY_FAILED` — The readings could not be read. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "READINGS_QUERY_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "READINGS_QUERY_FAILED",
                "when": "The readings could not be read. Retry later."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createSensorReadings",
        "tags": [
          "Sensor readings"
        ],
        "summary": "Post sensor readings",
        "description": "Stores readings from a device. Send one reading, or up to 500 at once as `{ \"readings\": [...] }`. **Batch them:** a gateway that buffers readings and posts every 30 to 60 seconds uses a small fraction of the requests of one that posts each reading as it is taken.\n\nThe response mirrors the request — one reading back for one reading sent, a list for a batch.\n\nTo make a retry safe, send `external_id` and `recorded_at` on every reading. A reading that repeats one already stored for the same device is skipped rather than stored twice, and is left out of the response — so a skipped single reading returns `ok: true` with no `data`.\n\nOne bad reading refuses the whole batch: if any reading fails validation, names a device or room outside the workspace, or comes from a device that is paused or retired, nothing in the request is stored.\n\nReadings have a budget of their own on top of the request limit: 5,000 per key or device token and 10,000 per workspace in any five minutes. Over it, the answer is `429 RATE_LIMIT_EXCEEDED` with a `Retry-After` header.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "deviceToken": []
          }
        ],
        "x-required-scopes": [
          "write:sensor_readings"
        ],
        "x-idempotent": false,
        "x-cursor": false,
        "x-device-token": true,
        "x-documented": true,
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "object",
                    "properties": {
                      "device_id": {
                        "type": "string",
                        "format": "uuid",
                        "description": "The device that took the reading."
                      },
                      "type": {
                        "type": "string",
                        "enum": [
                          "temperature",
                          "humidity",
                          "ph",
                          "co2",
                          "light",
                          "other"
                        ],
                        "description": "What was measured."
                      },
                      "value": {
                        "type": "number",
                        "description": "The measured value."
                      },
                      "unit": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 20,
                        "description": "The unit the value is in, for example `celsius` or `%`."
                      },
                      "room_id": {
                        "type": "string",
                        "format": "uuid",
                        "description": "The growing room the reading describes. Must be a room in the workspace."
                      },
                      "recorded_at": {
                        "type": "string",
                        "format": "date-time",
                        "description": "When the reading was taken, as an ISO 8601 timestamp in UTC ending in `Z`. Defaults to the time the request arrives. Required when you send `external_id`."
                      },
                      "notes": {
                        "type": "string",
                        "maxLength": 500,
                        "description": "A free-text note."
                      },
                      "external_id": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 200,
                        "description": "Your own id for the reading. A reading with the same device, `external_id` and `recorded_at` as one already stored is skipped, so a retried batch is not stored twice."
                      }
                    },
                    "required": [
                      "device_id",
                      "type",
                      "value",
                      "unit"
                    ]
                  },
                  {
                    "type": "object",
                    "properties": {
                      "readings": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "properties": {
                            "device_id": {
                              "type": "string",
                              "format": "uuid",
                              "description": "The device that took the reading."
                            },
                            "type": {
                              "type": "string",
                              "enum": [
                                "temperature",
                                "humidity",
                                "ph",
                                "co2",
                                "light",
                                "other"
                              ],
                              "description": "What was measured."
                            },
                            "value": {
                              "type": "number",
                              "description": "The measured value."
                            },
                            "unit": {
                              "type": "string",
                              "minLength": 1,
                              "maxLength": 20,
                              "description": "The unit the value is in, for example `celsius` or `%`."
                            },
                            "room_id": {
                              "type": "string",
                              "format": "uuid",
                              "description": "The growing room the reading describes. Must be a room in the workspace."
                            },
                            "recorded_at": {
                              "type": "string",
                              "format": "date-time",
                              "description": "When the reading was taken, as an ISO 8601 timestamp in UTC ending in `Z`. Defaults to the time the request arrives. Required when you send `external_id`."
                            },
                            "notes": {
                              "type": "string",
                              "maxLength": 500,
                              "description": "A free-text note."
                            },
                            "external_id": {
                              "type": "string",
                              "minLength": 1,
                              "maxLength": 200,
                              "description": "Your own id for the reading. A reading with the same device, `external_id` and `recorded_at` as one already stored is skipped, so a retried batch is not stored twice."
                            }
                          },
                          "required": [
                            "device_id",
                            "type",
                            "value",
                            "unit"
                          ]
                        },
                        "minItems": 1,
                        "maxItems": 500,
                        "description": "Between 1 and 500 readings."
                      }
                    },
                    "required": [
                      "readings"
                    ]
                  }
                ]
              },
              "example": {
                "readings": [
                  {
                    "device_id": "8e3b1f52-6c0d-4a7e-9b21-5f4d8c2a7e13",
                    "type": "temperature",
                    "value": 24.1,
                    "unit": "°C",
                    "room_id": "2c6f9a41-7d3e-4b8a-a1c5-9e0d4f7b3a26",
                    "recorded_at": "2026-09-25T14:00:00.000Z",
                    "external_id": "shelf3-temperature-20260925T140000Z"
                  },
                  {
                    "device_id": "8e3b1f52-6c0d-4a7e-9b21-5f4d8c2a7e13",
                    "type": "humidity",
                    "value": 88,
                    "unit": "%",
                    "room_id": "2c6f9a41-7d3e-4b8a-a1c5-9e0d4f7b3a26",
                    "recorded_at": "2026-09-25T14:00:00.000Z",
                    "external_id": "shelf3-humidity-20260925T140000Z"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/SensorReading"
                        },
                        {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/SensorReading"
                          }
                        }
                      ]
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": [
                    {
                      "id": "f1a7c3e9-2b4d-4f6a-8c0e-3d5b7a9c1e24",
                      "device_id": "8e3b1f52-6c0d-4a7e-9b21-5f4d8c2a7e13",
                      "team_id": "5d2e8b17-9c4a-4e3f-b6d0-1a7c9e2f4b83",
                      "room_id": "2c6f9a41-7d3e-4b8a-a1c5-9e0d4f7b3a26",
                      "type": "temperature",
                      "value": 24.1,
                      "unit": "°C",
                      "recorded_at": "2026-09-25T14:00:00.000Z",
                      "notes": null,
                      "created_at": "2026-09-25T14:00:31.000Z"
                    },
                    {
                      "id": "0b9d4e2f-6a8c-4d1e-9f3a-7c5e1b8d2a46",
                      "device_id": "8e3b1f52-6c0d-4a7e-9b21-5f4d8c2a7e13",
                      "team_id": "5d2e8b17-9c4a-4e3f-b6d0-1a7c9e2f4b83",
                      "room_id": "2c6f9a41-7d3e-4b8a-a1c5-9e0d4f7b3a26",
                      "type": "humidity",
                      "value": 88,
                      "unit": "%",
                      "recorded_at": "2026-09-25T14:00:00.000Z",
                      "notes": null,
                      "created_at": "2026-09-25T14:00:31.000Z"
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "400": {
            "description": "`VALIDATION_ERROR` — The request body is not valid JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "The request body is not valid JSON."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`PAID_PLAN_REQUIRED` — The workspace has no paid plan. Connecting devices is included with every paid plan.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "The workspace has no paid plan. Connecting devices is included with every paid plan."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_WRONG_DEVICE` — A device token was used to write about a device other than its own.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_WRONG_DEVICE"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_WRONG_DEVICE",
                "when": "A device token was used to write about a device other than its own."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "`NOT_FOUND` — A `device_id` or `room_id` in the request is not in the workspace. Nothing was stored.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "NOT_FOUND"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "A `device_id` or `room_id` in the request is not in the workspace. Nothing was stored."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "409": {
            "description": "`DEVICE_INGEST_DISABLED` — A device in the request is paused or retired, so its readings are refused. `error` names it; nothing was stored.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "DEVICE_INGEST_DISABLED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "DEVICE_INGEST_DISABLED",
                "when": "A device in the request is paused or retired, so its readings are refused. `error` names it; nothing was stored."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "422": {
            "description": "`VALIDATION_ERROR` — A reading failed validation. `error` names the first field, for example `readings.0.value: Expected number, received string`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "A reading failed validation. `error` names the first field, for example `readings.0.value: Expected number, received string`."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`READINGS_CREATE_FAILED` — The readings could not be stored. Retry the same request — readings that carry `external_id` are not stored twice.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "READINGS_CREATE_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "READINGS_CREATE_FAILED",
                "when": "The readings could not be stored. Retry the same request — readings that carry `external_id` are not stored twice."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      }
    },
    "/api/v1/sop-runs": {
      "post": {
        "operationId": "createSopRun",
        "tags": [
          "SOP runs"
        ],
        "summary": "Start an SOP run",
        "description": "Starts a run of an SOP: the record that a procedure was carried out, step by step, against the version the lab had in force.\n\nA bench station typically:\n\n1. Reads the SOP with Get an SOP and shows the steps of the version in force.\n2. Starts a run here, naming the batch or line it is for in `batch_code`.\n3. Posts evidence against each step as the work happens — a confirmation, a scanned vessel label, a reading from a balance or pH meter — with Record a step event and Record a step measurement.\n\nThe run follows the version in force when it starts, and you cannot choose another. Send an `Idempotency-Key` so a retried start does not open a second run.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "write:sop_runs"
        ],
        "x-idempotent": true,
        "x-cursor": false,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "sop_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "The SOP to run. It must have a version in force."
                  },
                  "batch_code": {
                    "type": "string",
                    "maxLength": 120,
                    "description": "Your lab's own identifier for what is being run, such as a batch or line code."
                  },
                  "plant_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "The plant the run is for, when there is one. It must be in this workspace."
                  }
                },
                "required": [
                  "sop_id"
                ]
              },
              "example": {
                "sop_id": "2e4f6a8c-0b1d-4e3f-9a5b-7c9d1e3f5a7b",
                "batch_code": "LINE-0412"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/SopRun"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": {
                    "id": "9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
                    "sopId": "2e4f6a8c-0b1d-4e3f-9a5b-7c9d1e3f5a7b",
                    "version": 2,
                    "versionId": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f",
                    "status": "in_progress",
                    "batchCode": "LINE-0412",
                    "startedAt": "2026-09-25T08:02:13.000Z",
                    "completedAt": null,
                    "progress": 0,
                    "steps": []
                  }
                }
              }
            },
            "headers": {
              "Idempotent-Replay": {
                "$ref": "#/components/headers/IdempotentReplay"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "400": {
            "description": "`VALIDATION_ERROR` — The request body is not valid JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "The request body is not valid JSON."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "`NOT_FOUND` — The SOP, or the plant named in `plant_id`, is not in this workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "NOT_FOUND"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "The SOP, or the plant named in `plant_id`, is not in this workspace."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "409": {
            "description": "`IDEMPOTENCY_IN_FLIGHT` — A request with this `Idempotency-Key` is still being processed; retry after `Retry-After` seconds.\n\n`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "IDEMPOTENCY_IN_FLIGHT",
                            "SOP_RUN_NOT_EFFECTIVE"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "IDEMPOTENCY_IN_FLIGHT",
                "when": "A request with this `Idempotency-Key` is still being processed; retry after `Retry-After` seconds."
              },
              {
                "code": "SOP_RUN_NOT_EFFECTIVE",
                "when": "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."
              }
            ],
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "422": {
            "description": "`VALIDATION_ERROR` — The `Idempotency-Key` header is malformed.\n\n`VALIDATION_ERROR` — 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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "The `Idempotency-Key` header is malformed."
              },
              {
                "code": "VALIDATION_ERROR",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`SOP_RUN_CREATE_FAILED` — The run could not be started. Retry with the same `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "SOP_RUN_CREATE_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "SOP_RUN_CREATE_FAILED",
                "when": "The run could not be started. Retry with the same `Idempotency-Key`."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      }
    },
    "/api/v1/sop-runs/{id}": {
      "get": {
        "operationId": "getSopRun",
        "tags": [
          "SOP runs"
        ],
        "summary": "Get an SOP run",
        "description": "One run: the SOP and version it follows, where it stands, and every piece of evidence posted against its steps, oldest first — the trail to read forwards when you need to know what was done, in what order.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "read:sop_runs"
        ],
        "x-idempotent": false,
        "x-cursor": false,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The run's id."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/SopRunDetail"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": {
                    "id": "9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
                    "sopId": "2e4f6a8c-0b1d-4e3f-9a5b-7c9d1e3f5a7b",
                    "version": 2,
                    "versionId": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f",
                    "status": "in_progress",
                    "batchCode": "LINE-0412",
                    "startedAt": "2026-09-25T08:02:13.000Z",
                    "completedAt": null,
                    "progress": 0,
                    "steps": [],
                    "events": [
                      {
                        "id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
                        "runId": "9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
                        "stepKey": "step-2",
                        "eventType": "measured",
                        "recordedAt": "2026-09-25T08:21:40+00:00",
                        "payload": {
                          "metric": "ph",
                          "value": 5.7,
                          "unit": "pH",
                          "notes": "Initiation medium before pouring."
                        }
                      },
                      {
                        "id": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b",
                        "runId": "9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
                        "stepKey": "step-3",
                        "eventType": "scanned",
                        "recordedAt": "2026-09-25T08:30:00+00:00",
                        "payload": {
                          "code": "LINE-0412-J07"
                        }
                      }
                    ]
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "`NOT_FOUND` — No run with this id exists in the key's workspace, or the run id is not a well-formed id.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "NOT_FOUND"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "No run with this id exists in the key's workspace, or the run id is not a well-formed id."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`SOP_RUN_QUERY_FAILED` — The run could not be read. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "SOP_RUN_QUERY_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "SOP_RUN_QUERY_FAILED",
                "when": "The run could not be read. Retry later."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      }
    },
    "/api/v1/sop-runs/{id}/steps/{stepId}/events": {
      "post": {
        "operationId": "createSopStepEvent",
        "tags": [
          "SOP runs"
        ],
        "summary": "Record a step event",
        "description": "Records what happened at one step of a run: someone confirmed it, scanned a label or vessel, skipped it, or left a note, or an instrument reported its state.\n\nEvidence is append-only. There is no edit or delete — to correct something, post another event that says so. Send an `Idempotency-Key` so a scanner retrying on a patchy connection records one event rather than two.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "write:sop_steps"
        ],
        "x-idempotent": true,
        "x-cursor": false,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The run's id."
          },
          {
            "name": "stepId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The step's `id` in the version the run follows, as Get an SOP lists it in `version.steps`. It is recorded as sent and not checked against the procedure."
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "event_type": {
                    "type": "string",
                    "enum": [
                      "confirmed",
                      "scanned",
                      "skipped",
                      "note",
                      "device_state"
                    ],
                    "description": "What happened at the step: `confirmed` (it was done), `scanned` (a label or vessel was scanned), `skipped`, `note` (a remark for whoever reads the run later) or `device_state` (what an instrument reported, such as a hood or autoclave cycle)."
                  },
                  "payload": {
                    "type": "object",
                    "additionalProperties": {},
                    "default": {},
                    "description": "Any detail worth keeping with the event, as a JSON object — the code that was scanned, the note's text, the device's reading. Stored as sent."
                  },
                  "recorded_at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "When it happened, in UTC with a trailing `Z`, such as `2026-09-25T08:30:00Z`. A timestamp with an offset is refused. Defaults to when xPlant receives it."
                  }
                },
                "required": [
                  "event_type"
                ]
              },
              "example": {
                "event_type": "scanned",
                "payload": {
                  "code": "LINE-0412-J07"
                },
                "recorded_at": "2026-09-25T08:30:00Z"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/SopStepEvent"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": {
                    "id": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b",
                    "runId": "9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
                    "stepKey": "step-3",
                    "eventType": "scanned",
                    "recordedAt": "2026-09-25T08:30:00+00:00",
                    "payload": {
                      "code": "LINE-0412-J07"
                    }
                  }
                }
              }
            },
            "headers": {
              "Idempotent-Replay": {
                "$ref": "#/components/headers/IdempotentReplay"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "400": {
            "description": "`VALIDATION_ERROR` — The request body is not valid JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "The request body is not valid JSON."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "`NOT_FOUND` — No run with this id exists in the key's workspace, or the run id is not a well-formed id.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "NOT_FOUND"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "No run with this id exists in the key's workspace, or the run id is not a well-formed id."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "409": {
            "description": "`IDEMPOTENCY_IN_FLIGHT` — A request with this `Idempotency-Key` is still being processed; retry after `Retry-After` seconds.\n\n`SOP_RUN_CLOSED` — The run is complete. A completed run's record is final and takes no more evidence.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "IDEMPOTENCY_IN_FLIGHT",
                            "SOP_RUN_CLOSED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "IDEMPOTENCY_IN_FLIGHT",
                "when": "A request with this `Idempotency-Key` is still being processed; retry after `Retry-After` seconds."
              },
              {
                "code": "SOP_RUN_CLOSED",
                "when": "The run is complete. A completed run's record is final and takes no more evidence."
              }
            ],
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "422": {
            "description": "`VALIDATION_ERROR` — The `Idempotency-Key` header is malformed.\n\n`VALIDATION_ERROR` — A field failed validation — an `event_type` outside the list, or a `recorded_at` that is not a UTC timestamp. `error` gives the first problem but does not name the field.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "The `Idempotency-Key` header is malformed."
              },
              {
                "code": "VALIDATION_ERROR",
                "when": "A field failed validation — an `event_type` outside the list, or a `recorded_at` that is not a UTC timestamp. `error` gives the first problem but does not name the field."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`SOP_STEP_EVENT_CREATE_FAILED` — The evidence could not be saved. Retry with the same `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "SOP_STEP_EVENT_CREATE_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "SOP_STEP_EVENT_CREATE_FAILED",
                "when": "The evidence could not be saved. Retry with the same `Idempotency-Key`."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      }
    },
    "/api/v1/sop-runs/{id}/steps/{stepId}/measurements": {
      "post": {
        "operationId": "createSopStepMeasurement",
        "tags": [
          "SOP runs"
        ],
        "summary": "Record a step measurement",
        "description": "Records a numeric reading against one step of a run — the pH of a medium, the mass of an ingredient, a temperature — with its unit, as the bench station running the SOP reads it from a balance, pH meter or probe.\n\nThe reading joins the same trail as the step events, with `eventType` `measured`, so a run reads in one order. The unit is required: a number without one is not a record anyone can rely on later. Send an `Idempotency-Key` so a retried post does not record the reading twice.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "write:sop_steps"
        ],
        "x-idempotent": true,
        "x-cursor": false,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The run's id."
          },
          {
            "name": "stepId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The step's `id` in the version the run follows, as Get an SOP lists it in `version.steps`. It is recorded as sent and not checked against the procedure."
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "metric": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 60,
                    "description": "What was measured, in your own words — `temperature`, `ph`, `mass`."
                  },
                  "value": {
                    "type": "number",
                    "description": "The reading."
                  },
                  "unit": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 20,
                    "description": "The unit the reading is in, such as `C`, `pH` or `g`. Required."
                  },
                  "recorded_at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "When it happened, in UTC with a trailing `Z`, such as `2026-09-25T08:30:00Z`. A timestamp with an offset is refused. Defaults to when xPlant receives it."
                  },
                  "notes": {
                    "type": "string",
                    "maxLength": 500,
                    "description": "Anything the reading needs beside it."
                  }
                },
                "required": [
                  "metric",
                  "value",
                  "unit"
                ]
              },
              "example": {
                "metric": "ph",
                "value": 5.7,
                "unit": "pH",
                "recorded_at": "2026-09-25T08:21:40Z",
                "notes": "Initiation medium before pouring."
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/SopStepEvent"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": {
                    "id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
                    "runId": "9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
                    "stepKey": "step-2",
                    "eventType": "measured",
                    "recordedAt": "2026-09-25T08:21:40+00:00",
                    "payload": {
                      "metric": "ph",
                      "value": 5.7,
                      "unit": "pH",
                      "notes": "Initiation medium before pouring."
                    }
                  }
                }
              }
            },
            "headers": {
              "Idempotent-Replay": {
                "$ref": "#/components/headers/IdempotentReplay"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "400": {
            "description": "`VALIDATION_ERROR` — The request body is not valid JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "The request body is not valid JSON."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "`NOT_FOUND` — No run with this id exists in the key's workspace, or the run id is not a well-formed id.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "NOT_FOUND"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "No run with this id exists in the key's workspace, or the run id is not a well-formed id."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "409": {
            "description": "`IDEMPOTENCY_IN_FLIGHT` — A request with this `Idempotency-Key` is still being processed; retry after `Retry-After` seconds.\n\n`SOP_RUN_CLOSED` — The run is complete. A completed run's record is final and takes no more evidence.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "IDEMPOTENCY_IN_FLIGHT",
                            "SOP_RUN_CLOSED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "IDEMPOTENCY_IN_FLIGHT",
                "when": "A request with this `Idempotency-Key` is still being processed; retry after `Retry-After` seconds."
              },
              {
                "code": "SOP_RUN_CLOSED",
                "when": "The run is complete. A completed run's record is final and takes no more evidence."
              }
            ],
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "422": {
            "description": "`VALIDATION_ERROR` — The `Idempotency-Key` header is malformed.\n\n`VALIDATION_ERROR` — A field failed validation — a missing `unit`, a `value` that is not a number, or a `recorded_at` that is not a UTC timestamp. `error` gives the first problem but does not name the field.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "The `Idempotency-Key` header is malformed."
              },
              {
                "code": "VALIDATION_ERROR",
                "when": "A field failed validation — a missing `unit`, a `value` that is not a number, or a `recorded_at` that is not a UTC timestamp. `error` gives the first problem but does not name the field."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`SOP_STEP_EVENT_CREATE_FAILED` — The evidence could not be saved. Retry with the same `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "SOP_STEP_EVENT_CREATE_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "SOP_STEP_EVENT_CREATE_FAILED",
                "when": "The evidence could not be saved. Retry with the same `Idempotency-Key`."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      }
    },
    "/api/v1/sops": {
      "get": {
        "operationId": "listSops",
        "tags": [
          "SOPs"
        ],
        "summary": "List SOPs",
        "description": "The workspace's standard operating procedures, most recently updated first — drafts and archived ones included, with `status` saying which. Summaries only: fetch one SOP to read the steps of the version in force.\n\nEditing an SOP moves it to the front of the list, so an SOP edited while you page through can move between pages: it may appear twice, or not at all.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "read:sops"
        ],
        "x-idempotent": false,
        "x-cursor": true,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SopSummary"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/PageMeta"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": [
                    {
                      "id": "2e4f6a8c-0b1d-4e3f-9a5b-7c9d1e3f5a7b",
                      "title": "Phalaenopsis flower-stalk node initiation",
                      "description": "Surface-sterilise nodes from a healthy flower stalk and place them onto initiation medium.",
                      "category": "tissue-culture",
                      "status": "active",
                      "level": "intermediate",
                      "tags": [
                        "initiation",
                        "orchid"
                      ],
                      "equipment": [
                        "laminar flow hood",
                        "autoclave"
                      ],
                      "estimatedTimeMinutes": 45,
                      "durationHours": null,
                      "currentVersion": 3,
                      "updatedAt": "2026-09-18T11:04:52.000Z",
                      "createdAt": "2026-06-02T08:30:00.000Z"
                    }
                  ],
                  "meta": {
                    "next_cursor": null
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "422": {
            "description": "`VALIDATION_ERROR` — Both `cursor` and `offset` were sent; use one.\n\n`INVALID_CURSOR` — The cursor is malformed, or came from a different list or different filters. Start again without it.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR",
                            "INVALID_CURSOR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "Both `cursor` and `offset` were sent; use one."
              },
              {
                "code": "INVALID_CURSOR",
                "when": "The cursor is malformed, or came from a different list or different filters. Start again without it."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`SOP_QUERY_FAILED` — The SOPs could not be read. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "SOP_QUERY_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "SOP_QUERY_FAILED",
                "when": "The SOPs could not be read. Retry later."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      }
    },
    "/api/v1/sops/{id}": {
      "get": {
        "operationId": "getSop",
        "tags": [
          "SOPs"
        ],
        "summary": "Get an SOP",
        "description": "One SOP and the version the lab works from right now, with its steps — what a bench station shows the person at the hood.\n\nOnly the version in force is returned, never a draft or a version waiting to take effect. When the SOP has no version in force, `version` is null: the procedure exists, but there is nothing approved to follow yet, and it cannot be run.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "read:sops"
        ],
        "x-idempotent": false,
        "x-cursor": false,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The record's id."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/Sop"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": {
                    "id": "2e4f6a8c-0b1d-4e3f-9a5b-7c9d1e3f5a7b",
                    "title": "Phalaenopsis flower-stalk node initiation",
                    "description": "Surface-sterilise nodes from a healthy flower stalk and place them onto initiation medium.",
                    "category": "tissue-culture",
                    "status": "active",
                    "level": "intermediate",
                    "tags": [
                      "initiation",
                      "orchid"
                    ],
                    "equipment": [
                      "laminar flow hood",
                      "autoclave"
                    ],
                    "estimatedTimeMinutes": 45,
                    "durationHours": null,
                    "currentVersion": 3,
                    "updatedAt": "2026-09-18T11:04:52.000Z",
                    "createdAt": "2026-06-02T08:30:00.000Z",
                    "version": {
                      "version": 2,
                      "lifecycleStatus": "effective",
                      "effectiveAt": "2026-08-12T00:00:00+00:00",
                      "approvedAt": "2026-08-11T16:20:00+00:00",
                      "steps": [
                        {
                          "id": "step-1",
                          "instruction": "Wipe the hood down and let it run for 15 minutes before starting.",
                          "estimatedMinutes": 15
                        },
                        {
                          "id": "step-2",
                          "instruction": "Cut single nodes from the flower stalk, leaving a short section either side.",
                          "notes": "Discard any node with a split or browned bract."
                        },
                        {
                          "id": "step-3",
                          "instruction": "Place each node onto initiation medium and label the vessel."
                        }
                      ],
                      "title": "Phalaenopsis flower-stalk node initiation",
                      "description": "Surface-sterilise nodes from a healthy flower stalk and place them onto initiation medium."
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "`NOT_FOUND` — No SOP with this id exists in the key's workspace, or `id` is not a well-formed id.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "NOT_FOUND"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "No SOP with this id exists in the key's workspace, or `id` is not a well-formed id."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`SOP_QUERY_FAILED` — The SOP could not be read. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "SOP_QUERY_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "SOP_QUERY_FAILED",
                "when": "The SOP could not be read. Retry later."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      }
    },
    "/api/v1/stages": {
      "get": {
        "operationId": "listStages",
        "tags": [
          "Stages"
        ],
        "summary": "List a stage history",
        "description": "Every stage one plant or explant has been through, newest first — where it is now and how it got there. Send exactly one of `plant_id` or `explant_id`.\n\nStage names are your lab's own, so expect the keys from your workspace's stage list rather than a fixed set.\n\nThe list is ordered by `entered_on`, which can be corrected after the fact: a stage whose date is changed while you page through can move between pages. Stages entered on the same day keep a fixed order.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "read:transfers"
        ],
        "x-idempotent": false,
        "x-cursor": true,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "name": "plant_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The plant whose stage history to read. Send exactly one of `plant_id` or `explant_id`."
          },
          {
            "name": "explant_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The explant whose stage history to read."
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Stage"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/PageMeta"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": [
                    {
                      "id": "6e5d4c3b-2a19-4807-96a5-b4c3d2e1f0a9",
                      "entity_type": "explant",
                      "entity_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
                      "stage": "multiplication",
                      "status": "active",
                      "entered_on": "2026-09-24T00:00:00+00:00",
                      "completed_at": null,
                      "room_id": null,
                      "notes": "Shoot clusters forming well on the Alocasia line.",
                      "created_at": "2026-09-24T08:41:17.000Z"
                    }
                  ],
                  "meta": {
                    "next_cursor": null
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "`NOT_FOUND` — The plant or explant is not in this workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "NOT_FOUND"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "The plant or explant is not in this workspace."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "422": {
            "description": "`VALIDATION_ERROR` — Both `cursor` and `offset` were sent; use one.\n\n`INVALID_CURSOR` — The cursor is malformed, or came from a different list or different filters. Start again without it.\n\n`VALIDATION_ERROR` — Neither or both of `plant_id` and `explant_id` were sent.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR",
                            "INVALID_CURSOR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "Both `cursor` and `offset` were sent; use one."
              },
              {
                "code": "INVALID_CURSOR",
                "when": "The cursor is malformed, or came from a different list or different filters. Start again without it."
              },
              {
                "code": "VALIDATION_ERROR",
                "when": "Neither or both of `plant_id` and `explant_id` were sent."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`STAGE_QUERY_FAILED` — The stage history could not be read. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "STAGE_QUERY_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "STAGE_QUERY_FAILED",
                "when": "The stage history could not be read. Retry later."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createStage",
        "tags": [
          "Stages"
        ],
        "summary": "Move to a new stage",
        "description": "Moves a plant or explant into a new stage in one call: the stage it is in now is marked `completed`, the new one is recorded, and it becomes the current stage. Send exactly one of `plant_id` or `explant_id`.\n\nEvery successful move answers with `meta` beside the stage: `meta.previous_stage_id` names the stage that was closed, so your system can mirror the change without reading the history back. Send an `Idempotency-Key` when you might retry: a repeated move would otherwise close the stage the first attempt opened.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "write:transfers"
        ],
        "x-idempotent": true,
        "x-cursor": false,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "plant_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "The plant this is for. Send exactly one of `plant_id` or `explant_id`."
                  },
                  "explant_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "The explant this is for. Send exactly one of `plant_id` or `explant_id`."
                  },
                  "stage": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 50,
                    "description": "The stage to move into, as a key from your lab's stage list — for example `multiplication` or `root_induction`. It is stored as sent and not checked against the list, so send a key your lab already uses."
                  },
                  "entered_on": {
                    "type": "string",
                    "description": "When it entered the stage: a date such as `2026-09-24`, or a full ISO 8601 timestamp. Defaults to today (UTC)."
                  },
                  "room_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "The growing room it is in for this stage. Must be a room in the workspace."
                  },
                  "notes": {
                    "type": "string",
                    "maxLength": 5000
                  }
                },
                "required": [
                  "stage"
                ]
              },
              "example": {
                "explant_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
                "stage": "multiplication",
                "entered_on": "2026-09-24",
                "notes": "Shoot clusters forming well on the Alocasia line."
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/Stage"
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "previous_stage_id": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "uuid",
                          "description": "The stage this one replaced, which is now `completed`. Null when there was no current stage."
                        },
                        "warning": {
                          "type": "string",
                          "description": "Sent instead of `previous_stage_id` when the stage was saved but could not be made the current one. The history is complete; set the current stage in xPlant."
                        }
                      }
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": {
                    "id": "6e5d4c3b-2a19-4807-96a5-b4c3d2e1f0a9",
                    "entity_type": "explant",
                    "entity_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
                    "stage": "multiplication",
                    "status": "active",
                    "entered_on": "2026-09-24T00:00:00+00:00",
                    "completed_at": null,
                    "room_id": null,
                    "notes": "Shoot clusters forming well on the Alocasia line.",
                    "created_at": "2026-09-24T08:41:17.000Z"
                  }
                }
              }
            },
            "headers": {
              "Idempotent-Replay": {
                "$ref": "#/components/headers/IdempotentReplay"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "400": {
            "description": "`VALIDATION_ERROR` — The request body is not valid JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "The request body is not valid JSON."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "`NOT_FOUND` — The plant or explant, or the growing room named in `room_id`, is not in this workspace. Nothing was changed.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "NOT_FOUND"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "The plant or explant, or the growing room named in `room_id`, is not in this workspace. Nothing was changed."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "409": {
            "description": "`IDEMPOTENCY_IN_FLIGHT` — A request with this `Idempotency-Key` is still being processed; retry after `Retry-After` seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "IDEMPOTENCY_IN_FLIGHT"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "IDEMPOTENCY_IN_FLIGHT",
                "when": "A request with this `Idempotency-Key` is still being processed; retry after `Retry-After` seconds."
              }
            ],
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "422": {
            "description": "`VALIDATION_ERROR` — The `Idempotency-Key` header is malformed.\n\n`VALIDATION_ERROR` — 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`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "The `Idempotency-Key` header is malformed."
              },
              {
                "code": "VALIDATION_ERROR",
                "when": "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`."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`STAGE_ADVANCE_FAILED` — The stage could not be saved. Retry with the same `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "STAGE_ADVANCE_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "STAGE_ADVANCE_FAILED",
                "when": "The stage could not be saved. Retry with the same `Idempotency-Key`."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      }
    },
    "/api/v1/tasks": {
      "get": {
        "operationId": "listTasks",
        "tags": [
          "Tasks"
        ],
        "summary": "List tasks",
        "description": "The workspace's tasks, earliest due first. Filter by board column or assignee; with neither, you get the whole queue a page at a time.\n\nA task's due date changes when it is rescheduled, so a task rescheduled while you page through the list can move between pages: it may appear twice, or not at all. Tasks due at the same moment keep a fixed order.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "read:tasks"
        ],
        "x-idempotent": false,
        "x-cursor": true,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "backlog",
                "todo",
                "in_progress",
                "waiting_blocked",
                "review",
                "done"
              ]
            },
            "description": "Only tasks in this board column."
          },
          {
            "name": "assigned_to",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Only tasks assigned to this workspace member (user id)."
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Task"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/PageMeta"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": [
                    {
                      "id": "5b0f6f3e-2c1a-4d8e-9f47-0a6c3e1b7d22",
                      "title": "Transfer the Alocasia line onto fresh medium",
                      "status": "todo",
                      "due_date": "2026-10-02T09:00:00.000Z",
                      "assigned_to": "0d7e4b9a-6f21-4c3e-8a5b-2e9f1c7d4a60",
                      "priority": "high",
                      "priority_rank": 1.5,
                      "priority_source": "auto",
                      "category": "transfer",
                      "created_at": "2026-09-25T14:12:03.000Z"
                    }
                  ],
                  "meta": {
                    "next_cursor": null
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "422": {
            "description": "`VALIDATION_ERROR` — Both `cursor` and `offset` were sent; use one.\n\n`INVALID_CURSOR` — The cursor is malformed, or came from a different list or different filters. Start again without it.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR",
                            "INVALID_CURSOR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "Both `cursor` and `offset` were sent; use one."
              },
              {
                "code": "INVALID_CURSOR",
                "when": "The cursor is malformed, or came from a different list or different filters. Start again without it."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`TASK_QUERY_FAILED` — The tasks could not be read. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "TASK_QUERY_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "TASK_QUERY_FAILED",
                "when": "The tasks could not be read. Retry later."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createTask",
        "tags": [
          "Tasks"
        ],
        "summary": "Create a task",
        "description": "Adds a task to the workspace. Set `priority`, `priority_rank` and `assigned_to` in the same call so a scheduler can place work in one request. Order set this way counts as automatic, so a later hand-reorder in the app takes precedence over your next sync.\n\nLink the task to the plant or explant it is about with `entity_type` and `entity_id` — send both or neither.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "write:tasks"
        ],
        "x-idempotent": true,
        "x-cursor": false,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 300
                  },
                  "due_date": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "is_all_day": {
                    "type": "boolean"
                  },
                  "category": {
                    "type": "string",
                    "enum": [
                      "media_prep",
                      "transfer",
                      "contamination",
                      "subculture",
                      "sop_review",
                      "acclimation",
                      "cleaning",
                      "monitoring",
                      "other"
                    ],
                    "default": "media_prep"
                  },
                  "notes": {
                    "type": "string",
                    "maxLength": 5000
                  },
                  "priority": {
                    "type": "string",
                    "enum": [
                      "low",
                      "medium",
                      "high",
                      "urgent"
                    ]
                  },
                  "priority_rank": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "workflow_status": {
                    "type": "string",
                    "enum": [
                      "backlog",
                      "todo",
                      "in_progress",
                      "waiting_blocked",
                      "review",
                      "done"
                    ],
                    "default": "todo"
                  },
                  "assigned_to": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "genus": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 100
                  },
                  "entity_type": {
                    "type": "string",
                    "enum": [
                      "plant",
                      "explant"
                    ]
                  },
                  "entity_id": {
                    "type": "string",
                    "format": "uuid"
                  }
                },
                "required": [
                  "title"
                ]
              },
              "example": {
                "title": "Transfer the Alocasia line onto fresh medium",
                "due_date": "2026-10-02T09:00:00.000Z",
                "category": "transfer",
                "priority": "high",
                "priority_rank": 1.5,
                "assigned_to": "0d7e4b9a-6f21-4c3e-8a5b-2e9f1c7d4a60",
                "genus": "Alocasia"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/Task"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": {
                    "id": "5b0f6f3e-2c1a-4d8e-9f47-0a6c3e1b7d22",
                    "title": "Transfer the Alocasia line onto fresh medium",
                    "status": "todo",
                    "due_date": "2026-10-02T09:00:00.000Z",
                    "assigned_to": "0d7e4b9a-6f21-4c3e-8a5b-2e9f1c7d4a60",
                    "priority": "high",
                    "priority_rank": 1.5,
                    "priority_source": "auto",
                    "category": "transfer",
                    "created_at": "2026-09-25T14:12:03.000Z"
                  }
                }
              }
            },
            "headers": {
              "Idempotent-Replay": {
                "$ref": "#/components/headers/IdempotentReplay"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "400": {
            "description": "`VALIDATION_ERROR` — The request body is not valid JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "The request body is not valid JSON."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "409": {
            "description": "`IDEMPOTENCY_IN_FLIGHT` — A request with this `Idempotency-Key` is still being processed; retry after `Retry-After` seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "IDEMPOTENCY_IN_FLIGHT"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "IDEMPOTENCY_IN_FLIGHT",
                "when": "A request with this `Idempotency-Key` is still being processed; retry after `Retry-After` seconds."
              }
            ],
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "422": {
            "description": "`VALIDATION_ERROR` — The `Idempotency-Key` header is malformed.\n\n`VALIDATION_ERROR` — A field failed validation. `error` names the first one, for example `title: title is required`.\n\n`VALIDATION_ERROR` — `assigned_to` is not an active member of the workspace, or the linked plant or explant is not in it.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "The `Idempotency-Key` header is malformed."
              },
              {
                "code": "VALIDATION_ERROR",
                "when": "A field failed validation. `error` names the first one, for example `title: title is required`."
              },
              {
                "code": "VALIDATION_ERROR",
                "when": "`assigned_to` is not an active member of the workspace, or the linked plant or explant is not in it."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`TASK_CREATE_FAILED` — The task could not be saved. Retry with the same `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "TASK_CREATE_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "TASK_CREATE_FAILED",
                "when": "The task could not be saved. Retry with the same `Idempotency-Key`."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      }
    },
    "/api/v1/tasks/{id}": {
      "get": {
        "operationId": "getTask",
        "tags": [
          "Tasks"
        ],
        "summary": "Get a task",
        "description": "One task, including the plant or explant it is linked to.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "read:tasks"
        ],
        "x-idempotent": false,
        "x-cursor": false,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The record's id."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/Task"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": {
                    "id": "5b0f6f3e-2c1a-4d8e-9f47-0a6c3e1b7d22",
                    "title": "Transfer the Alocasia line onto fresh medium",
                    "status": "todo",
                    "due_date": "2026-10-02T09:00:00.000Z",
                    "assigned_to": "0d7e4b9a-6f21-4c3e-8a5b-2e9f1c7d4a60",
                    "priority": "high",
                    "priority_rank": 1.5,
                    "priority_source": "auto",
                    "category": "transfer",
                    "created_at": "2026-09-25T14:12:03.000Z",
                    "entity_link": null
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "`NOT_FOUND` — No task with this id exists in the key's workspace, or `id` is not a well-formed id.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "NOT_FOUND"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "No task with this id exists in the key's workspace, or `id` is not a well-formed id."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`TASK_QUERY_FAILED` — The task could not be read. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "TASK_QUERY_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "TASK_QUERY_FAILED",
                "when": "The task could not be read. Retry later."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updateTask",
        "tags": [
          "Tasks"
        ],
        "summary": "Update a task",
        "description": "Changes only the fields you send. Move a task along the board with `workflow_status`; complete it with `\"done\"`.\n\nA task someone placed by hand keeps its position through automated writes. Send `release: true` to hand it back to automatic ordering. Send `clear_entity_link: true` to unlink it from its plant or explant.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "write:tasks"
        ],
        "x-idempotent": false,
        "x-cursor": false,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The record's id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 300
                  },
                  "due_date": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "is_all_day": {
                    "type": "boolean"
                  },
                  "category": {
                    "type": "string",
                    "enum": [
                      "media_prep",
                      "transfer",
                      "contamination",
                      "subculture",
                      "sop_review",
                      "acclimation",
                      "cleaning",
                      "monitoring",
                      "other"
                    ]
                  },
                  "notes": {
                    "type": "string",
                    "maxLength": 5000
                  },
                  "priority": {
                    "type": "string",
                    "enum": [
                      "low",
                      "medium",
                      "high",
                      "urgent"
                    ]
                  },
                  "priority_rank": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "release": {
                    "type": "boolean"
                  },
                  "workflow_status": {
                    "type": "string",
                    "enum": [
                      "backlog",
                      "todo",
                      "in_progress",
                      "waiting_blocked",
                      "review",
                      "done"
                    ]
                  },
                  "assigned_to": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "genus": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "minLength": 1,
                    "maxLength": 100
                  },
                  "entity_type": {
                    "type": "string",
                    "enum": [
                      "plant",
                      "explant"
                    ]
                  },
                  "entity_id": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "clear_entity_link": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "workflow_status": "done"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/Task"
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "priority_write": {
                          "type": "object",
                          "properties": {
                            "applied": {
                              "type": "boolean",
                              "description": "Whether the requested ordering change was written."
                            },
                            "reason": {
                              "type": "string",
                              "enum": [
                                "manual_override"
                              ],
                              "description": "Present only when the change was declined."
                            },
                            "priority_source": {
                              "type": "string",
                              "enum": [
                                "default",
                                "auto",
                                "manual"
                              ],
                              "description": "Where the order stands now."
                            },
                            "message": {
                              "type": "string",
                              "description": "A plain-language explanation, safe to show in a sync log."
                            }
                          },
                          "required": [
                            "applied",
                            "priority_source",
                            "message"
                          ],
                          "description": "Sent when the update asked to change the task's order: whether it was applied, and why not if it was declined. Left out otherwise."
                        }
                      },
                      "required": [
                        "priority_write"
                      ]
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": {
                    "id": "5b0f6f3e-2c1a-4d8e-9f47-0a6c3e1b7d22",
                    "title": "Transfer the Alocasia line onto fresh medium",
                    "status": "done",
                    "due_date": "2026-10-02T09:00:00.000Z",
                    "assigned_to": "0d7e4b9a-6f21-4c3e-8a5b-2e9f1c7d4a60",
                    "priority": "high",
                    "priority_rank": 1.5,
                    "priority_source": "auto",
                    "category": "transfer",
                    "created_at": "2026-09-25T14:12:03.000Z",
                    "entity_link": null
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "400": {
            "description": "`VALIDATION_ERROR` — The request body is not valid JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "The request body is not valid JSON."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "`NOT_FOUND` — No task with this id exists in the key's workspace, or `id` is not a well-formed id.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "NOT_FOUND"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "No task with this id exists in the key's workspace, or `id` is not a well-formed id."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "422": {
            "description": "`VALIDATION_ERROR` — A field failed validation. `error` names the first one, for example `title: title is required`.\n\n`VALIDATION_ERROR` — `assigned_to` is not an active member of the workspace, or the linked plant or explant is not in it.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "A field failed validation. `error` names the first one, for example `title: title is required`."
              },
              {
                "code": "VALIDATION_ERROR",
                "when": "`assigned_to` is not an active member of the workspace, or the linked plant or explant is not in it."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`TASK_UPDATE_FAILED` — The change could not be saved. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "TASK_UPDATE_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "TASK_UPDATE_FAILED",
                "when": "The change could not be saved. Retry later."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      }
    },
    "/api/v1/tasks/demand": {
      "get": {
        "operationId": "listDemandSignals",
        "tags": [
          "Demand signals"
        ],
        "summary": "List demand signals",
        "description": "Demand signals pushed for the workspace, newest first, a page at a time. Add `genus` to see one genus, and `current=true` as well to get its current demand as a single reading; that answer is never paged, and `meta.next_cursor` is always null.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "read:tasks"
        ],
        "x-idempotent": false,
        "x-cursor": true,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "name": "genus",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Only signals for this genus."
          },
          {
            "name": "current",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "true"
              ]
            },
            "description": "With `genus`, return the genus's current demand as a single reading instead of its history."
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/DemandSignal"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/PageMeta"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": [
                    {
                      "id": "c41e8a07-93d2-4b6f-8e15-7a2d0f9b3c58",
                      "genus": "Alocasia",
                      "source_type": "tissue",
                      "demand_score": 42,
                      "source": "storefront",
                      "observed_at": "2026-09-24T00:00:00.000Z",
                      "created_at": "2026-09-24T00:05:11.000Z"
                    }
                  ],
                  "meta": {
                    "next_cursor": null
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "422": {
            "description": "`VALIDATION_ERROR` — Both `cursor` and `offset` were sent; use one.\n\n`INVALID_CURSOR` — The cursor is malformed, or came from a different list or different filters. Start again without it.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR",
                            "INVALID_CURSOR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "Both `cursor` and `offset` were sent; use one."
              },
              {
                "code": "INVALID_CURSOR",
                "when": "The cursor is malformed, or came from a different list or different filters. Start again without it."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`DEMAND_QUERY_FAILED` — The signals could not be read. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "DEMAND_QUERY_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "DEMAND_QUERY_FAILED",
                "when": "The signals could not be read. Retry later."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createDemandSignal",
        "tags": [
          "Demand signals"
        ],
        "summary": "Push a demand signal",
        "description": "Records how much demand there is for a genus — orders, sales velocity, a forecast — from any source you run. xPlant uses the latest signal per genus when it scores the task queue. Send one whenever your number changes; history is kept.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "write:demand"
        ],
        "x-idempotent": true,
        "x-cursor": false,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "genus": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 100
                  },
                  "source_type": {
                    "type": "string",
                    "maxLength": 50
                  },
                  "demand_score": {
                    "type": "number",
                    "minimum": 0
                  },
                  "source": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 100
                  },
                  "observed_at": {
                    "type": "string",
                    "format": "date-time"
                  }
                },
                "required": [
                  "genus",
                  "demand_score",
                  "source"
                ]
              },
              "example": {
                "genus": "Alocasia",
                "source_type": "tissue",
                "demand_score": 42,
                "source": "storefront",
                "observed_at": "2026-09-24T00:00:00.000Z"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/DemandSignal"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": {
                    "id": "c41e8a07-93d2-4b6f-8e15-7a2d0f9b3c58",
                    "genus": "Alocasia",
                    "source_type": "tissue",
                    "demand_score": 42,
                    "source": "storefront",
                    "observed_at": "2026-09-24T00:00:00.000Z",
                    "created_at": "2026-09-24T00:05:11.000Z"
                  }
                }
              }
            },
            "headers": {
              "Idempotent-Replay": {
                "$ref": "#/components/headers/IdempotentReplay"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "400": {
            "description": "`VALIDATION_ERROR` — The request body is not valid JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "The request body is not valid JSON."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "409": {
            "description": "`IDEMPOTENCY_IN_FLIGHT` — A request with this `Idempotency-Key` is still being processed; retry after `Retry-After` seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "IDEMPOTENCY_IN_FLIGHT"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "IDEMPOTENCY_IN_FLIGHT",
                "when": "A request with this `Idempotency-Key` is still being processed; retry after `Retry-After` seconds."
              }
            ],
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "422": {
            "description": "`VALIDATION_ERROR` — The `Idempotency-Key` header is malformed.\n\n`VALIDATION_ERROR` — A field failed validation. `error` names the first one, for example `title: title is required`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "The `Idempotency-Key` header is malformed."
              },
              {
                "code": "VALIDATION_ERROR",
                "when": "A field failed validation. `error` names the first one, for example `title: title is required`."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`DEMAND_CREATE_FAILED` — The signal could not be saved. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "DEMAND_CREATE_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "DEMAND_CREATE_FAILED",
                "when": "The signal could not be saved. Retry later."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      }
    },
    "/api/v1/transfers": {
      "get": {
        "operationId": "listTransfers",
        "tags": [
          "Transfers"
        ],
        "summary": "List transfers",
        "description": "The transfer history of one plant or explant, newest first: every move onto fresh media, where it went and which subculture it was. Send exactly one of `plant_id` or `explant_id`.\n\nEach transfer carries your lab's own fields in `custom_fields`.\n\nThe list is ordered by `transfer_date`, which changes when a planned transfer is postponed or edited: a transfer changed while you page through can move between pages. Transfers on the same date keep a fixed order.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "read:transfers"
        ],
        "x-idempotent": false,
        "x-cursor": true,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "name": "plant_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The plant whose transfers to list. Send exactly one of `plant_id` or `explant_id`."
          },
          {
            "name": "explant_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The explant whose transfers to list."
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Transfer"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/PageMeta"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": [
                    {
                      "id": "c7d8e9f0-a1b2-4c3d-8e4f-5a6b7c8d9e0f",
                      "entity_type": "explant",
                      "entity_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
                      "transfer_date": "2026-09-24T00:00:00+00:00",
                      "transfer_cycle": 5,
                      "from_location": "Growth room 1, shelf B2",
                      "to_location": "Growth room 1, shelf C1",
                      "status": "active",
                      "notes": "LINE-0412 onto fresh multiplication medium. Two jars browning at the base, set aside.",
                      "created_at": "2026-09-24T09:12:44.000Z",
                      "custom_fields": {
                        "vessel_lot": "LOT-2291",
                        "hood": "Hood 2"
                      }
                    }
                  ],
                  "meta": {
                    "next_cursor": null
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "`NOT_FOUND` — The plant or explant is not in this workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "NOT_FOUND"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "The plant or explant is not in this workspace."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "422": {
            "description": "`VALIDATION_ERROR` — Both `cursor` and `offset` were sent; use one.\n\n`INVALID_CURSOR` — The cursor is malformed, or came from a different list or different filters. Start again without it.\n\n`VALIDATION_ERROR` — Neither or both of `plant_id` and `explant_id` were sent.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR",
                            "INVALID_CURSOR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "Both `cursor` and `offset` were sent; use one."
              },
              {
                "code": "INVALID_CURSOR",
                "when": "The cursor is malformed, or came from a different list or different filters. Start again without it."
              },
              {
                "code": "VALIDATION_ERROR",
                "when": "Neither or both of `plant_id` and `explant_id` were sent."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`TRANSFER_QUERY_FAILED` — The transfers could not be read. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "TRANSFER_QUERY_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "TRANSFER_QUERY_FAILED",
                "when": "The transfers could not be read. Retry later."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createTransfer",
        "tags": [
          "Transfers"
        ],
        "summary": "Record a transfer",
        "description": "Records a transfer — a subculture onto fresh media — for one plant or explant. Send exactly one of `plant_id` or `explant_id`. Everything else is optional: the date defaults to today and the cycle to the next one in the record's history.\n\nFill in the fields your lab has added to transfers with `custom_fields`. They are checked against your lab's field list exactly as the transfer form in xPlant checks them, so a key the lab has not defined, or a value of the wrong kind, is refused before anything is saved.\n\nSend an `Idempotency-Key` when you might retry: a repeated transfer would also advance the cycle twice.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "write:transfers"
        ],
        "x-idempotent": true,
        "x-cursor": false,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "plant_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "The plant this is for. Send exactly one of `plant_id` or `explant_id`."
                  },
                  "explant_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "The explant this is for. Send exactly one of `plant_id` or `explant_id`."
                  },
                  "transfer_date": {
                    "type": "string",
                    "description": "When the transfer was done: a date such as `2026-09-24`, or a full ISO 8601 timestamp. Defaults to today (UTC)."
                  },
                  "from_location": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "Where the culture was before the transfer, in your lab's own words — a room, shelf or hood."
                  },
                  "to_location": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "Where the culture went."
                  },
                  "transfer_cycle": {
                    "type": "integer",
                    "minimum": 1,
                    "description": "Which subculture this is for the plant or explant — 1 for the first. Defaults to one more than the highest cycle already recorded for it."
                  },
                  "notes": {
                    "type": "string",
                    "maxLength": 5000
                  },
                  "custom_fields": {
                    "type": [
                      "object",
                      "null"
                    ],
                    "additionalProperties": {
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "number"
                        },
                        {
                          "type": "boolean"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "description": "Values for the fields your lab has added to transfers, keyed by field key — for example `{ \"vessel_lot\": \"LOT-2291\" }`. Each value must suit its field: text, a number, `true` or `false`, a date (stored as `YYYY-MM-DD`), or one of a list field's options. `null` leaves a field empty. A key your lab has not defined is refused, and the values may total at most 10,000 bytes. Explant transfers only: a plant transfer takes no custom fields."
                  }
                }
              },
              "example": {
                "explant_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
                "transfer_date": "2026-09-24",
                "from_location": "Growth room 1, shelf B2",
                "to_location": "Growth room 1, shelf C1",
                "notes": "LINE-0412 onto fresh multiplication medium. Two jars browning at the base, set aside.",
                "custom_fields": {
                  "vessel_lot": "LOT-2291",
                  "hood": "Hood 2"
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/Transfer"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": {
                    "id": "c7d8e9f0-a1b2-4c3d-8e4f-5a6b7c8d9e0f",
                    "entity_type": "explant",
                    "entity_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
                    "transfer_date": "2026-09-24T00:00:00+00:00",
                    "transfer_cycle": 5,
                    "from_location": "Growth room 1, shelf B2",
                    "to_location": "Growth room 1, shelf C1",
                    "status": "active",
                    "notes": "LINE-0412 onto fresh multiplication medium. Two jars browning at the base, set aside.",
                    "created_at": "2026-09-24T09:12:44.000Z",
                    "custom_fields": {
                      "vessel_lot": "LOT-2291",
                      "hood": "Hood 2"
                    }
                  }
                }
              }
            },
            "headers": {
              "Idempotent-Replay": {
                "$ref": "#/components/headers/IdempotentReplay"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "400": {
            "description": "`VALIDATION_ERROR` — The request body is not valid JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "The request body is not valid JSON."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "`NOT_FOUND` — The plant or explant is not in this workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "NOT_FOUND"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "The plant or explant is not in this workspace."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "409": {
            "description": "`IDEMPOTENCY_IN_FLIGHT` — A request with this `Idempotency-Key` is still being processed; retry after `Retry-After` seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "IDEMPOTENCY_IN_FLIGHT"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "IDEMPOTENCY_IN_FLIGHT",
                "when": "A request with this `Idempotency-Key` is still being processed; retry after `Retry-After` seconds."
              }
            ],
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "422": {
            "description": "`VALIDATION_ERROR` — The `Idempotency-Key` header is malformed.\n\n`VALIDATION_ERROR` — 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`.\n\n`VALIDATION_ERROR` — `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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "VALIDATION_ERROR"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "VALIDATION_ERROR",
                "when": "The `Idempotency-Key` header is malformed."
              },
              {
                "code": "VALIDATION_ERROR",
                "when": "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`."
              },
              {
                "code": "VALIDATION_ERROR",
                "when": "`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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`TRANSFER_CREATE_FAILED` — The transfer could not be saved. Retry with the same `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "TRANSFER_CREATE_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "TRANSFER_CREATE_FAILED",
                "when": "The transfer could not be saved. Retry with the same `Idempotency-Key`."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      }
    },
    "/api/v1/workspaces": {
      "get": {
        "operationId": "listWorkspaces",
        "tags": [
          "Account"
        ],
        "summary": "List workspaces",
        "description": "The lab this key acts in, with its name — so an integration can show which lab it is writing to, and stop if it has been pointed at the wrong one.\n\nA key belongs to one workspace, so the list has exactly one entry even when the person who created the key belongs to several labs. It is a list so that your code does not change shape if that ever changes. Takes no paging parameters.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "read:workspace"
        ],
        "x-idempotent": false,
        "x-cursor": false,
        "x-device-token": false,
        "x-documented": true,
        "parameters": [],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Workspace"
                      }
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "data": [
                    {
                      "id": "8c2d4f6a-1b3e-4a5c-9d7e-0f1a2b3c4d5e",
                      "name": "Riverside Propagation Lab",
                      "type": "lab"
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. `code`: `UNAUTHORIZED`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "UNAUTHORIZED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "`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.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "PAID_PLAN_REQUIRED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAID_PLAN_REQUIRED",
                "when": "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."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "`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.\n\n`DEVICE_TOKEN_NOT_ACCEPTED` — A device token was sent; this operation needs a workspace API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "FORBIDDEN",
                            "DEVICE_TOKEN_NOT_ACCEPTED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "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."
              },
              {
                "code": "DEVICE_TOKEN_NOT_ACCEPTED",
                "when": "A device token was sent; this operation needs a workspace API key."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED` — too many requests for this key, device token or workspace. Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "RATE_LIMIT_EXCEEDED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "`WORKSPACE_QUERY_FAILED` — The workspace could not be read. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "WORKSPACE_QUERY_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "x-error-codes": [
              {
                "code": "WORKSPACE_QUERY_FAILED",
                "when": "The workspace could not be read. Retry later."
              }
            ],
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "xpk_live_…",
        "description": "A workspace API key, created in xPlant under Settings → Integrations → API Keys. It carries only the scopes granted to it and acts on one workspace."
      },
      "deviceToken": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "xpd_live_…",
        "description": "A device token, minted for one registered device. It can only post that device's sensor readings, events and heartbeat — never read lab records."
      }
    },
    "parameters": {
      "Limit": {
        "name": "limit",
        "in": "query",
        "required": false,
        "description": "Page size. Values above 200 are capped at 200.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 200,
          "default": 50
        }
      },
      "Offset": {
        "name": "offset",
        "in": "query",
        "required": false,
        "description": "Number of records to skip. Prefer `cursor` where a list offers it: an offset shifts when records are added ahead of it.",
        "schema": {
          "type": "integer",
          "minimum": 0,
          "default": 0
        }
      },
      "Cursor": {
        "name": "cursor",
        "in": "query",
        "required": false,
        "description": "Continue from the previous page: pass its `meta.next_cursor` unchanged, with the same filters. Treat it as opaque. Not combinable with `offset`.",
        "schema": {
          "type": "string",
          "maxLength": 2048
        }
      },
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "Any unique string you choose per logical write. A retry carrying the same key within 24 hours returns the first result instead of writing again. Scoped to your API key and this operation.",
        "schema": {
          "type": "string",
          "minLength": 8,
          "maxLength": 255,
          "pattern": "^[A-Za-z0-9._:~-]+$"
        }
      }
    },
    "headers": {
      "RequestId": {
        "description": "Identifies this request. Include it when you contact support.",
        "schema": {
          "type": "string"
        }
      },
      "RetryAfter": {
        "description": "Whole seconds to wait before retrying.",
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      },
      "IdempotentReplay": {
        "description": "`true` when this response is a replay of an earlier request with the same `Idempotency-Key`.",
        "schema": {
          "type": "string",
          "enum": [
            "true"
          ]
        }
      }
    },
    "schemas": {
      "PageMeta": {
        "type": "object",
        "required": [
          "next_cursor"
        ],
        "properties": {
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Pass as `cursor` to fetch the next page. `null` means this is the last page."
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "ok",
          "data",
          "error",
          "code"
        ],
        "properties": {
          "ok": {
            "const": false
          },
          "data": {
            "type": "null"
          },
          "error": {
            "type": "string",
            "description": "What went wrong, for people. May be reworded."
          },
          "code": {
            "type": "string",
            "description": "Stable machine-readable code. Branch on this."
          }
        }
      },
      "Asset": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "target": {
            "type": "string",
            "enum": [
              "plant",
              "explant",
              "contamination",
              "sop"
            ],
            "description": "The kind of record the file is attached to."
          },
          "target_id": {
            "type": "string",
            "format": "uuid",
            "description": "The id of that record."
          },
          "kind": {
            "type": "string",
            "description": "What the file is: `photo`, `video`, `annotation`, `document`, `diagram`, `scan`, `audio`, `other`. Images attached through the API are `photo`."
          },
          "file_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "The file's name as xPlant shows it."
          },
          "content_type": {
            "type": [
              "string",
              "null"
            ],
            "description": "The file's media type, for example `image/jpeg`, `image/png`."
          },
          "caption": {
            "type": [
              "string",
              "null"
            ],
            "description": "The note stored with the file."
          },
          "captured_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "When the photo was taken, where that was recorded. ISO 8601."
          },
          "uploaded_by": {
            "type": [
              "string",
              "null"
            ],
            "description": "User id of the workspace member who added the file."
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "When the file was added. ISO 8601."
          },
          "view_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "A link to the file itself, valid for 15 minutes from this response. Fetch the asset again for a fresh link, and never store one. `null` when the file cannot be linked."
          },
          "view_url_expires_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "When `view_url` stops working. ISO 8601."
          }
        },
        "required": [
          "id",
          "target",
          "target_id",
          "kind",
          "file_name",
          "content_type",
          "caption",
          "captured_at",
          "uploaded_by",
          "created_at",
          "view_url",
          "view_url_expires_at"
        ]
      },
      "ChangeEvent": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The event's id. Events are never edited, so an id you already hold is the same event."
          },
          "entity_type": {
            "type": "string",
            "enum": [
              "plant",
              "explant"
            ],
            "description": "Whether the event is about a plant or an explant."
          },
          "entity_id": {
            "type": "string",
            "format": "uuid",
            "description": "The plant's or explant's id."
          },
          "stage_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "The stage the event was recorded against, when there was one."
          },
          "event_type": {
            "type": "string",
            "description": "What happened — for example `transfer`, `stage_change`, `contamination`, `observation` or `label_print`. New kinds can appear; treat one you do not recognise as informational."
          },
          "event_time": {
            "type": "string",
            "description": "When it happened in the lab, as an ISO 8601 timestamp."
          },
          "recorded_by": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "The workspace member who recorded it, when known."
          },
          "payload": {
            "description": "The event's details, as a JSON value whose fields depend on `event_type` — a transfer, for example, carries its cycle, locations and vessel counts. Can be null."
          },
          "created_at": {
            "type": "string",
            "description": "When xPlant recorded the event. Pass the newest one you hold back as `since`."
          }
        },
        "required": [
          "id",
          "entity_type",
          "entity_id",
          "stage_id",
          "event_type",
          "event_time",
          "recorded_by",
          "payload",
          "created_at"
        ]
      },
      "Comment": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "entity_type": {
            "type": "string",
            "enum": [
              "plant",
              "explant",
              "contamination",
              "task",
              "media_recipe",
              "sop"
            ],
            "description": "The kind of record the comment is on."
          },
          "entity_id": {
            "type": "string",
            "format": "uuid",
            "description": "The record the comment is on."
          },
          "parent_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "The comment this one replies to."
          },
          "body": {
            "type": "string",
            "description": "The comment in Markdown, exactly as written — mentions and record links included as the `@name` and `#type:Label` text the author typed."
          },
          "status": {
            "type": "string",
            "description": "`active`; `edited` once changed after posting; or `deleted`, which keeps the comment's place in the thread but not its text."
          },
          "is_pinned": {
            "type": "boolean",
            "description": "Pinned to the top of the record's discussion."
          },
          "author": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid",
                "description": "User id of the author."
              },
              "name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The name shown beside the comment in the app, or null when there is none to show."
              }
            },
            "required": [
              "id",
              "name"
            ]
          },
          "mentioned_user_ids": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Workspace members the comment named. Each was notified."
          },
          "references": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "entity_type": {
                  "type": "string",
                  "enum": [
                    "plant",
                    "explant",
                    "contamination",
                    "task",
                    "media_recipe",
                    "sop"
                  ]
                },
                "entity_id": {
                  "type": "string",
                  "format": "uuid"
                },
                "label": {
                  "type": "string",
                  "description": "The name the body links from."
                }
              },
              "required": [
                "entity_type",
                "entity_id",
                "label"
              ]
            },
            "description": "Records in the workspace the body links to. A record removed since is left out."
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO 8601."
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO 8601."
          },
          "edited_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "When the text was last changed. ISO 8601."
          }
        },
        "required": [
          "id",
          "entity_type",
          "entity_id",
          "parent_id",
          "body",
          "status",
          "is_pinned",
          "author",
          "mentioned_user_ids",
          "references",
          "created_at",
          "updated_at",
          "edited_at"
        ]
      },
      "Contamination": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "workspace_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "The workspace the log belongs to."
          },
          "type": {
            "type": [
              "string",
              "null"
            ],
            "description": "What was seen: `mold`, `bacteria`, `hyperhydricity`, `phenolic`, `algae`, `yeast`, `endophytic`, `viral`, `fungal`, `physiological`, `contaminated_media`, `damage`, `insect`, `other`."
          },
          "type_other": {
            "type": [
              "string",
              "null"
            ],
            "description": "The contamination in the logger's own words, when `type` is `other`."
          },
          "issue": {
            "type": "string",
            "description": "A short summary of what was seen."
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "notes": {
            "type": [
              "string",
              "null"
            ]
          },
          "severity": {
            "type": "string",
            "description": "How serious it is: `very low`, `low`, `medium`, `high`, `critical`."
          },
          "status": {
            "type": "string",
            "description": "Where it stands: `active`, `resolved`, `quarantined`, `archived`, `under investigation`."
          },
          "observed_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "When it was seen. ISO 8601."
          },
          "resolved_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "When it was marked resolved. ISO 8601."
          },
          "vessels_affected": {
            "type": [
              "integer",
              "null"
            ],
            "description": "How many vessels it reached. Null when nobody counted, which is not the same as none."
          },
          "plants_affected": {
            "type": [
              "integer",
              "null"
            ],
            "description": "How many plants it reached. Null when nobody counted, which is not the same as none."
          },
          "affected_vessel_markings": {
            "type": [
              "string",
              "null"
            ],
            "description": "Which vessels, as written on them."
          },
          "custom_fields": {
            "type": "object",
            "additionalProperties": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "number"
                },
                {
                  "type": "boolean"
                },
                {
                  "type": "null"
                }
              ]
            },
            "description": "Your lab's own fields on this contamination, keyed by field key. Always an object: `{}` when none were filled in."
          },
          "plant_ids": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The plants the log is linked to."
          },
          "explant_ids": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The explants the log is linked to."
          },
          "logged_by": {
            "type": "string",
            "format": "uuid",
            "description": "User id of the workspace member who logged it."
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "When it was logged. ISO 8601."
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "When it last changed. ISO 8601."
          }
        },
        "required": [
          "id",
          "workspace_id",
          "type",
          "type_other",
          "issue",
          "description",
          "notes",
          "severity",
          "status",
          "observed_at",
          "resolved_at",
          "vessels_affected",
          "plants_affected",
          "affected_vessel_markings",
          "custom_fields",
          "plant_ids",
          "explant_ids",
          "logged_by",
          "created_at",
          "updated_at"
        ]
      },
      "CultureLinePrice": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "plant_id": {
            "type": "string",
            "format": "uuid",
            "description": "The culture line this price is for (a plant id)."
          },
          "list_price": {
            "$ref": "#/components/schemas/Money"
          },
          "wholesale_price": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Money"
              },
              {
                "type": "null"
              }
            ]
          },
          "previous_list_price": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Money"
              },
              {
                "type": "null"
              }
            ],
            "description": "The list price before the latest change, in the currency it was set in. Null until the price has changed once."
          },
          "pricing_tier": {
            "type": [
              "string",
              "null"
            ],
            "description": "The line's pricing tier, as your lab names its tiers."
          },
          "tier_score": {
            "type": [
              "string",
              "null"
            ],
            "description": "The score the tier was derived from, as exact decimal text. Not money."
          },
          "price_source": {
            "type": "string",
            "description": "Who set the list price: `manual`, `store_adopted`, `store_follow`. `manual` is a person typing it, `store_adopted` a person accepting the price from your store, and `store_follow` an automatic update that follows your store's price."
          },
          "price_source_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "When the list price was last set that way, as an ISO 8601 timestamp."
          },
          "notes": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "When the culture line was first priced."
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "plant_id",
          "list_price",
          "wholesale_price",
          "previous_list_price",
          "pricing_tier",
          "tier_score",
          "price_source",
          "price_source_at",
          "notes",
          "created_at",
          "updated_at"
        ]
      },
      "CustomFieldValues": {
        "type": "object",
        "additionalProperties": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "number"
            },
            {
              "type": "boolean"
            },
            {
              "type": "null"
            }
          ]
        },
        "description": "The lab's own fields for this record, keyed by each field's key as set up in the lab's settings. Values are text, numbers, `true` or `false`, or dates written as `YYYY-MM-DD`; a field left blank is absent or `null`. Empty when the lab has set up no fields."
      },
      "DemandSignal": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The signal's id — or `current` on the reading `current=true` returns."
          },
          "genus": {
            "type": "string"
          },
          "source_type": {
            "type": [
              "string",
              "null"
            ]
          },
          "demand_score": {
            "type": "number"
          },
          "source": {
            "type": "string"
          },
          "observed_at": {
            "type": "string"
          },
          "created_at": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "genus",
          "source_type",
          "demand_score",
          "source",
          "observed_at",
          "created_at"
        ]
      },
      "Device": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "description": "One of `sensor`, `controller`, `gateway`."
          },
          "hardware": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "description": "`active` while the device is in service. `paused` or `retired` once someone in the lab takes it out of service, which refuses its readings and revokes its device tokens."
          },
          "firmware_version": {
            "type": [
              "string",
              "null"
            ]
          },
          "room_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "The growing room the device sits in, if one was set."
          },
          "last_seen_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "When the device was last heard from, as an ISO 8601 timestamp. A heartbeat updates it. Null if it has never been heard from."
          },
          "metadata": {
            "type": "object",
            "additionalProperties": {},
            "description": "The JSON object stored with the device when it was registered."
          },
          "created_at": {
            "type": "string"
          },
          "updated_at": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "name",
          "type",
          "hardware",
          "status",
          "firmware_version",
          "room_id",
          "last_seen_at",
          "metadata",
          "created_at",
          "updated_at"
        ]
      },
      "DeviceEvent": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "device_id": {
            "type": "string",
            "format": "uuid"
          },
          "team_id": {
            "type": "string",
            "format": "uuid",
            "description": "The id of the workspace the event belongs to."
          },
          "event_type": {
            "type": "string",
            "description": "One of `heartbeat`, `alert`, `firmware_update`, `config_change`, `error`, `other`."
          },
          "payload": {
            "type": "object",
            "additionalProperties": {}
          },
          "occurred_at": {
            "type": "string",
            "description": "When it happened, as an ISO 8601 timestamp."
          },
          "created_at": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "device_id",
          "team_id",
          "event_type",
          "payload",
          "occurred_at",
          "created_at"
        ]
      },
      "DeviceToken": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "deviceId": {
            "type": "string",
            "format": "uuid",
            "description": "The device the token belongs to."
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "The label it was given when it was minted."
          },
          "prefix": {
            "type": "string",
            "description": "The first characters of the token — enough to recognise it, never enough to use it."
          },
          "status": {
            "type": "string",
            "description": "`active`, or `revoked` once it can no longer be used."
          },
          "lastUsedAt": {
            "type": [
              "string",
              "null"
            ],
            "description": "When the token last authenticated a request, as an ISO 8601 timestamp. Null if never."
          },
          "revokedAt": {
            "type": [
              "string",
              "null"
            ],
            "description": "When the token was revoked, as an ISO 8601 timestamp. Null while it is active."
          },
          "createdAt": {
            "type": "string",
            "description": "When the token was minted."
          }
        },
        "required": [
          "id",
          "deviceId",
          "name",
          "prefix",
          "status",
          "lastUsedAt",
          "revokedAt",
          "createdAt"
        ]
      },
      "DeviceTokenCreated": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "deviceId": {
            "type": "string",
            "format": "uuid",
            "description": "The device the token belongs to."
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "The label it was given when it was minted."
          },
          "prefix": {
            "type": "string",
            "description": "The first characters of the token — enough to recognise it, never enough to use it."
          },
          "token": {
            "type": "string",
            "description": "The secret. Returned in this response only — store it on the device now, because it cannot be shown again."
          },
          "createdAt": {
            "type": "string",
            "description": "When the token was minted."
          }
        },
        "required": [
          "id",
          "deviceId",
          "name",
          "prefix",
          "token",
          "createdAt"
        ]
      },
      "Equipment": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "category": {
            "type": "string",
            "description": "The kind of equipment: `balance_scale`, `autoclave_pressure_cooker`, `laminar_flow_hood`, `still_air_box`, `incubator`, `light_rack`, `fridge_freezer`, `ph_ec_meter`, `microscope`, `label_printer`, `other`."
          },
          "status": {
            "type": "string",
            "description": "`active`, or `archived` once the lab has retired it."
          },
          "manufacturer": {
            "type": [
              "string",
              "null"
            ]
          },
          "model": {
            "type": [
              "string",
              "null"
            ]
          },
          "serial_number": {
            "type": [
              "string",
              "null"
            ]
          },
          "location": {
            "type": [
              "string",
              "null"
            ],
            "description": "Where it is kept, as the lab wrote it."
          },
          "purchase_date": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO 8601 date."
          },
          "vendor_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "A link to the product or supplier page."
          },
          "notes": {
            "type": [
              "string",
              "null"
            ]
          },
          "last_calibrated_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "When a calibration schedule was last completed, as an ISO 8601 timestamp."
          },
          "next_calibration_due_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "The date the next calibration is due (ISO 8601 date), or null when no calibration schedule is active."
          },
          "last_maintenance_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "When a maintenance schedule was last completed, as an ISO 8601 timestamp."
          },
          "next_maintenance_due_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "The date the next preventive maintenance is due (ISO 8601 date), or null when no maintenance schedule is active."
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "name",
          "category",
          "status",
          "manufacturer",
          "model",
          "serial_number",
          "location",
          "purchase_date",
          "vendor_url",
          "notes",
          "last_calibrated_at",
          "next_calibration_due_at",
          "last_maintenance_at",
          "next_maintenance_due_at",
          "created_at",
          "updated_at"
        ]
      },
      "EquipmentEvent": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "equipment_id": {
            "type": "string",
            "format": "uuid"
          },
          "kind": {
            "type": "string",
            "description": "`used` for a use; `calibration`, `preventive_maintenance` for a maintenance record."
          },
          "occurred_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "When it happened — the use, or the work — as an ISO 8601 timestamp in UTC."
          },
          "outcome": {
            "type": [
              "string",
              "null"
            ],
            "description": "Maintenance only: `pass`, `pass_after_adjustment`, `out_of_tolerance`, `fail`, `not_performed`."
          },
          "subject_type": {
            "type": [
              "string",
              "null"
            ],
            "description": "Uses only — what it was used on: `sop_log`, `media_batch`, `plant_transfer`, `explant_transfer`, `contamination_log`."
          },
          "subject_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "Uses only: the id of that record."
          },
          "subject_label": {
            "type": [
              "string",
              "null"
            ],
            "description": "Uses only: a readable name for that record."
          },
          "performed_by_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Maintenance only: who did the work, a person or a service company."
          },
          "provider": {
            "type": [
              "string",
              "null"
            ],
            "description": "Maintenance only: the service provider, if any."
          },
          "as_found_condition": {
            "type": [
              "string",
              "null"
            ],
            "description": "Maintenance only: the state the instrument was found in, before any adjustment."
          },
          "as_left_condition": {
            "type": [
              "string",
              "null"
            ],
            "description": "Maintenance only: the state it was left in."
          },
          "result_summary": {
            "type": [
              "string",
              "null"
            ]
          },
          "certificate_number": {
            "type": [
              "string",
              "null"
            ]
          },
          "certificate_url": {
            "type": [
              "string",
              "null"
            ]
          },
          "next_due_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "Maintenance only: the next due date the work set, as an ISO 8601 date."
          },
          "notes": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "equipment_id",
          "kind",
          "occurred_at",
          "outcome",
          "subject_type",
          "subject_id",
          "subject_label",
          "performed_by_name",
          "provider",
          "as_found_condition",
          "as_left_condition",
          "result_summary",
          "certificate_number",
          "certificate_url",
          "next_due_at",
          "notes"
        ]
      },
      "EquipmentEventReceipt": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The recorded event's id, as it appears in the equipment's history."
          },
          "equipmentId": {
            "type": "string",
            "format": "uuid"
          },
          "kind": {
            "type": "string",
            "description": "The kind that was recorded."
          },
          "recordedAt": {
            "type": "string",
            "description": "When the use or the work happened, as the event records it. ISO 8601 timestamp."
          }
        },
        "required": [
          "id",
          "equipmentId",
          "kind",
          "recordedAt"
        ]
      },
      "Explant": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "label": {
            "type": [
              "string",
              "null"
            ],
            "description": "The culture's label — the name the lab knows it by."
          },
          "external_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Your identifier for the culture: the `external_id` you gave it, otherwise its batch number, otherwise its label. Null when it has none of these."
          },
          "status": {
            "type": "string",
            "description": "Where the culture stands, usually one of `active`, `establishing`, `growing`, `needs_subculture`, `quarantined`, `senescing`, `discarded`, `retired`, `lost`."
          },
          "plant_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "The plant the explant was taken from, when one is recorded."
          },
          "workspace_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "The workspace the explant belongs to."
          },
          "initial_count": {
            "type": [
              "integer",
              "null"
            ],
            "description": "The count recorded when the culture was started."
          },
          "current_count": {
            "type": [
              "integer",
              "null"
            ],
            "description": "The culture's count as last recorded."
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO 8601 timestamp."
          },
          "custom_fields": {
            "$ref": "#/components/schemas/CustomFieldValues"
          }
        },
        "required": [
          "id",
          "label",
          "external_id",
          "status",
          "plant_id",
          "workspace_id",
          "initial_count",
          "current_count",
          "created_at",
          "custom_fields"
        ]
      },
      "Heartbeat": {
        "type": "object",
        "properties": {
          "received_at": {
            "type": "string",
            "description": "When the heartbeat was recorded, as an ISO 8601 timestamp."
          }
        },
        "required": [
          "received_at"
        ]
      },
      "LabelContentItem": {
        "type": "object",
        "properties": {
          "item_id": {
            "type": "string",
            "format": "uuid",
            "description": "The stored item's id."
          },
          "record_type": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "plant",
              "explant",
              null
            ],
            "description": "What the item is, when it is linked to a plant or explant."
          },
          "record_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "The plant's or explant's id, when the item is linked to one."
          },
          "display_name": {
            "type": "string",
            "description": "A name to show for the item."
          },
          "url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Where the linked record opens in xPlant, as a path on the xPlant web address. Null when there is no linked record."
          }
        },
        "required": [
          "item_id",
          "record_type",
          "record_id",
          "display_name",
          "url"
        ]
      },
      "LabelResolution": {
        "type": "object",
        "properties": {
          "barcode": {
            "type": "string",
            "description": "The code that was looked up, trimmed."
          },
          "record_type": {
            "type": "string",
            "enum": [
              "plant",
              "explant",
              "container"
            ],
            "description": "What the code identifies. A `container` is a labelled location — a rack, shelf or box — and resolves to the cultures stored there."
          },
          "record_id": {
            "type": "string",
            "format": "uuid",
            "description": "The plant's, explant's or container's id."
          },
          "display_name": {
            "type": "string",
            "description": "A name to show the person scanning: the plant's common name or species, the explant's label or batch number, or the container's label."
          },
          "url": {
            "type": "string",
            "description": "Where the record opens in xPlant, as a path on the xPlant web address — for example `/dashboard/explants/<id>`."
          },
          "contents": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LabelContentItem"
            },
            "description": "For a container only: the items stored at that location."
          }
        },
        "required": [
          "barcode",
          "record_type",
          "record_id",
          "display_name",
          "url"
        ]
      },
      "LabelScan": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "barcode": {
            "type": "string",
            "description": "The code as the scanner read it."
          },
          "resolved": {
            "type": "boolean",
            "description": "True when the scan was recorded against a plant or explant — that is, when `plant_id` or `explant_id` was sent."
          },
          "plantId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "explantId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "context": {
            "type": [
              "string",
              "null"
            ],
            "description": "Where the scan happened, as it was described."
          },
          "scannedAt": {
            "type": "string",
            "description": "When the scan happened: the `scanned_at` that was sent, or when xPlant received it."
          }
        },
        "required": [
          "id",
          "barcode",
          "resolved",
          "plantId",
          "explantId",
          "context",
          "scannedAt"
        ]
      },
      "Me": {
        "type": "object",
        "properties": {
          "key": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid",
                "description": "The key's id."
              },
              "name": {
                "type": "string",
                "description": "The name the key was given when it was created."
              },
              "prefix": {
                "type": "string",
                "description": "The visible start of the key, as xPlant's key list shows it. Never the whole key."
              },
              "environment": {
                "type": "string",
                "description": "`production` for an `xpk_live_` key, `development` for an `xpk_dev_` key. Both act on the same workspace."
              },
              "status": {
                "type": "string",
                "description": "Always `active` here: a revoked key is refused before this endpoint answers."
              },
              "lastUsedAt": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "When the key was used before this request, or null if this is its first."
              },
              "createdAt": {
                "type": "string",
                "description": "When the key was created."
              }
            },
            "required": [
              "id",
              "name",
              "prefix",
              "environment",
              "status",
              "lastUsedAt",
              "createdAt"
            ],
            "description": "The key this request was made with."
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Every scope the key was created with, such as `read:plants` or `write:transfers`."
          },
          "effectiveScopes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The scopes the key can use right now: its scopes, capped at what its owner's current role may do in xPlant and at what the workspace's plan includes. Check here before calling rather than discovering a limit from a `403` or a `402`."
          },
          "role": {
            "type": "string",
            "description": "The key owner's role in the workspace as of this request: `owner`, `admin`, `manager`, `member`, `viewer` or `guest`. A key never does more than this role can in xPlant."
          },
          "apiAccess": {
            "type": "string",
            "enum": [
              "full",
              "devices"
            ],
            "description": "`full` when the workspace's plan includes the API (xPlant+ Teams and Enterprise). `devices` on plans that include connected devices only: the key can register devices, manage their tokens, and post their readings and events."
          },
          "workspace": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid",
                "description": "The workspace's id."
              }
            },
            "required": [
              "id"
            ],
            "description": "The workspace the key acts in. A key belongs to exactly one."
          },
          "user": {
            "type": "object",
            "properties": {
              "id": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "uuid",
                "description": "The person the key belongs to."
              }
            },
            "required": [
              "id"
            ],
            "description": "The xPlant account the key belongs to."
          }
        },
        "required": [
          "key",
          "scopes",
          "effectiveScopes",
          "role",
          "apiAccess",
          "workspace",
          "user"
        ]
      },
      "MediaRecipe": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": [
              "string",
              "null"
            ],
            "description": "One of `active`, `archived`, `deprecated`, `draft`, `published`."
          },
          "origin": {
            "type": [
              "string",
              "null"
            ],
            "description": "Where the recipe came from, for example `user` or `imported`."
          },
          "visibility": {
            "type": "string",
            "description": "`private`: only its creator can see it. `team`: everyone in the workspace can."
          },
          "is_public": {
            "type": "boolean",
            "description": "Published to xPlant's public recipe library."
          },
          "notes": {
            "type": [
              "string",
              "null"
            ]
          },
          "ph_target": {
            "type": [
              "number",
              "null"
            ],
            "description": "Target pH."
          },
          "sterilization_notes": {
            "type": [
              "string",
              "null"
            ]
          },
          "storage_notes": {
            "type": [
              "string",
              "null"
            ]
          },
          "usage_notes": {
            "type": [
              "string",
              "null"
            ]
          },
          "components": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "description": "The component's id. Send it back in an update to keep the component."
                },
                "name": {
                  "type": "string",
                  "description": "The ingredient."
                },
                "qty": {
                  "type": "string",
                  "description": "The amount as the recipe writes it."
                },
                "unit": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "The unit of `qty`."
                },
                "concentration": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "A concentration, when the recipe states one apart from the amount."
                }
              },
              "required": [
                "id",
                "name",
                "qty",
                "unit",
                "concentration"
              ]
            },
            "description": "The ingredients, in order."
          },
          "version": {
            "type": "integer",
            "description": "The recipe's version. It goes up by one each time the formulation changes, and every version is kept."
          },
          "created_by": {
            "type": "string",
            "description": "User id of the workspace member who created the recipe. Only they can change it."
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO 8601."
          }
        },
        "required": [
          "id",
          "title",
          "status",
          "origin",
          "visibility",
          "is_public",
          "notes",
          "ph_target",
          "sterilization_notes",
          "storage_notes",
          "usage_notes",
          "components",
          "version",
          "created_by",
          "created_at"
        ]
      },
      "Money": {
        "type": "object",
        "properties": {
          "amount": {
            "type": "string",
            "description": "An exact decimal, as text — for example `\"1250.00\"`. Never a floating-point number; parse it with a decimal type."
          },
          "currency": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO 4217 currency code, for example `USD`. Null only when the record carries no currency. Never add amounts in different currencies."
          }
        },
        "required": [
          "amount",
          "currency"
        ]
      },
      "OrderLine": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "product_link_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "The store product this line sold, as xPlant tracks it; null when it is not tracked yet. Pass it as `product_link_id` to list that product's lines."
          },
          "plant_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "The culture line the product is matched to (a plant id). Null until someone in the lab matches the product."
          },
          "store_product_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Your store's own id for the product."
          },
          "store_variant_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Your store's own id for the variant, when the product has variants."
          },
          "quantity": {
            "type": "integer"
          },
          "unit_price": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Money"
              },
              {
                "type": "null"
              }
            ],
            "description": "The price per unit, with its currency. Null when your store reported none."
          },
          "occurred_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "When the sale happened in your store, as an ISO 8601 timestamp in UTC."
          }
        },
        "required": [
          "id",
          "product_link_id",
          "plant_id",
          "store_product_id",
          "store_variant_id",
          "quantity",
          "unit_price",
          "occurred_at"
        ]
      },
      "Plant": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "description": "What the lab calls the plant: its common name, or its species when it has none."
          },
          "species": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "description": "Where the plant stands in the lab, usually one of `active`, `dormant`, `harvested`, `contaminated`, `failed`, `in_culture`, `ready_for_transfer`, `quarantined`, `archived`."
          },
          "workspace_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "The workspace the plant belongs to."
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO 8601 timestamp."
          },
          "external_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Your own identifier for the plant, exactly as it was given, or null when it has none."
          },
          "custom_fields": {
            "$ref": "#/components/schemas/CustomFieldValues"
          }
        },
        "required": [
          "id",
          "name",
          "species",
          "status",
          "workspace_id",
          "created_at",
          "external_id",
          "custom_fields"
        ]
      },
      "PriceEvent": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "plant_id": {
            "type": "string",
            "format": "uuid",
            "description": "The culture line whose price changed (a plant id)."
          },
          "list_price": {
            "$ref": "#/components/schemas/Money"
          },
          "previous_list_price": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Money"
              },
              {
                "type": "null"
              }
            ],
            "description": "The price it moved from, in its own currency. Null for a line's first price."
          },
          "price_source": {
            "type": "string",
            "description": "Who set the list price: `manual`, `store_adopted`, `store_follow`. `manual` is a person typing it, `store_adopted` a person accepting the price from your store, and `store_follow` an automatic update that follows your store's price."
          },
          "changed_by": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "The workspace member who changed it. Null for an automatic change."
          },
          "changed_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "When the price moved, as an ISO 8601 timestamp in UTC."
          }
        },
        "required": [
          "id",
          "plant_id",
          "list_price",
          "previous_list_price",
          "price_source",
          "changed_by",
          "changed_at"
        ]
      },
      "SellThroughGroup": {
        "type": "object",
        "properties": {
          "plant_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "The culture line (a plant id). Null groups the sales of store products not yet matched to one."
          },
          "currency": {
            "type": [
              "string",
              "null"
            ],
            "description": "The currency of every amount in this group. Null for lines that carried none."
          },
          "units": {
            "type": "integer",
            "description": "Total quantity sold."
          },
          "revenue": {
            "$ref": "#/components/schemas/Money"
          },
          "order_line_count": {
            "type": "integer"
          },
          "priced_line_count": {
            "type": "integer",
            "description": "Order lines that carried a unit price. Lines without one count toward `units` but not `revenue`."
          },
          "first_occurred_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "The earliest sale in the group."
          },
          "last_occurred_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "The latest sale in the group."
          }
        },
        "required": [
          "plant_id",
          "currency",
          "units",
          "revenue",
          "order_line_count",
          "priced_line_count",
          "first_occurred_at",
          "last_occurred_at"
        ]
      },
      "SensorReading": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "device_id": {
            "type": "string",
            "format": "uuid"
          },
          "team_id": {
            "type": "string",
            "format": "uuid",
            "description": "The id of the workspace the reading belongs to."
          },
          "room_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "type": {
            "type": "string",
            "description": "What was measured: `temperature`, `humidity`, `ph`, `co2`, `light`, `other`."
          },
          "value": {
            "type": "number"
          },
          "unit": {
            "type": "string"
          },
          "recorded_at": {
            "type": "string",
            "description": "When the reading was taken, as an ISO 8601 timestamp."
          },
          "notes": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "device_id",
          "team_id",
          "room_id",
          "type",
          "value",
          "unit",
          "recorded_at",
          "notes",
          "created_at"
        ]
      },
      "Sop": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "category": {
            "type": [
              "string",
              "null"
            ],
            "description": "The kind of procedure, such as `media-prep`, `sterilization` or `tissue-culture`."
          },
          "status": {
            "type": [
              "string",
              "null"
            ],
            "description": "`draft`, `active` or `archived`."
          },
          "level": {
            "type": [
              "string",
              "null"
            ],
            "description": "Who it is written for: `beginner`, `intermediate`, `advanced` or `expert`."
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "equipment": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Equipment the procedure calls for."
          },
          "estimatedTimeMinutes": {
            "type": [
              "number",
              "null"
            ],
            "description": "How long one run is expected to take, in minutes."
          },
          "durationHours": {
            "type": [
              "number",
              "null"
            ],
            "description": "The procedure's duration in hours, as the lab recorded it."
          },
          "currentVersion": {
            "type": "integer",
            "description": "The newest version number. It can be a draft; the version the lab works from is the one Get an SOP returns."
          },
          "updatedAt": {
            "type": [
              "string",
              "null"
            ]
          },
          "createdAt": {
            "type": [
              "string",
              "null"
            ]
          },
          "version": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/SopVersion"
              },
              {
                "type": "null"
              }
            ],
            "description": "The version in force — the one the lab works from — with its steps. Null when no version has been put into force, and then there are no steps to follow: a draft is never returned."
          }
        },
        "required": [
          "id",
          "title",
          "description",
          "category",
          "status",
          "level",
          "tags",
          "equipment",
          "estimatedTimeMinutes",
          "durationHours",
          "currentVersion",
          "updatedAt",
          "createdAt",
          "version"
        ]
      },
      "SopRun": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "sopId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "The SOP being run."
          },
          "version": {
            "type": [
              "integer",
              "null"
            ],
            "description": "The version of the SOP this run follows — the one in force when it started. It never changes afterwards."
          },
          "versionId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "That version's id."
          },
          "status": {
            "type": "string",
            "description": "`in_progress` from the moment a run starts. A run is completed in xPlant, once every step is ticked off; runs can also be `pending`, `on_hold`, `failed`, `cancelled` or `archived`."
          },
          "batchCode": {
            "type": [
              "string",
              "null"
            ],
            "description": "Your lab's own identifier for what is being run."
          },
          "startedAt": {
            "type": [
              "string",
              "null"
            ]
          },
          "completedAt": {
            "type": [
              "string",
              "null"
            ]
          },
          "progress": {
            "type": "number",
            "description": "How far through its steps the run is, from 0 to 100, as ticked off in xPlant."
          },
          "steps": {
            "type": "array",
            "items": {},
            "description": "The step checklist as ticked off in xPlant, one entry per step, such as `{ \"id\": \"step-3\", \"completed\": true }`. Empty until someone works the run in xPlant. Evidence posted through the API is in `events` and does not tick steps off here."
          }
        },
        "required": [
          "id",
          "sopId",
          "version",
          "versionId",
          "status",
          "batchCode",
          "startedAt",
          "completedAt",
          "progress",
          "steps"
        ]
      },
      "SopRunDetail": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "sopId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "The SOP being run."
          },
          "version": {
            "type": [
              "integer",
              "null"
            ],
            "description": "The version of the SOP this run follows — the one in force when it started. It never changes afterwards."
          },
          "versionId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "That version's id."
          },
          "status": {
            "type": "string",
            "description": "`in_progress` from the moment a run starts. A run is completed in xPlant, once every step is ticked off; runs can also be `pending`, `on_hold`, `failed`, `cancelled` or `archived`."
          },
          "batchCode": {
            "type": [
              "string",
              "null"
            ],
            "description": "Your lab's own identifier for what is being run."
          },
          "startedAt": {
            "type": [
              "string",
              "null"
            ]
          },
          "completedAt": {
            "type": [
              "string",
              "null"
            ]
          },
          "progress": {
            "type": "number",
            "description": "How far through its steps the run is, from 0 to 100, as ticked off in xPlant."
          },
          "steps": {
            "type": "array",
            "items": {},
            "description": "The step checklist as ticked off in xPlant, one entry per step, such as `{ \"id\": \"step-3\", \"completed\": true }`. Empty until someone works the run in xPlant. Evidence posted through the API is in `events` and does not tick steps off here."
          },
          "events": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SopStepEvent"
            },
            "description": "Every piece of evidence posted against the run's steps, oldest first."
          }
        },
        "required": [
          "id",
          "sopId",
          "version",
          "versionId",
          "status",
          "batchCode",
          "startedAt",
          "completedAt",
          "progress",
          "steps",
          "events"
        ]
      },
      "SopStepEvent": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "runId": {
            "type": "string",
            "format": "uuid",
            "description": "The run the evidence belongs to."
          },
          "stepKey": {
            "type": "string",
            "description": "The step it was recorded against — the `stepId` it was posted to."
          },
          "eventType": {
            "type": "string",
            "description": "`confirmed`, `scanned`, `skipped`, `note` or `device_state` for a step event; `measured` for a measurement."
          },
          "recordedAt": {
            "type": "string",
            "description": "When it happened: the `recorded_at` that was sent, or when xPlant received it."
          },
          "payload": {
            "type": "object",
            "additionalProperties": {},
            "description": "The detail sent with the event. For a measurement: `metric`, `value`, `unit`, and `notes` when there were any."
          }
        },
        "required": [
          "id",
          "runId",
          "stepKey",
          "eventType",
          "recordedAt",
          "payload"
        ]
      },
      "SopSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "category": {
            "type": [
              "string",
              "null"
            ],
            "description": "The kind of procedure, such as `media-prep`, `sterilization` or `tissue-culture`."
          },
          "status": {
            "type": [
              "string",
              "null"
            ],
            "description": "`draft`, `active` or `archived`."
          },
          "level": {
            "type": [
              "string",
              "null"
            ],
            "description": "Who it is written for: `beginner`, `intermediate`, `advanced` or `expert`."
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "equipment": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Equipment the procedure calls for."
          },
          "estimatedTimeMinutes": {
            "type": [
              "number",
              "null"
            ],
            "description": "How long one run is expected to take, in minutes."
          },
          "durationHours": {
            "type": [
              "number",
              "null"
            ],
            "description": "The procedure's duration in hours, as the lab recorded it."
          },
          "currentVersion": {
            "type": "integer",
            "description": "The newest version number. It can be a draft; the version the lab works from is the one Get an SOP returns."
          },
          "updatedAt": {
            "type": [
              "string",
              "null"
            ]
          },
          "createdAt": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "title",
          "description",
          "category",
          "status",
          "level",
          "tags",
          "equipment",
          "estimatedTimeMinutes",
          "durationHours",
          "currentVersion",
          "updatedAt",
          "createdAt"
        ]
      },
      "SopVersion": {
        "type": "object",
        "properties": {
          "version": {
            "type": "integer",
            "description": "The version number."
          },
          "lifecycleStatus": {
            "type": "string",
            "const": "effective",
            "description": "Always `effective`: only the version in force is returned."
          },
          "effectiveAt": {
            "type": [
              "string",
              "null"
            ],
            "description": "When this version was put into force."
          },
          "approvedAt": {
            "type": [
              "string",
              "null"
            ],
            "description": "When it was approved."
          },
          "steps": {
            "type": "array",
            "items": {},
            "description": "The procedure's steps, in order, as this version records them. Each step carries an `id` — the value to post evidence against as `stepId` when you run it — and an `instruction`, and may carry `notes`, `safetyWarning` and `estimatedMinutes`. Other fields can appear; ignore any you do not use."
          },
          "title": {
            "type": [
              "string",
              "null"
            ],
            "description": "The title as this version records it."
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "The description as this version records it."
          }
        },
        "required": [
          "version",
          "lifecycleStatus",
          "effectiveAt",
          "approvedAt",
          "steps",
          "title",
          "description"
        ]
      },
      "Stage": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "entity_type": {
            "type": "string",
            "enum": [
              "plant",
              "explant"
            ],
            "description": "Whether the stage belongs to a plant or an explant."
          },
          "entity_id": {
            "type": "string",
            "format": "uuid",
            "description": "The plant's or explant's id."
          },
          "stage": {
            "type": "string",
            "description": "The stage, usually as a key from your lab's stage list such as `multiplication`. Records made by older tools can carry the stage's display name instead, such as `Multiplication`."
          },
          "status": {
            "type": "string",
            "description": "`active` for the stage the plant or explant is in now, `completed` for one it has moved on from. Stages can also be `failed` or `archived`."
          },
          "entered_on": {
            "type": [
              "string",
              "null"
            ],
            "description": "When it entered this stage, as an ISO 8601 timestamp."
          },
          "completed_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "When it moved on from this stage. Null while the stage is current."
          },
          "room_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "The room it was in for this stage, when one was recorded."
          },
          "notes": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "entity_type",
          "entity_id",
          "stage",
          "status",
          "entered_on",
          "completed_at",
          "room_id",
          "notes",
          "created_at"
        ]
      },
      "Task": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "description": "Board column: `backlog`, `todo`, `in_progress`, `waiting_blocked`, `review`, `done`."
          },
          "due_date": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO 8601 timestamp."
          },
          "assigned_to": {
            "type": [
              "string",
              "null"
            ],
            "description": "User id of the workspace member the task is assigned to."
          },
          "priority": {
            "type": [
              "string",
              "null"
            ]
          },
          "priority_rank": {
            "type": [
              "number",
              "null"
            ],
            "description": "Queue position. Lower sorts first; null means the priority label decides."
          },
          "priority_source": {
            "type": "string",
            "description": "Where the current order came from: `default`, `auto`, `manual`. `manual` means a person placed the task, and automated writes leave it where it is unless they send `release: true`."
          },
          "category": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "entity_link": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "entity_type": {
                "type": "string",
                "enum": [
                  "plant",
                  "explant"
                ]
              },
              "entity_id": {
                "type": "string",
                "format": "uuid"
              }
            },
            "required": [
              "entity_type",
              "entity_id"
            ],
            "description": "The plant or explant the task is about. Returned when you create, fetch or update one task; lists leave it out."
          }
        },
        "required": [
          "id",
          "title",
          "status",
          "due_date",
          "assigned_to",
          "priority",
          "priority_rank",
          "priority_source",
          "category",
          "created_at"
        ]
      },
      "Transfer": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "entity_type": {
            "type": "string",
            "enum": [
              "plant",
              "explant"
            ],
            "description": "Whether the transfer was of a plant or an explant."
          },
          "entity_id": {
            "type": "string",
            "format": "uuid",
            "description": "The plant's or explant's id."
          },
          "transfer_date": {
            "type": [
              "string",
              "null"
            ],
            "description": "When the transfer was done, as an ISO 8601 timestamp."
          },
          "transfer_cycle": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Which subculture this was for the plant or explant — 1 for the first."
          },
          "from_location": {
            "type": [
              "string",
              "null"
            ],
            "description": "Where the culture was before the transfer."
          },
          "to_location": {
            "type": [
              "string",
              "null"
            ],
            "description": "Where the culture went."
          },
          "status": {
            "type": "string",
            "description": "`active` for a transfer recorded through the API. Transfers scheduled in xPlant read `pending` until someone marks them `completed` or `cancelled`."
          },
          "notes": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "custom_fields": {
            "type": "object",
            "additionalProperties": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "number"
                },
                {
                  "type": "boolean"
                },
                {
                  "type": "null"
                }
              ]
            },
            "description": "Your lab's own fields on this transfer, keyed by field key. Always an object: `{}` when none were filled in, and always `{}` on a plant transfer."
          }
        },
        "required": [
          "id",
          "entity_type",
          "entity_id",
          "transfer_date",
          "transfer_cycle",
          "from_location",
          "to_location",
          "status",
          "notes",
          "created_at",
          "custom_fields"
        ]
      },
      "Workspace": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "description": "The lab's name as it appears in xPlant."
          },
          "type": {
            "type": "string",
            "description": "`lab` for a lab, `team` for a group inside one."
          }
        },
        "required": [
          "id",
          "name",
          "type"
        ]
      }
    }
  },
  "x-docs-source": {
    "ref": "origin/main",
    "commit": "7b2641feb"
  }
}
