{
  "openapi": "3.0.1",
  "info": {
    "title": "Purple Grain API",
    "version": "1.0.0",
    "description": "## Presenting property data and images\n\nProperty, paddock and mob answers come from the PurpleGrain records returned\nfor that resource. The photos on a record and the boundary maps PurpleGrain\nsupplies are the only images of a property PurpleGrain holds. A generated\nboundary map is an outline of recorded boundaries, not satellite imagery or\na property photo. A record with no photo has no photo: a stock image, a\nreal-estate listing or a web image result is not a photo of the person's\nproperty, even when the name or locality matches. Property names, addresses\nand coordinates are private records, shared with other services only when\nthe person asks. An external image the person asks for is attributed to its\nsource and is not a PurpleGrain record or a verified photo of their\nproperty. Missing record values remain \"not recorded\".\n\n## Attaching a photo from disk\n\nA file on the agent's disk is never base64-encoded into a tool call. It is\nstaged with `createImageUpload`, whose `curl_example` PUTs the bytes (the\nURL is on this API host and needs no bearer token), and attached with\n`image_upload_ids` on the create or update. A public http(s) URL uses\n`image_urls`. `image_base64s` is only for a tiny generated image under\nabout 100 KB decoded. One upload can go on several records until it\nexpires. On update, `append` (the default) adds photos, `replace` swaps\nthem all, and `remove_image_ids` deletes specific ones. `createImageUpload`\nand the PUT do not need `confirm`. The call that attaches the photo does.\n\nKeep reads and answers within the requested business or property. For a\nnamed business, resolve its id from the user's accepted memberships, then\ncall `listFarms` with that `organization_id`. Do not enumerate or include\nthe user's personal properties or another business's properties as an\nextra unless the person asks for them. Return only the account and contact\ndetails needed to answer the request; do not echo identifiers or street\naddresses merely because a tool returned them.\n\n## Properties, blocks, paddocks and mobs\n\nUse the app's words when you answer: a **property** (one PIC, one boundary), owned by a\n**business** or by the person's own account; **paddocks** inside it; **mobs** of animals,\ncounted in **head**; areas in **ha**. The tree, and where each link is read:\n\n- **Property**: `getFarm` or `listFarms`. Its `blocks` lists each block with `id`, `name`,\n  `kind`, `block_number`, `paddock_ids`, a derived `area_ha` and a derived outline (`hull`).\n- **Block**: a label for where paddocks sit inside one property (\"Racecourse block\", an\n  outblock across the road). It has no page and no records, and is read only from the\n  property. `createBlock` names one and groups `paddock_ids` into it. `updateBlock` renames\n  it, changes `kind` or `block_number`, or with `paddock_ids` replaces its members (a paddock\n  left out goes back to the home block). `deleteBlock` ungroups it.\n- **Paddock**: `listPaddocks` with `farm_id`, or `getPaddock`. It belongs to the property;\n  `block_id` is its block, null for the home block. `updatePaddock` with `block_id` moves one\n  paddock into a block of the same property, and `block_id: null` takes it out.\n- **Mob**: `listMobs` with `farm_id`, or `getMob`. It belongs to the property, not to a\n  paddock. `current_paddock_id` is the paddock it is in now, derived from its latest\n  completed move, and null while it has never been placed. It is read-only: move a mob with\n  `moveMob`, which writes the move record.\n\nTasks and records are filed on paddocks and mobs, never on a block. To read the whole tree,\ncall `getFarm` for the blocks, `listPaddocks` with `farm_id` for each paddock's `block_id`,\nand `listMobs` with `farm_id` for each mob's `current_paddock_id` (`getFarmPicture` returns\nthe property with every paddock and mob in one call). When you describe a property, name its\nblocks and say which paddocks are in each; every other paddock is in the home block.\nGrouping, regrouping or ungrouping paddocks never writes, moves or removes a record.\n\n## Livestock treatment (drench) records\n\nA drench, vaccination, pour-on, jetting or injection is one record on one mob.\nSave it with `createTreatmentRecord` and `category: \"drench\"` for a drench\n(`POST /mobs/{mob_id}/treatment-records`). That call sets the producer from the\nproperty's owner, `operation_type` to `treatment` and `capture_source` to `agent`. A date today or earlier is a completed\nrecord; a future date is an open task. One record holds every product given\ntogether (up to 10). To record three mobs, create three records.\n\nPer product, Australian LPA wants the treatment date, the mob and head\ncount treated, the product trade name, batch number, expiry, dose (value,\nunit, and per head or per kg), route, WHP days, ESI days, the WHP and ESI\nend dates, and who treated. `createTreatmentRecord` stores the date, the head\ncount, and each product's name, active constituent, batch, dose, `whp_days`\nand `esi_days`, plus `expiry` and the dose as a value, unit and basis when\nthe farmer stated them, and the route. Any other category (vaccine, lice, pain relief) is\nthe same tool with that `category`.\nWrite \"not recorded\" (leave the field unset) rather than inventing a value. If the label has no ESI, send\nthe WHP number as `esi_days`; the server does not substitute. Never invent\na batch, expiry, WHP or ESI. A value you cannot read from a photo stays\nunconfirmed until the farmer confirms it.\n\nBefore saving: collect the missing fields, check that mob's completed\nrecords for a same-date same-product duplicate, and confirm the summary\nwith the farmer. `listTasks` with `asset_id` set to the mob and\n`filter[status][]=completed` is that history; read `treatment.products`, not\ntags. Writes are refused until the tool is called again with `confirm: true`.\n\nFind the mob first. `fetchCurrentUser` is the signed-in user.\n`listOrganizationRoles` with that user's id and `include=organization` is\nthe businesses they belong to and the permission (`owner`, `manager`, and\nthe rest). `listFarms` with no `organization_id` is personal properties\nonly — a business's properties appear only with `organization_id`.\n`getFarmPicture` is every paddock and mob on one property. `listMobs` with\nneither `farm_id` nor `organization_id` is personal-property mobs only.\n\n`treatment.safe_for_slaughter_on` is the completion date plus the longest WHP\namong named products that carry a withholding period.\n`treatment.export_eligible_on` is the same with ESI. Both are null while the\ntask is open. `getMob` returns `withholding` while either date is still ahead.\nA mob with no head count yet takes the head recorded on its first drench as its count;\na count already set is never changed. `listProducts?kind=drench` is a short catalogue (name, active, WHP, ESI),\nnot the APVMA list — confirm the label with the farmer, because ESIs change.\n\nIf a write returns a gateway error (502 or 504) or the MCP session drops,\nread the mob's records back before retrying. The write may already have\nsucceeded. Do not send `broadcast` or `share_scope`; an API or MCP client\nthat names either is refused and nothing is written.\n\nOn `createTask`, a property owned by a business needs `producer` set to\nthat organization. Omitting it returns 403 Forbidden, detail \"Unauthorized\",\nwith `source.pointer` `/request/headers/authorization`. That is a missing\nproducer, not a bad token. A 403 whose `code` is `insufficient_access` is a\nreal access refusal: its `detail` names the business and the access that is missing. `createTreatmentRecord`\nsets the producer itself, so that missing-producer 403 does not apply to it.\n\n## Changing this connection's access from the chat\n\nA connection starts with read and write wherever the person's role allows it\n(owner, admin or manager), read only elsewhere (since 2026-10-09). A refusal\nwith code `insufficient_access` means the person narrowed this connection, or\nconnected before that date. If they want to allow the change, offer to switch\nthis connection to read and write in that business: `getConnection` is what it\nmay do in each business and the most their role allows (`meta.access`), and\n`updateConnectionAccess` with `{ \"grants\": [{ \"organization_id\": \"<id>\",\n\"access\": \"write\" }] }` (null `organization_id` is their own account) sets it,\nonce they have said yes in the chat. It can never exceed their role: a viewer\nor contractor stays read only, and then the business has to change the role.\nA connection made before 2026-10-09 has no `write` scope, so that call is\nrefused too; then the `manage_url` in the refusal is the way. The person is\nemailed what changed, and the same link is where they undo it.\n\nUse these words. The offer, where the person's role allows the write: \"Want\nme to switch this connection to read and write for {business}? You can\nnarrow it again any time.\" Where it does not: \"I can't widen this one: your\nrole in {business} is {role}, so an owner of the business would have to\nchange your role first.\" After the yes and a successful\n`updateConnectionAccess`, make the change they asked for, then: \"Done.\n{assistant} now has read and write in {business}. We've emailed you.\"\n\n## Spray records\n\nA herbicide, pesticide or fungicide spray is one record on one paddock. Save it with\n`createSprayRecord` (`POST /paddocks/{paddock_id}/spray-records`). That call sets the\nproducer from the farm's owner, `operation_type` to `spray` and `capture_source` to\n`agent`. A date today or earlier is a completed record; a future date is an open task. One\nrecord holds every product in the tank (up to 10). To record three paddocks, create three\nrecords. A spray belongs to a paddock and a drench to a mob; a task carries at most one of\nthe two.\n\nPer product, `createSprayRecord` stores the name, active constituent, batch, rate,\n`grazing_whp_days` and `harvest_whp_days`, and the record stores `area_ha` (the paddock's area\nwhen you omit it), plus the application method, water rate, nozzle, droplet category,\nspeed and a list of `weather_readings`, and per product the role, expiry, ESI, EGI and EAFI.\nLeave a field unset rather than inventing a value; it prints \"not recorded\". Never invent a batch, rate or withholding period. A value you cannot read\nfrom a photo stays unconfirmed until the farmer confirms it.\n\nBefore saving: collect the missing fields, check that paddock's completed records for a\nsame-date same-product duplicate, and confirm the summary with the farmer. `listTasks` with\n`asset_id` set to the paddock and `filter[status][]=completed` is that history; read\n`spray.products`, not tags. Writes are refused until the tool is called again with\n`confirm: true`. `getFarmPicture` is every paddock and mob on one property.\n\n`spray.safe_to_graze_on` is the completion date plus the longest grazing withholding period\namong named products, and `spray.safe_to_harvest_on` is the same with the harvest period.\nEach is null while the task is open or while no named product has a value above zero for it.\n`getPaddock` returns `withholding` while either date is still ahead.\n`listProducts?kind=herbicide` is a short catalogue (name, active, grazing and harvest WHP,\nwhere `whp_days` is the grazing period and `esi_days` the harvest period), not the APVMA list\n— confirm the label with the farmer. If a write returns a gateway error (502 or 504) or the\nMCP session drops, read the paddock's records back before retrying.\n\n## Treatment, husbandry, fertiliser, sowing and field work records\n\nA drench is one **category** of treatment. Record any treatment given to a mob (vaccine, lice or\nfluke treatment, vitamin or mineral, antibiotic, pain relief, additive, foot treatment,\nreproductive, HGP) with `createTreatmentRecord` (`POST /mobs/{mob_id}/treatment-records`); it needs a\n`category`, and takes `route`, and per product the `expiry` and the dose. Record something done\n**to** a mob (marking, shearing, crutching, weaning, scanning, weighing) with\n`createHusbandryRecord`: marking day is ONE record with procedures `mark`, `tail_dock` and\n`castrate` and product lines for the vaccine and the pain relief, and its `outcomes` hold the\ncounts. Record fertiliser spread with `createFertiliserRecord`, a crop sown with\n`createSowingRecord` and cultivation, harvest, hay or silage, slashing, rolling, burning or\nirrigation with `createFieldworkRecord`, each on one paddock. A record belongs to one asset, so\nspraying three paddocks is three records. Type a rate as the farmer said it (\"100 kg/ha\"); a\nnumber with no unit is refused, so ask for the unit.\n\nOnly a dated interval on a product line holds an asset back. **Fertiliser has no withholding and\nnever changes a paddock's status**; its `advisory_note` is advice to pass on. A sowing's seed\ntreatment gives the paddock a grazing date, and completing a sowing sets the paddock's current\ncrop. Field work is checked: read `warnings` on the response, which flags a harvest before a\nharvest withholding period ends or hay cut before a grazing one ends, and tell the farmer. A\nwarning never blocks the write. Check `listProducts` for the label, read its intervals by their own\nnames (`meat_whp_days`, `grazing_whp_days`, `harvest_whp_days`, `export_esi_days`, `egi_days`),\nand never invent a batch, expiry, rate or withholding period.\n\n## PurpleGrain AI records\n\nA farmer can write a record in plain words (and a photo) without choosing its type. It is a\ntask with `operation_type: ai`, `capture_source` `voice` or `typed`, `status: completed`,\n`created_as: record` and the farmer's words as `description`, created from the PurpleGrain app;\nthe mob or paddock is optional. A background formatter (`AiFormatJob`) then writes the real\ntreatment, husbandry, spray, fertiliser, sowing or field-work record, links the asset when the\nwords name exactly one, and sets `ai_format_done_at`. Never create one yourself: `createTask` or\n`updateTask` with `operation_type: ai` is refused with 403 for an assistant, because you already\nknow what the record is. Write the typed record instead (`createTreatmentRecord` and the other\n`create*Record` operations, or `createTask` with the typed child) with `capture_source: agent`.\nRead `ai_format_meta` for what it did:\n`outcome`, `questions` (what the farmer still has to supply), `candidates` (the mobs or\npaddocks the words could mean, when it left the record unassigned) and `unconfirmed` (stated\nproduct fields the farmer's words and photos do not support). Records with no asset are\n`listTasks` with `filter_farm_id`, `filter_unassigned=true` and `filter_operation_type=ai`; attach\none with `updateTask` and the `asset` relationship. A record that already has its typed child, or\nwhose `ai_format_done_at` is set, is never formatted again, and an edit does not queue it. Never\ninvent a batch, dose, expiry, WHP or ESI for one: leave it unset and ask. `POST /tasks/ai`\nstays paused. Two settings sit beside it: `fetchCurrentUser` carries the person's\n`default_record_mode` (`ai`, the default, or `general`: which record type their composer opens\non; `updateUser` changes it, and anything else is refused), and `getOrganization` carries a\nbusiness's `ai_format_enabled` (\"Allow PurpleGrain AI to read my records\", default on; off means\nthe formatter's request carries no farm context for that business; `updateOrganization` changes it).\n\n## Registers\n\n`getTreatmentRegister` (`GET /farms/{farm_id}/treatment-register`) answers with the property's\nrecords as the rows a register prints, by `kind`: `register` (the LPA treatment register: every\ntreatment and every husbandry **product** on the chosen mobs), `nvd` (only what is still under a\nwithholding period, for the National Vendor Declaration), `spray` (the chemical use register for\npaddocks) and `paddock-inputs` (fertiliser and sowing). Choose mobs with `mob_ids` for the first two\nand paddocks with `paddock_ids` for the others. A value a record should carry and does not is the\ntext `not recorded`: say so to the farmer and ask for it; never fill it in. Fertiliser is in the\npaddock inputs register, never in the spray register or the NVD, because it has no withholding.\n\n## What a mob or a paddock is under\n\nRead it, never compute it. `getFarmStatus` (`GET /farms/{farm_id}/status`) answers every active\npaddock and mob on a property in one call, each with a `state` and, when something is holding it\nback, its `withholding`. It is the same value `getMob` and `getPaddock` return as `withholding`,\nderived from completed records and never stored. A mob is held back by the meat WHP and the\nexport slaughter interval on the products of its completed **treatment and husbandry** records\n(a 6-in-1 given at marking counts like a drench); a paddock by the grazing and harvest periods on\nits completed sprays and on a sowing's seed treatments. A spray label that says DO NOT graze is\nrecorded on the spray but gives the paddock no status, since it has no date. Each date\nis the latest end across every record still under an interval, not only the newest.\n**Fertiliser and field work never change a paddock's status**; a fertiliser's advisory note is\nadvice to pass on, not a status. Do not tell a farmer an asset is clear from reading its records\nyourself: ask `getFarmStatus`. A drench is a treatment of category `drench`: record it with\n`createTreatmentRecord` and read it from a task's `treatment`. The deprecated `drench` key on\n`createTask` and `updateTask` still writes that same treatment.\n\n## Authentication\n\nAll protected `/api/v1/*` routes expect an **`Authorization: Bearer`** header with the access token string, where the token is an\n**opaque OAuth 2.0 token issued by Doorkeeper** (not a JWT).\n\n### How to obtain tokens\n\n1. **Standard OAuth 2.0** — `POST /oauth/token` at the **API host root** (e.g. `https://api.example.com/oauth/token`),\n   **not** under `/api/v1`. Documented under the **OAuth** tag with per-operation `servers`. Supports\n   `grant_type=password`, `refresh_token`, `client_credentials`, and `authorization_code` (with PKCE for public clients).\n\n2. **First-party JSON helpers** (same token format, plus `user_id` in the response body):\n   - `POST /api/v1/users/sign-in` — email/password JSON body; no `client_id` / `client_secret` required.\n   - `POST /api/v1/users/access-tokens` — omniauth-style JSON after external IdP validation.\n\n### Using tokens\n\n- Send **`Authorization: Bearer &lt;access_token&gt;`** on API requests.\n- **`POST /api/v1/users/validate-access-token`** — check whether a token string is still valid (optional helper).\n- **Sign out / revoke:** `DELETE /api/v1/users/sign-out` with the bearer token, or **`POST /oauth/revoke`**.\n\n### Authorization code flow\n\nBrowser-based apps use **`GET /oauth/authorize`** (host root) and then exchange the code at **`POST /oauth/token`**.\nThis deployment does not rely on a Rails session cookie for the authorize step; interactive login is expected to be\ncoordinated externally (e.g. `OAUTH_RESOURCE_OWNER_LOGIN_URL`) where applicable.\n"
  },
  "tags": [
    {
      "name": "Connection",
      "description": "The AI-assistant connection a request arrives through: what it may do in\neach business and the most the person's role allows, and the change the\nassistant makes to it from the chat with the person's say-so.\n"
    },
    {
      "name": "PropertySetupDrafts",
      "description": "The map editor's memory of a property not saved yet (design handoff 29): one per person per\nowner, read only by the person who wrote it. First-party only.\n"
    },
    {
      "name": "Blocks",
      "description": "Labels for where paddocks sit inside one property (design handoff 30): an outblock, an\nirrigation farm or a management block. A block has no page and no records of its own; a\nproperty read lists its blocks with the member paddocks and a derived area and outline.\n"
    },
    {
      "name": "BoundaryDrafts",
      "description": "The map editor's memory of a boundary being drawn on a property or paddock (design handoff 29):\none per person per target, cleared by that person's boundary save. First-party only.\n"
    },
    {
      "name": "OAuth",
      "description": "OAuth 2.0 + MCP onboarding at the **API host root** (`https://{defaultHost}`), not under `/api/v1`;\neach operation sets `servers` accordingly. Covers the Doorkeeper core (`/oauth/token`, `/oauth/revoke`,\n`/oauth/authorize`), **Dynamic Client Registration** (`/oauth/register`, RFC 7591), the headless\nauthorization grant (`/oauth/authorize/grant`), and the unauthenticated discovery documents under\n`/.well-known/*` (RFC 8414 + RFC 9728) that let MCP/AI clients bootstrap auth with zero configuration.\n"
    },
    {
      "name": "Sessions",
      "description": "First-party sign-in and sign-out in JSON; issues and revokes Doorkeeper access tokens."
    },
    {
      "name": "Subscriptions",
      "description": "Provider subscriptions that gate entitlements (listing services, free job broadcasts)."
    }
  ],
  "servers": [
    {
      "url": "https://api.purplegrain.com/api/v1",
      "description": "Versioned JSON API"
    }
  ],
  "paths": {
    "/openapi.json": {
      "get": {
        "operationId": "getOpenapiJson",
        "summary": "OpenAPI specification (JSON)",
        "description": "Returns the OpenAPI 3.0.1 specification in JSON format.\nStandard endpoint for documentation UIs and SDK generation.\n",
        "tags": [
          "Documentation"
        ],
        "responses": {
          "200": {
            "description": "The OpenAPI specification",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "OpenAPI 3.0.1 spec"
                }
              }
            }
          },
          "404": {
            "description": "Spec file not found"
          }
        }
      }
    },
    "/openapi.yaml": {
      "get": {
        "operationId": "getOpenapiYaml",
        "summary": "OpenAPI specification (YAML)",
        "description": "Returns the OpenAPI 3.0.1 specification in YAML format.\n",
        "tags": [
          "Documentation"
        ],
        "responses": {
          "200": {
            "description": "The OpenAPI specification",
            "content": {
              "application/vnd.oai.openapi": {
                "schema": {
                  "type": "object",
                  "description": "OpenAPI 3.0.1 spec"
                }
              }
            }
          },
          "404": {
            "description": "Spec file not found"
          }
        }
      }
    },
    "/openapi.yml": {
      "get": {
        "operationId": "getOpenapiYml",
        "summary": "OpenAPI specification (YAML, alternate extension)",
        "description": "Same as /openapi.yaml.",
        "tags": [
          "Documentation"
        ],
        "responses": {
          "200": {
            "description": "The OpenAPI specification",
            "content": {
              "application/vnd.oai.openapi": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "Spec file not found"
          }
        }
      }
    },
    "/oauth/token": {
      "servers": [
        {
          "url": "https://{defaultHost}",
          "description": "Doorkeeper is mounted at the API host root.",
          "variables": {
            "defaultHost": {
              "default": "api.purplegrain.com"
            }
          }
        }
      ],
      "post": {
        "tags": [
          "OAuth"
        ],
        "summary": "OAuth 2.0 token endpoint",
        "description": "**Doorkeeper** token endpoint (RFC 6749). Request body must be `application/x-www-form-urlencoded`.\n\n**Supported `grant_type` values:**\n- `password` — Resource owner password credentials. Use `username` (user email), `password`, `client_id`, and\n  `client_secret` if the OAuth application is confidential. Requires a registered `Doorkeeper::Application`.\n- `refresh_token` — `refresh_token`, `client_id`, and `client_secret` (if confidential).\n- `client_credentials` — `client_id`, `client_secret`, optional `scope`. No user context: the token has no\n  resource owner (RFC 6749 §4.4). For `api`-kind applications with an owner, requests act for that owning\n  User/Organization at edit-level authority (destroy-level operations always require a human token).\n- `authorization_code` — `code`, `redirect_uri`, `client_id`, `client_secret` (if confidential), and PKCE\n  `code_verifier` when the client used `code_challenge` on `/oauth/authorize`.\n\n**First-party apps** without embedding a client secret can use `POST /users/sign-in` or\n`POST /users/access-tokens` instead; those return the same token fields plus `user_id` in JSON.\n\n**Revocation:** use `POST /oauth/revoke` or `DELETE /api/v1/users/sign-out` with the access token.\n",
        "operationId": "oauthToken",
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "required": [
                  "grant_type"
                ],
                "properties": {
                  "grant_type": {
                    "type": "string",
                    "description": "OAuth 2.0 grant type selector.",
                    "enum": [
                      "password",
                      "refresh_token",
                      "client_credentials",
                      "authorization_code"
                    ]
                  },
                  "username": {
                    "type": "string",
                    "format": "email",
                    "description": "Resource owner email (`grant_type=password`)."
                  },
                  "password": {
                    "type": "string",
                    "description": "Resource owner password (`grant_type=password`)."
                  },
                  "refresh_token": {
                    "type": "string",
                    "description": "Refresh token (`grant_type=refresh_token`)."
                  },
                  "organization_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Scope the token to act for an organization the user holds an\naccepted role in (`grant_type=password`). Carried on the\ntoken, preserved across refresh, and re-validated on every\nrequest — removing the member's role de-scopes the token.\nRole authority still applies (e.g. api-kind server clients\nare additionally bound to exactly this organization).\n"
                  },
                  "code": {
                    "type": "string",
                    "description": "Authorization code (`grant_type=authorization_code`)."
                  },
                  "redirect_uri": {
                    "type": "string",
                    "format": "uri",
                    "description": "Must match the redirect URI used in `/oauth/authorize` (`authorization_code`)."
                  },
                  "client_id": {
                    "type": "string",
                    "description": "OAuth application UID from Doorkeeper."
                  },
                  "client_secret": {
                    "type": "string",
                    "description": "Required for confidential clients."
                  },
                  "scope": {
                    "type": "string",
                    "description": "Requested scopes (space-separated)."
                  },
                  "code_verifier": {
                    "type": "string",
                    "description": "PKCE code verifier (`grant_type=authorization_code` when PKCE was used)."
                  },
                  "resource": {
                    "type": "string",
                    "format": "uri",
                    "description": "Must match the MCP resource bound at consent; carried unchanged across refresh. Optional for existing clients."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Token issued.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DoorkeeperAccessTokenResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request or invalid grant.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Invalid client credentials."
          }
        }
      }
    },
    "/oauth/revoke": {
      "servers": [
        {
          "url": "https://{defaultHost}",
          "variables": {
            "defaultHost": {
              "default": "api.purplegrain.com"
            }
          }
        }
      ],
      "post": {
        "tags": [
          "OAuth"
        ],
        "summary": "OAuth 2.0 token revocation",
        "description": "Revokes an access or refresh token (RFC 7009). Body is `application/x-www-form-urlencoded`.\nSame effect as `DELETE /api/v1/users/sign-out` when revoking the current access token.\n",
        "operationId": "oauthRevoke",
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "properties": {
                  "token": {
                    "type": "string",
                    "description": "The access or refresh token to revoke."
                  },
                  "token_type_hint": {
                    "type": "string",
                    "enum": [
                      "access_token",
                      "refresh_token"
                    ]
                  },
                  "client_id": {
                    "type": "string"
                  },
                  "client_secret": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Token revoked or request acknowledged (per RFC 7009)."
          }
        }
      }
    },
    "/oauth/authorize": {
      "servers": [
        {
          "url": "https://{defaultHost}",
          "variables": {
            "defaultHost": {
              "default": "api.purplegrain.com"
            }
          }
        }
      ],
      "get": {
        "tags": [
          "OAuth"
        ],
        "summary": "OAuth 2.0 authorization endpoint (authorization code + PKCE)",
        "description": "Starts the **authorization code** flow. The resource owner must be known to the server (e.g. via external\nlogin and `OAUTH_RESOURCE_OWNER_LOGIN_URL`); this API does not use a Rails session cookie for that step.\n\nPublic clients must send PKCE: `code_challenge` and `code_challenge_method=S256`.\n",
        "operationId": "oauthAuthorize",
        "parameters": [
          {
            "name": "client_id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "redirect_uri",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uri"
            }
          },
          {
            "name": "response_type",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "code"
              ]
            }
          },
          {
            "name": "scope",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "state",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "code_challenge",
            "in": "query",
            "description": "PKCE code challenge (S256).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "code_challenge_method",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "S256"
              ]
            }
          },
          {
            "name": "resource",
            "in": "query",
            "description": "Resource indicator (RFC 8707); must be the exact MCP resource URI advertised by discovery.",
            "schema": {
              "type": "string",
              "format": "uri"
            }
          }
        ],
        "responses": {
          "302": {
            "description": "Redirect to `redirect_uri` with `code` or OAuth error query parameters."
          },
          "401": {
            "description": "Resource owner not authenticated."
          }
        }
      }
    },
    "/oauth/register": {
      "servers": [
        {
          "url": "https://{defaultHost}",
          "variables": {
            "defaultHost": {
              "default": "api.purplegrain.com"
            }
          }
        }
      ],
      "post": {
        "tags": [
          "OAuth"
        ],
        "summary": "Dynamic Client Registration",
        "description": "**RFC 7591** Dynamic Client Registration. One unauthenticated endpoint that\nlets any compliant OAuth/MCP client self-register and receive its own\n`client_id`, so AI chatbots (ChatGPT, Claude, Cursor, …) onboard with zero\nmanual configuration.\n\nRegistration alone grants no data access: a registered client can only reach\nresources after a Purple Grain user completes the interactive\n`GET /oauth/authorize` login + consent (PKCE is enforced). Most clients\nshould register as public (`token_endpoint_auth_method=none`).\n",
        "operationId": "registerClient",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ClientRegistrationRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Client registered.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientRegistrationResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid client metadata or redirect URI.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/oauth/authorize/grant": {
      "servers": [
        {
          "url": "https://{defaultHost}",
          "variables": {
            "defaultHost": {
              "default": "api.purplegrain.com"
            }
          }
        }
      ],
      "post": {
        "tags": [
          "OAuth"
        ],
        "summary": "Headless authorization grant",
        "description": "Mints an authorization code on behalf of an already-authenticated user.\nBecause sessions are disabled on the API, the interactive\n`GET /oauth/authorize` consent UI cannot establish a resource owner; the\nfrontend authenticates the user, shows the consent screen, and POSTs the\noriginal authorize params here with the user's **`Authorization: Bearer`**\ntoken. PKCE is enforced exactly as in the standard flow. Returns the\nredirect URI (carrying `code` + `state`) for the browser to follow back to\nthe client, which then exchanges the code at `POST /oauth/token`.\n",
        "operationId": "createAuthorizationGrant",
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AuthorizationGrantRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Authorization code issued.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthorizationGrantResponse"
                }
              }
            }
          },
          "400": {
            "description": "Authorization request is not authorizable (invalid client, redirect URI, scope, or PKCE).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/oauth/authorize/preview": {
      "servers": [
        {
          "url": "https://{defaultHost}",
          "variables": {
            "defaultHost": {
              "default": "api.purplegrain.com"
            }
          }
        }
      ],
      "post": {
        "tags": [
          "OAuth"
        ],
        "operationId": "previewAuthorizationRequest",
        "summary": "Validate a consent request before showing its client and permissions",
        "description": "Public first-party consent helper. Checks client registration, callback, scopes, resource and S256 PKCE. Creates no grant.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AuthorizationGrantRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Validated client identity and requested permissions.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "client_name",
                    "scopes",
                    "resource"
                  ],
                  "properties": {
                    "client_name": {
                      "type": "string"
                    },
                    "scopes": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "resource": {
                      "type": "string",
                      "format": "uri"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request; the consent UI must not enable approval or follow the query callback.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/oauth/authorize/deny": {
      "servers": [
        {
          "url": "https://{defaultHost}",
          "variables": {
            "defaultHost": {
              "default": "api.purplegrain.com"
            }
          }
        }
      ],
      "post": {
        "tags": [
          "OAuth"
        ],
        "operationId": "denyAuthorizationRequest",
        "summary": "Return a validated denial callback",
        "description": "Public first-party consent helper. Applies the same validation as approval and returns the registered callback carrying access_denied and state. Creates no grant.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AuthorizationGrantRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Validated callback carrying the OAuth denial.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthorizationGrantResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request; no callback is returned.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/oauth/mcp-token": {
      "servers": [
        {
          "url": "https://{defaultHost}",
          "variables": {
            "defaultHost": {
              "default": "api.purplegrain.com"
            }
          }
        }
      ],
      "get": {
        "tags": [
          "OAuth"
        ],
        "operationId": "authenticateMcpRequest",
        "summary": "Authenticate the current MCP bearer and resource",
        "description": "Used by the MCP transport before every request. Requires a live human-backed MCP token for the configured resource, never accepts query/body tokens, and returns no credentials or personal data. Uncached.",
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Current bearer is valid for the MCP resource.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "active",
                    "resource",
                    "scopes",
                    "client_id"
                  ],
                  "properties": {
                    "active": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "resource": {
                      "type": "string",
                      "format": "uri"
                    },
                    "scopes": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "client_id": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid, expired, revoked, wrong-channel or wrong-resource bearer. Includes an OAuth resource-metadata challenge.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/.well-known/oauth-authorization-server": {
      "servers": [
        {
          "url": "https://{defaultHost}",
          "variables": {
            "defaultHost": {
              "default": "api.purplegrain.com"
            }
          }
        }
      ],
      "get": {
        "tags": [
          "OAuth"
        ],
        "summary": "Authorization Server Metadata",
        "description": "**RFC 8414** discovery document. Unauthenticated. Lets MCP/OAuth clients\nbootstrap the auth flow with no manual configuration — it advertises the\nauthorization, token, registration, introspection, and revocation\nendpoints plus supported grant types, scopes, and PKCE methods.\n",
        "operationId": "getOAuthAuthorizationServerMetadata",
        "responses": {
          "200": {
            "description": "Authorization server metadata.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthServerMetadata"
                }
              }
            }
          }
        }
      }
    },
    "/.well-known/oauth-protected-resource": {
      "servers": [
        {
          "url": "https://{defaultHost}",
          "variables": {
            "defaultHost": {
              "default": "api.purplegrain.com"
            }
          }
        }
      ],
      "get": {
        "tags": [
          "OAuth"
        ],
        "summary": "Protected Resource Metadata",
        "description": "**RFC 9728** discovery document. Unauthenticated. Describes the MCP\nresource server and points clients back at this authorization server.\nClients may also probe the resource-suffixed form\n`/.well-known/oauth-protected-resource/mcp`.\n",
        "operationId": "getOAuthProtectedResourceMetadata",
        "responses": {
          "200": {
            "description": "Protected resource metadata.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthProtectedResourceMetadata"
                }
              }
            }
          }
        }
      }
    },
    "/.well-known/oauth-protected-resource/mcp": {
      "servers": [
        {
          "url": "https://{defaultHost}",
          "variables": {
            "defaultHost": {
              "default": "api.purplegrain.com"
            }
          }
        }
      ],
      "get": {
        "tags": [
          "OAuth"
        ],
        "summary": "Protected Resource Metadata (MCP resource)",
        "description": "Resource-suffixed form of `/.well-known/oauth-protected-resource` (RFC 9728)\nthat some clients probe for the `/mcp` resource. Returns the same metadata.\n",
        "operationId": "getMcpProtectedResourceMetadata",
        "responses": {
          "200": {
            "description": "Protected resource metadata.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthProtectedResourceMetadata"
                }
              }
            }
          }
        }
      }
    },
    "/addresses/{id}": {
      "get": {
        "operationId": "getAddress",
        "summary": "Get a saved address by ID",
        "description": "Retrieve an existing PurpleGrain address using its address ID, not a Google place ID. Returns its street number, route, city, subdivision, country and Google place ID. Use include to expand the city or location coordinates. This lookup does not change data.",
        "tags": [
          "Addresses"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "ID of an existing PurpleGrain address.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IncludeAddress"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful. When the `include` query parameter is used, the response may contain an `included` array (JSON:API) or inlined related resources (application/json).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AddressesShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/AddressesShowResponse"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/asset-resources": {
      "get": {
        "summary": "List Asset Resources",
        "operationId": "listAssetResources",
        "description": "Everything filed on one paddock or one mob. `resources` lists them\nwithout a follow-up call: the property (`type: farm`), the paddock a\nmob is standing in (`type: paddock`), the live mobs standing in a\npaddock (`type: mob`), and the tasks and records (`type: task`).\n\nA move is a task whose asset is the mob. On a paddock it is still\nlisted, because the move names that paddock as where the mob left or\narrived. Placement uses the newest completed record that names a\ndestination, the same rule as `current_paddock_id`.\n\n`asset_type` is `paddock` or `mob` (singular or plural). Omit it and\n`asset_id` is tried as a paddock, then as a mob. Tasks are one page\n(`page`, `per_page`, default 25) in board order; `meta` carries the\npage. The property, the current paddock and the mobs are always\ncomplete, not paged. Pass `include` for the full records.\n\nThe caller must be able to read the asset: an accepted member of the\nowning business, or the person who owns a personal property.\n",
        "tags": [
          "Asset resources"
        ],
        "security": [
          {},
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "asset_id",
            "in": "query",
            "required": true,
            "description": "The paddock or mob.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "asset_type",
            "in": "query",
            "required": false,
            "description": "`paddock` or `mob`, singular or plural. Omitted, `asset_id` is\nresolved as a paddock and then as a mob. Deliberately no enum: a\nschema stricter than the lookup rejects a plural the day request\nvalidation is enforced.\n",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page of tasks and records. The property, paddock and mobs are not paged.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Tasks and records per page. Defaults to 25.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "$ref": "#/components/parameters/IncludeAssetResources"
          }
        ],
        "responses": {
          "200": {
            "description": "The asset and the resources filed on it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssetResourcesShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/AssetResourcesShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found — `asset_id` is missing or names nothing the type can resolve.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/attachments": {
      "post": {
        "summary": "Upload an attachment",
        "operationId": "createAttachment",
        "description": "Pre-upload a photo for the job composer (design handoff 11): each photo\nuploads on its own before the job exists, so a retried create never\nre-uploads and the composer can show thumbnails as each one lands. Jobs\nreference attachments by id (POST /tasks/ai `attachment_ids`).\nOwner-scoped at creation so an attachment can't be attached to another\nowner's job later.\n",
        "tags": [
          "Attachments"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [],
        "responses": {
          "201": {
            "description": "The stored attachment.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AttachmentsShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/AttachmentsShowResponse"
                }
              }
            }
          },
          "400": {
            "description": "No usable image source provided"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Not allowed to upload for the requested owner"
          }
        },
        "requestBody": {
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "The image file (jpeg, png, gif, webp)."
                  },
                  "image_url": {
                    "type": "string",
                    "description": "Agent-friendly alternative — a remote http(s) image the server fetches."
                  },
                  "image_base64": {
                    "type": "string",
                    "description": "Agent-friendly alternative — base64 image data (optionally a data URI)."
                  },
                  "owner_type": {
                    "type": "string",
                    "enum": [
                      "user",
                      "organization"
                    ],
                    "description": "Owner of the upload; defaults to the current user."
                  },
                  "owner_id": {
                    "type": "string"
                  }
                }
              }
            }
          },
          "required": true,
          "description": "The image to store"
        }
      }
    },
    "/attachments/{id}": {
      "get": {
        "operationId": "getAttachment",
        "summary": "Get an attachment",
        "description": "Returns the attachment's metadata, including the URL of the stored photo.\nRequesting the bytes themselves needs an image extension or an image\nAccept type on the HTTP request. An assistant's tool call cannot set\nthat, so use the URL. Ids are unguessable UUIDs.\n",
        "tags": [
          "Attachments"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the record",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AttachmentsShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/AttachmentsShowResponse"
                }
              }
            }
          },
          "404": {
            "description": "Not found"
          }
        }
      }
    },
    "/image-uploads": {
      "post": {
        "operationId": "createImageUpload",
        "summary": "Create Image Upload",
        "description": "Stage a photo on the agent's disk that the person wants attached to a\ntask or record. Send `filename`, `content_type` and `byte_size` (and an\noptional `checksum`, standard base64 of the raw MD5 or SHA-256 of the\nfile). The response `data` object carries `upload_id`, a one-time\n`upload_url` on this API host, `method` (`PUT`), `headers`, `expires_at`\n(about 30 minutes), `max_bytes` (20 MB) and `curl_example`.\n\n`curl_example` is the ready-made PUT of the file, run from the directory\nthat contains it. The URL authenticates itself: no bearer token. The PUT returns\n`{ upload_id, status: \"uploaded\", content_type, byte_size }`. HEIC and\nHEIF are stored as JPEG, EXIF orientation is applied, the capture time\nis kept and GPS is removed.\n\nThen pass `upload_id` as `image_upload_ids` on the create or update.\n`addTaskImages` appends those photos to a task. `updateTaskImage` is the\nlegacy call that replaces the gallery with one image; prefer\n`addTaskImages`, or `updateTask` with `image_mode` `replace`.\nNever base64-encode a local photo into a tool call. A public http(s)\nURL uses `image_urls` instead. `image_base64s` is only for a tiny\ngenerated image under about 100 KB decoded.\n\nOne upload can be attached to several records (up to 20) until it\nexpires, 24 hours after it was staged. This call does not write farm\ndata and does not need `confirm`. The call that attaches the photo still\ndoes. An `Idempotency-Key` header makes a retry of this call return the\nsame upload.\n",
        "tags": [
          "Image uploads"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "responses": {
          "201": {
            "description": "The staged upload. Run `data.curl_example`, then pass `data.upload_id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImageUploadResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ImageUploadResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "422": {
            "description": "The filename, content type or byte size was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/ImageUploadCreateModel"
              }
            },
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ImageUploadCreateModelAttributes"
              }
            }
          }
        }
      }
    },
    "/image-uploads/{token}": {
      "put": {
        "operationId": "putImageUpload",
        "summary": "Upload Image Bytes",
        "x-mcp-excluded": true,
        "description": "The one-time PUT from `createImageUpload`. The token in the path is the\ncredential (this user, this upload, this byte size); send no bearer\ntoken. The body is the raw file and must be exactly `byte_size` bytes,\nat most 20 MB, and its magic bytes must match the declared content type.\nA second PUT, or a PUT after `expires_at`, is refused. Not an MCP tool:\nthe agent runs `curl_example` from the shell.\n",
        "tags": [
          "Image uploads"
        ],
        "security": [],
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "description": "The credential from `upload_url`. Not the `upload_id`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "image/jpeg": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "image/png": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "image/webp": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "image/heic": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "image/heif": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "application/octet-stream": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The file is stored. `content_type` is the stored type (HEIC and HEIF become image/jpeg).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImageUploadPutResponse"
                }
              }
            }
          },
          "422": {
            "description": "Expired, already used, the wrong size, a checksum mismatch, or magic bytes that do not match content_type.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/categories": {
      "get": {
        "operationId": "listCategories",
        "summary": "List agricultural service categories",
        "description": "List the category catalogue, returning each category's ID, name and type. Use the exact name as the category filter for service or task searches. Results are paginated with page and per_page; meta provides current_page, next_page and prev_page. This lookup does not change data.",
        "tags": [
          "Categories"
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Select within paginated results",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Number results to return",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CategoriesIndexResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/CategoriesIndexResponse"
                }
              }
            }
          }
        }
      }
    },
    "/categories/{id}": {
      "get": {
        "operationId": "getCategory",
        "summary": "Get a category by ID",
        "description": "Retrieve one category using its PurpleGrain category ID. Returns the category's ID, name and type. Use its exact name when filtering service or task searches by category. This lookup does not change data.",
        "tags": [
          "Categories"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the record",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CategoriesShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/CategoriesShowResponse"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/cities/ip": {
      "get": {
        "operationId": "getCityByIp",
        "summary": "Resolve or create a city from an IP address",
        "description": "Resolve an IPv4 or IPv6 address the person supplies through the local GeoIP catalogue. If `ip` is omitted, the API uses the request's remote IP, which over MCP is the server's address rather than the person's. Reuses an existing city with the same name, subdivision and country, or persists a shared city and location record when missing. Returns the saved city ID and fields; request `include=location` for coordinates. This lookup can create records even though the API uses GET, so its MCP tool requires `confirm: true` when confirmation is enabled. Returns 404 when the GeoIP data has no city-level location.",
        "tags": [
          "Cities"
        ],
        "parameters": [
          {
            "name": "ip",
            "in": "query",
            "required": false,
            "description": "An IPv4 or IPv6 address the person supplies. Omitted, the request's remote address is used, which over MCP is the server's.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IncludeCity"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful. When the `include` query parameter is used, the response may contain an `included` array (JSON:API) or inlined related resources (application/json).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CitiesShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/CitiesShowResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "404": {
            "description": "Valid IP but no city-level GeoIP record (e.g. datacenter/VPN egress) — treat as \"no location default\", not an error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/cities/place": {
      "get": {
        "summary": "Resolve or create a city from a Google Place ID",
        "operationId": "getCityByPlace",
        "x-mcp-excluded": true,
        "description": "Resolve a Google Places `place_id` to a saved city. Reuses an existing match or sends the place ID to Google Places and persists a shared city and location record when missing. Returns the saved city ID and fields; request `include=location` for coordinates. A newly created city can subsequently be read with `getCity`. An AI assistant cannot call this; the app and an API key still can. An assistant resolves a place with `getPrediction`.",
        "tags": [
          "Cities"
        ],
        "parameters": [
          {
            "name": "place_id",
            "in": "query",
            "required": true,
            "description": "Google Places place_id to resolve to a city.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IncludeCity"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful. When the `include` query parameter is used, the response may contain an `included` array (JSON:API) or inlined related resources (application/json).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CitiesShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/CitiesShowResponse"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/cities/{id}": {
      "get": {
        "operationId": "getCity",
        "summary": "Get a saved city by ID",
        "description": "Retrieve an existing PurpleGrain city using its city ID, not a Google place ID or IP address. Returns its name, subdivision, country and Google place ID. Use include=location to embed its coordinates and accuracy. This lookup reads saved data without resolving or creating a place.",
        "tags": [
          "Cities"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the record",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IncludeCity"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful. When the `include` query parameter is used, the response may contain an `included` array (JSON:API) or inlined related resources (application/json).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CitiesShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/CitiesShowResponse"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/clients": {
      "get": {
        "summary": "List Clients",
        "operationId": "listClients",
        "description": "The caller's client contact book, ordered by name. Pass owner_id/owner_type\nto read an organization's clients (requires edit rights on the\norganization). Pass q to search by name or location.\n",
        "tags": [
          "Clients"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "owner_id",
            "in": "query",
            "required": false,
            "description": "Owner id to scope the list to (defaults to the current user).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "owner_type",
            "in": "query",
            "required": false,
            "description": "Owner type, \"User\" or \"Organization\".",
            "schema": {
              "type": "string",
              "enum": [
                "User",
                "Organization"
              ]
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Case-insensitive search over client name and location.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Select within paginated results",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Number results to return",
            "schema": {
              "type": "integer"
            }
          },
          {
            "$ref": "#/components/parameters/IncludeClient"
          }
        ],
        "responses": {
          "200": {
            "description": "Clients for the requested owner. When the `include` query parameter is used, the response may contain an `included` array (JSON:API) or inlined related resources (application/json).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientsIndexResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientsIndexResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          }
        }
      },
      "post": {
        "summary": "Create Client",
        "operationId": "createClient",
        "description": "Add a client to the contact book. The owner is the current user; an\nOrganization owner is accepted when the caller can edit that organization.\nThe mobile number is normalized to E.164 (+61…).\n",
        "tags": [
          "Clients"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IncludeClient"
          }
        ],
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientsShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientsShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "422": {
            "description": "Unprocessable Entity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ClientCreateModelAttributes"
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/ClientCreateModel"
              }
            }
          },
          "required": true,
          "description": "Create client attributes"
        }
      }
    },
    "/clients/{id}": {
      "get": {
        "summary": "Get a client contact by ID",
        "operationId": "getClient",
        "description": "Retrieve one saved client contact by its PurpleGrain client ID. Returns its name, mobile, email, location, task count and last task date; include can expand its owner or linked user. Requires permission to manage the owning account or organization, including write access in an assistant connection's grant. This lookup does not modify the contact or send messages.",
        "tags": [
          "Clients"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the record",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IncludeClient"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful. When the `include` query parameter is used, the response may contain an `included` array (JSON:API) or inlined related resources (application/json).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientsShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientsShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      },
      "put": {
        "summary": "Update Client",
        "operationId": "updateClient",
        "tags": [
          "Clients"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the record",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IncludeClient"
          }
        ],
        "responses": {
          "200": {
            "description": "Updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientsShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientsShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Entity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ClientUpdateModelAttributes"
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/ClientUpdateModel"
              }
            }
          },
          "required": true,
          "description": "Update client attributes"
        }
      },
      "delete": {
        "summary": "Delete a client contact",
        "operationId": "deleteClient",
        "description": "Removes this contact from the book. It does not delete tasks. Needs\nedit rights on the contact's owner.\n",
        "tags": [
          "Clients"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the record",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/contractors": {
      "get": {
        "summary": "Contractors directory",
        "operationId": "listContractors",
        "description": "The public \"Find contractors\" directory: providers (Users and\nOrganizations) grouped from the active-service pool, with the same\nfilters as the services search (category, term, location radius).\nOrdering follows the underlying search — nearest first when latitude/longitude are\ngiven. `liked` reflects the caller's Liked-contractors set when signed\nin (always false when signed out).\n",
        "tags": [
          "Contractors"
        ],
        "parameters": [
          {
            "name": "category",
            "in": "query",
            "required": false,
            "description": "Category name to filter on — a single, exact `Category.name` (see `GET /categories`). This is the only category filter; ids and multiple values are not accepted. An unknown name returns no results.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "term",
            "in": "query",
            "required": false,
            "description": "Free-text search over the underlying services.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "latitude",
            "in": "query",
            "required": false,
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "longitude",
            "in": "query",
            "required": false,
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "radius",
            "in": "query",
            "required": false,
            "description": "Metres; combined with latitude/longitude.",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Providers grouped from matching services.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContractorsIndexResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ContractorsIndexResponse"
                }
              }
            }
          }
        }
      }
    },
    "/mobs/{mob_id}/drench-records": {
      "post": {
        "operationId": "createDrenchRecord",
        "x-mcp-excluded": true,
        "summary": "Create Drench Record",
        "description": "Deprecated alias for recording a drench on one mob. The app and an API key can still call\nit. An AI assistant cannot: the response is 403 with detail \"An AI assistant cannot call\nthis.\" An assistant records a drench with `createTreatmentRecord` and `category` `drench`.\n\nCreates the one task that is the record and answers with that task, `drench` included,\nexactly as `GET /tasks/{id}` would. The server sets the producer from the farm's owner,\n`operation_type` to `treatment`, `capture_source` to `agent`, `operator` from the signed-in\nuser, and the treatment category to `drench`. `head` defaults to the mob's current head\ncount. A mob with no head count yet takes the `head` recorded as its count; a count already\nset is never changed. `date` (`YYYY-MM-DD`) defaults to today and is stored as midday in\nthe application time zone; a future date creates the open task (`status: pending`,\n`created_as: task`) instead of a completed record.\n\nSend one `products` entry per product (name, active, batch, dose, `whp_days`, `esi_days`,\nand `expiry` and the dose parts when the farmer stated them). If the label has no ESI, send\nthe WHP number as `esi_days`. This alias does not take `category` or `route`. A completed\nrecord with no product row is refused (422, \"A drench needs at least one product\").\n\nPhotos travel the same way as on a treatment (`image_upload_ids`, `image_urls`,\n`image_base64s`). If any image fails, the whole create is rolled back.\n\nExample (`application/vnd.api+json`):\n\n```json\n{\n  \"data\": {\n    \"type\": \"drench-record\",\n    \"attributes\": {\n      \"date\": \"2025-12-23\",\n      \"head\": 11,\n      \"description\": \"Drench rams\",\n      \"products\": [\n        {\n          \"name\": \"Startect\",\n          \"batch\": \"728661\",\n          \"dose\": \"20 mL per head\",\n          \"whp_days\": 14,\n          \"esi_days\": 28\n        }\n      ]\n    }\n  }\n}\n```\n**Photos.** A file on disk is staged with `createImageUpload`, whose `curl_example` PUTs the bytes, and attached with `image_upload_ids`. Never base64-encode a local photo into a tool call. A public http(s) URL: pass `image_urls`. Only a tiny generated image (under about 100 KB decoded) uses `image_base64s`. One upload can be attached to several records until it expires. On update, `image_mode` defaults to `append` (add to the existing photos); `replace` swaps them all out; `remove_image_ids` deletes specific ones. If any image fails, the whole create or update is rolled back. The attaching call still needs `confirm: true`. `createImageUpload` and its PUT do not.\n",
        "tags": [
          "Drench records"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "mob_id",
            "in": "path",
            "required": true,
            "description": "The mob that was drenched.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Created. The task, with its `drench`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated"
          },
          "403": {
            "description": "Not allowed to write on this mob, or an AI assistant (detail \"An AI assistant cannot call this.\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such mob."
          },
          "422": {
            "description": "Invalid request."
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DrenchRecordCreateModelAttributes"
              },
              "example": {
                "data": {
                  "date": "2025-12-23",
                  "head": 11,
                  "description": "Drench rams",
                  "products": [
                    {
                      "name": "Startect",
                      "batch": "728661",
                      "dose": "20 mL per head",
                      "whp_days": 14,
                      "esi_days": 28
                    }
                  ]
                }
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/DrenchRecordCreateModel"
              },
              "example": {
                "data": {
                  "type": "drench-record",
                  "attributes": {
                    "date": "2025-12-23",
                    "head": 11,
                    "description": "Drench rams",
                    "products": [
                      {
                        "name": "Startect",
                        "batch": "728661",
                        "dose": "20 mL per head",
                        "whp_days": 14,
                        "esi_days": 28
                      }
                    ]
                  }
                }
              }
            }
          },
          "description": "Drench record attributes"
        }
      }
    },
    "/drench-records/{id}": {
      "put": {
        "operationId": "updateDrenchRecord",
        "x-mcp-excluded": true,
        "summary": "Update Drench Record",
        "description": "Deprecated alias for changing a drench's head and products. The app and an API key can still\ncall it. An AI assistant cannot: the response is 403 with detail \"An AI assistant cannot call\nthis.\" An assistant uses `updateTreatmentRecord`.\n\n`products` replaces the set. On a done record this never reopens the task; on an open task\nthe same child is updated. Answers with the task exactly as `GET /tasks/{id}` would. `id` is\nthe treatment id (`drench.id` on the task, the same value as `treatment.id`), not the task\nid. A done record with no product row is refused (422, \"A drench needs at least one\nproduct\"). Photos travel the same way as on a treatment update (`image_upload_ids`,\n`image_urls`, `image_base64s`, `image_mode`, `remove_image_ids`). If any image fails, the\nwhole update is rolled back.\n",
        "tags": [
          "Drench records"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The DrenchRecord id (`drench.id` on the task).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success. The task, with its `drench`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated"
          },
          "403": {
            "description": "Not allowed to write on this record, or an AI assistant (detail \"An AI assistant cannot call this.\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such record."
          },
          "422": {
            "description": "Invalid request."
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DrenchRecordUpdateModelAttributes"
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/DrenchRecordUpdateModel"
              }
            }
          },
          "description": "Drench record attributes"
        }
      }
    },
    "/farms": {
      "get": {
        "summary": "List Farms",
        "operationId": "listFarms",
        "description": "The properties of one owner, ordered by name: a business's,\nwith `organization_id`, or — with no `organization_id` — the signed-in\nuser's own personal properties. A property owned by a business does not\nappear in that personal list — pass `organization_id` or the farm is\nmissing. Find those businesses with `listOrganizationRoles` on the\nsigned-in user (`fetchCurrentUser`). Members of a business can list its\nfarms; editing requires business edit rights. A personal property is\nthe person's alone.\n\nEach farm carries its outline as `boundary` — an array of closed rings of\n`[longitude, latitude]` pairs, **longitude first**, never a GeoJSON\nobject —       plus its map point and the live `paddock_count` / `mob_count`.\nOne property with every paddock and mob is `getFarmPicture`. The\npaddocks or mobs alone are `listPaddocks` / `listMobs` with `farm_id`.\nBefore recording a drench, `getFarmPicture` is every mob on the property\nin one call.\n",
        "tags": [
          "Farms"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "organization_id",
            "in": "query",
            "required": false,
            "description": "The business whose farms to list. Omit it for the signed-in user's personal properties.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "$ref": "#/components/parameters/IncludeFarm"
          }
        ],
        "responses": {
          "200": {
            "description": "Farms for the owner.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FarmsIndexResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/FarmsIndexResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          }
        }
      },
      "post": {
        "summary": "Create Farm",
        "operationId": "createFarm",
        "description": "A name makes a property. It belongs to the business named by the\n`organization` relationship, or — with no `organization` — to the\nsigned-in user's personal account (the `user` relationship may name\nthem explicitly). `centroid_lat` / `centroid_lon` give a property with\nno outline yet a point for the map to open on — send both or neither.\n\n`boundary`, `area_ha` and `area_source` are not accepted here: write the\noutline with `updateFarmBoundary`, which measures the area. Requires\nedit rights on the owner: a business's edit roles, or the person.\nAssistants and API keys cannot set `visibility` to `link` or `public`\n(403, nothing written). `private`, or leaving it unset, is allowed.\n",
        "tags": [
          "Farms"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IncludeFarm"
          }
        ],
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FarmsShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/FarmsShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "422": {
            "description": "Unprocessable Entity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FarmCreateModelAttributes"
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/FarmCreateModel"
              }
            }
          },
          "required": true,
          "description": "Create farm attributes"
        }
      }
    },
    "/farms/{id}": {
      "get": {
        "summary": "Get a property",
        "operationId": "getFarm",
        "description": "One property: its name and labels, the point the map opens on\n(`centroid_lat` / `centroid_lon`), `area_ha` with its `area_source`, the\nlive `paddock_count` / `mob_count`, its outline, and its `blocks` (each\nblock's name, kind, paddock ids, and a derived area and outline).\n\n`boundary` is an array of closed rings, each an array of\n`[longitude, latitude]` pairs — **longitude first**, never a GeoJSON\nobject — or null while the property has none; `boundary_source` says how\nit was captured. Write it with `updateFarmBoundary`. The paddocks and\nmobs on the property, with their own outlines and counts, are\n`getFarmPicture`. Any accepted member of the owning business can read; a\npersonal property is readable by its owner alone.\n",
        "tags": [
          "Farms"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the record",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IncludeFarm"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FarmsShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/FarmsShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      },
      "put": {
        "summary": "Update Farm",
        "operationId": "updateFarm",
        "description": "Rename the property or change its labels (`location`, `size`,\n`place_label`) and its map point (`centroid_lat` / `centroid_lon`, both\nor neither). The owner cannot be changed.\n\n`boundary`, `area_ha` and `area_source` are not accepted — see\n`updateFarmBoundary`. Requires edit rights on the owner. Assistants and\nAPI keys cannot widen `visibility` to `link` or `public`. Setting it to\n`private`, or leaving it unchanged, is allowed.\n",
        "tags": [
          "Farms"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the record",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IncludeFarm"
          }
        ],
        "responses": {
          "200": {
            "description": "Updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FarmsShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/FarmsShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Entity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FarmUpdateModelAttributes"
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/FarmUpdateModel"
              }
            }
          },
          "required": true,
          "description": "Update farm attributes"
        }
      },
      "delete": {
        "summary": "Delete Farm",
        "operationId": "deleteFarm",
        "description": "Removes the property for good. This cannot be undone. It also removes\nevery paddock and mob on it, live or archived.\n\nUnlike `deletePaddock` and `deleteMob`, this is not refused while\nrecords exist: the tasks and records filed on the property, its\npaddocks and its mobs are kept on the owner's board, but their\nproperty, paddock and mob links are cleared. To keep that history\nattached, archive the paddocks and mobs instead. Requires edit rights\non the owner.\n",
        "tags": [
          "Farms"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the record",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/farms/{id}/boundary": {
      "put": {
        "summary": "Update Farm Boundary",
        "operationId": "updateFarmBoundary",
        "description": "Save (or clear) the property outline, and with it the geocoded map\nlabel and the point the map opens on.\n\nA dedicated endpoint rather than attributes on `updateFarm`, because it\nis the single place three rules are enforced:\n\n* **A typed area is never overwritten by a drawn one.**\n  `area_source: \"drawn\"` recomputes `area_ha` from the ring;\n  `area_source: \"typed\"` saves the ring and leaves `area_ha` exactly as\n  it is. A request that names no source keeps the property's own, so\n  omitting the field can never clobber a typed number; a property that\n  has never had one gets `drawn`.\n* **Geometry is repaired before it is measured.** Rings are validated\n  (at least 4 positions, closed, finite, latitude within ±90 and\n  longitude within ±180), repaired with `ST_MakeValid`, wound\n  counter-clockwise and stored as a MultiPolygon — so a self-crossing\n  ring cannot report a plausible-but-wrong hectare figure. The geometry\n  read back may therefore be reordered or split into parts.\n* **A saved ring always leaves the map a point to open on.** When the\n  property has no centroid and the request names none, one is derived\n  from the ring. An existing centroid is never moved.\n\nArea is computed by PostGIS as `ST_Area(geography)` in hectares.\n\n**Provenance.** `boundary_source` (traced | grid | gps | imported) is\nthe client's claim about how the ring was captured and travels with it;\n`boundary_captured_by` is stamped server-side from the acting user and\nis never accepted from the request. Both are cleared with the ring. A\nproperty boundary writes no diary row — records attach to paddocks and\nmobs, not to a property — and it is never distance-guarded the way a\npaddock ring is: the property's own point may have been geocoded to the\ntown, and the ring is what corrects it. Requires edit rights on the\nowner.\n",
        "tags": [
          "Farms"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the record",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IncludeFarm"
          }
        ],
        "responses": {
          "200": {
            "description": "The property with its saved boundary.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FarmsShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/FarmsShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "422": {
            "description": "A ring with fewer than 4 positions, an unclosed ring, a coordinate out of range, geometry that cannot be repaired into a polygon, a half-written centroid pair, or an unknown `boundary_source`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FarmBoundaryModelAttributes"
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/FarmBoundaryModel"
              }
            }
          },
          "required": true,
          "description": "The property outline, plus the area source, capture provenance, map label and centroid that travel with it"
        }
      }
    },
    "/farms/{id}/picture": {
      "get": {
        "summary": "Get a property with all paddocks and mobs",
        "operationId": "getFarmPicture",
        "description": "One property in full — the call that lists every paddock and every mob\nbefore a drench is recorded — whether it belongs to the signed-in user\nor to a business: its name and labels, the point the map opens on, `area_ha`,\nthe live `paddock_count` / `mob_count`, its outline and its `blocks`, plus\nevery paddock and every mob on it.\n\n`boundary` — on the property and on each paddock — is an array of closed\nrings of `[longitude, latitude]` pairs, **longitude first**, never a\nGeoJSON object, or null while that outline has not been drawn. A paddock\nalso carries `block_id` (its block, null in the home block), land use,\ncrop, archive state and diary counters. A mob\ncarries `head_count`, archive state, diary counters and\n`current_paddock_id`. Archived assets are included; `paddock_count` and\n`mob_count` stay live-only.\n\nWith `Accept: application/vnd.api+json` the assets are in `included`.\nWith `Accept: application/json` they are inlined as `paddocks` and\n`mobs` on the farm. What is filed on one of those assets is\n`listAssetResources`. Any accepted member of the owning business can\nread; a personal property is readable by its owner alone; and so is anyone\nthe property is shared with (`visibility` `public`, or `link` with the\n`share` token). Credentials are optional; a caller with none who may not\nsee it is told **404**.\n",
        "tags": [
          "Farms"
        ],
        "security": [
          {},
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the property",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IncludeFarm"
          }
        ],
        "responses": {
          "200": {
            "description": "The property with its paddocks and mobs. JSON:API puts them in\n`included`; the attributes adapter inlines `paddocks` and `mobs`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FarmsShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/FarmsShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/farms/{id}/reset-share-token": {
      "post": {
        "summary": "Reset a property's share link",
        "operationId": "resetFarmShareToken",
        "description": "Mints a new share token for a property that is already shared by link\n(`visibility` `link`). The old link stops working at once. Requires edit\nrights on the property. A property that is not shared by link answers\n400. Setting `visibility` to `link` or `public` in the first place stays\na first-party action on `createFarm` and `updateFarm`.\n",
        "tags": [
          "Farms"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The property with its new `share_token` and `url`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FarmsShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/FarmsShowResponse"
                }
              }
            }
          },
          "400": {
            "description": "The property is not shared by link.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found"
          }
        }
      }
    },
    "/farms/{farm_id}/status": {
      "get": {
        "operationId": "getFarmStatus",
        "summary": "Get what every paddock and mob on a property is under",
        "description": "The current status of every active paddock and mob on one property, from a single read: the\ncall the property map colours its assets by, and the one an assistant uses to ask what is under\nwithholding across the whole property without reading every paddock and mob. Archived assets\nare left out.\n\nA status is **derived on read** from the completed records on the asset, never stored, by the\nsame function that fills `withholding` on `getMob` and `getPaddock`, so the two always agree:\n\n* A **mob** is held back by the meat withholding period (WHP) and the export slaughter interval\n  (ESI) on the product lines of its completed treatment **and** husbandry records, so a 6-in-1\n  given at marking counts like a drench. `state` is `under_withholding` while the meat WHP is\n  running, `under_export_withholding` while only the ESI is, and `clear` otherwise.\n* A **paddock** is held back by the grazing and harvest withholding periods on its completed\n  sprays and on the seed treatments of a completed sowing. `state` is `graze_restricted` while a\n  grazing WHP is running, `harvest_restricted` while only a harvest WHP is, and `clear`\n  otherwise. **Fertiliser and field work never change a paddock's status**; a fertiliser's\n  advisory note is advice, not a status. A spray label that says DO NOT graze is recorded on the\n  spray (`spray.products[].do_not_graze`) but gives the paddock no status, since it has no date.\n\nWithin each asset the status is the **latest end date of each kind across every record still\nunder an interval**, not just the newest record. Only records completed in the last 12 months\nare read. `withholding` is null when the asset is clear.\n\nPlain JSON, not JSON:API. Needs read access to the property's records: any member of the\nowning business (a viewer included) or the person on a personal property, and an assistant\nwithin its read grant. A property shared publicly or by link does NOT share its status. Not\ncached.\n",
        "tags": [
          "Farms"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "farm_id",
            "in": "path",
            "required": true,
            "description": "The property.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Every active mob and paddock with its state and, when held back, its withholding.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FarmStatusResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/FarmStatusResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated"
          },
          "403": {
            "description": "Not a member of this property's business.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such property."
          }
        }
      }
    },
    "/invites/{token}": {
      "get": {
        "operationId": "getInvite",
        "x-mcp-excluded": true,
        "summary": "Preview a team invite",
        "description": "Public (unauthenticated) preview of a pending team invite — the\nunguessable invite token from the SMS/email link is the credential.\nReturns the organization, inviter and a **masked** invited identifier\n(never the full mobile/email). `status` is `expired` once the invite is\nolder than 7 days. Unknown, revoked or already-claimed tokens return 404.\n",
        "tags": [
          "Public",
          "Invites"
        ],
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "description": "The invite token from the invite link.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The invite preview.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InviteResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/InviteResponse"
                }
              }
            }
          },
          "404": {
            "description": "Unknown, revoked or already-claimed invite.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/invites/{token}/verify": {
      "post": {
        "operationId": "verifyInvite",
        "x-web-or-mobile-only": true,
        "summary": "Send the invite verification code (SMS invites)",
        "description": "SMS-channel invites only: sends a self-issued one-time code to the\ninvited mobile, server-side — the client never sees the number. Sends\nare rate-limited per number and per IP; always returns **204** when the\ninvite is eligible, so the response leaks nothing about send state.\n",
        "tags": [
          "Public",
          "Invites"
        ],
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "description": "The invite token from the invite link.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Accepted (a code was sent if rate limits allow)."
          },
          "403": {
            "description": "Forbidden — first-party only; refused to an API key or an assistant's token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "404": {
            "description": "Unknown, revoked or already-claimed invite.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "422": {
            "description": "Invite is expired or is not an SMS-channel invite.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/invites/{token}/accept": {
      "post": {
        "operationId": "acceptInvite",
        "x-web-or-mobile-only": true,
        "summary": "Accept a team invite (verify code)",
        "description": "SMS-channel invites only: checks the one-time code against the invited\nmobile. On success the account is **created if new** (unusable random\npassword, default grower — exactly like phone sign-in), the mobile is\nmarked verified, this role is attached and accepted, any other pending\ninvites for the number are claimed, and the standard first-party\nDoorkeeper token payload is returned (same shape as\n`POST /users/phone-sessions/verify`).\n",
        "tags": [
          "Public",
          "Invites"
        ],
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "description": "The invite token from the invite link.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Invite accepted — Doorkeeper access token; includes `user_id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DoorkeeperAccessTokenResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/DoorkeeperAccessTokenResponse"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — first-party only; refused to an API key or an assistant's token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "404": {
            "description": "Unknown, revoked or already-claimed invite.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "422": {
            "description": "Incorrect/expired code, expired invite, or not an SMS-channel invite.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "code": {
                    "type": "string",
                    "description": "The 6-digit code from the SMS."
                  }
                },
                "required": [
                  "code"
                ]
              }
            }
          },
          "required": true,
          "description": "The one-time code sent to the invited mobile"
        }
      }
    },
    "/invites/{token}/decline": {
      "post": {
        "operationId": "declineInvite",
        "x-mcp-excluded": true,
        "summary": "Decline a team invite",
        "description": "Public (unauthenticated) decline — the invitee says no. No one-time\ncode is required (the token is the credential and declining grants\nnothing). The invite keeps its token: the owner can re-send it\n(`POST /organization-roles/{organization_role_id}/resend-invite`), and\naccepting off the original link still works while the invite is valid,\nso a mistaken decline is recoverable.\n",
        "tags": [
          "Public",
          "Invites"
        ],
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "description": "The invite token from the invite link.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Invite declined."
          },
          "404": {
            "description": "Unknown, revoked or already-claimed invite.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "422": {
            "description": "Invite is expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/tasks": {
      "get": {
        "operationId": "listTasks",
        "summary": "List Tasks",
        "description": "Tasks and records. For one mob's completed history — the duplicate check\nbefore `createTreatmentRecord` — pass `asset_id` set to the mob id and\n`filter[status][]=completed`, then compare `treatment.products` and\n`completed_at`. A treatment on the same date with the same product is\nprobably a duplicate. Tags are not required for that check.\n`filter[status][]` is sent as `filter_status` by the MCP tool.\n\nPurpleGrain AI records that name no mob or paddock (the Unassigned group\nof a property's Tasks & records board): send\n`filter[farm_id]=<farm id>&filter[unassigned]=true&filter[operation_type][]=ai`.\nThe MCP tool sends these as `filter_farm_id`, `filter_unassigned` and\n`filter_operation_type`. A formatted record that was left unassigned (the\nwords fit more than one mob) is the same query without the `ai` filter,\nor with its real type, and carries the candidates in `ai_format_meta`.\n",
        "tags": [
          "Tasks"
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Select within paginated results",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Number results to return",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "producer_id",
            "in": "query",
            "required": false,
            "description": "Producer id to filter on",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "producer_type",
            "in": "query",
            "required": false,
            "description": "Producer type to filter on",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "provider_id",
            "in": "query",
            "required": false,
            "description": "Provider (assigned contractor) id to filter on",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "provider_type",
            "in": "query",
            "required": false,
            "description": "Provider type to filter on",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "asset_type",
            "in": "query",
            "required": false,
            "description": "The kind of farm asset `asset_id` names — `paddock` or `mob`, singular or plural. Honoured when it names one of those two and ignored otherwise, since `asset_id` is a uuid and identifies the row on its own. Deliberately no enum, exactly like `producer_type` and `provider_type` on this operation: a schema stricter than the filter rejects valid traffic the day request validation is enforced.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "asset_id",
            "in": "query",
            "required": false,
            "description": "The property board: every task and record on one paddock or one mob, in board order. This is an ENTRY scope — it needs no producer or provider alongside it, and it is what a paddock or mob screen calls. Combine with `filter[status][]` to split the board's To do from its Done, and with `include=asset` to embed the paddock or mob itself.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "has_asset",
            "in": "query",
            "required": false,
            "description": "Narrow an owner listing to the rows that are (or are not) about a farm asset. `has_asset=true` alongside `producer_id` / `producer_type` is the org-wide property board in one page — `producer_id=<org id>&producer_type=organizations&has_asset=true&include=asset` — instead of one request per paddock and per mob per farm. `has_asset=false` is the inverse: the work that is about no block of ground.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "roadmap",
            "in": "query",
            "required": false,
            "description": "Return the public roadmap's items instead of a producer/provider listing: the tasks posted by the roadmap account, oldest first. Which account that is comes from server configuration, so the client does not name it. These tasks are deliberately excluded from GET /tasks/search.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "filter[id][]",
            "in": "query",
            "required": false,
            "description": "Filter returned results on ids",
            "style": "form",
            "explode": true,
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "filter[status][]",
            "in": "query",
            "required": false,
            "description": "Filter returned results on status. Narrows whichever entry scope was chosen — the producer/provider listing, `filter[id][]`, the roadmap or a paddock/mob board — so `asset_id=…&filter[status][]=pending` is a board's To do and `asset_id=<mob id>&filter[status][]=completed` is that mob's records (check these for a same-date, same-product drench before creating one). A value outside the enum returns no rows rather than the unfiltered set. The MCP tool sends this parameter as `filter_status`.",
            "style": "form",
            "explode": true,
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "pending",
                  "cancelled",
                  "completed"
                ]
              }
            }
          },
          {
            "name": "filter[parent_id][]",
            "in": "query",
            "required": false,
            "description": "Only the sub-tasks of these parent tasks. Narrows a producer/provider listing, or stands on its own — a parent's children are as public as the parent. The literal `null` selects top-level tasks (no parent), so `producer_id=…&filter[parent_id][]=null&include=children` fetches a whole board in one request: the top-level tasks with their sub-tasks embedded. `null` on its own, without an owner, returns nothing.",
            "style": "form",
            "explode": true,
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "filter[farm_id]",
            "in": "query",
            "required": false,
            "description": "Everything filed on one property, as an ENTRY scope like `asset_id` (it needs no producer alongside it), or a narrowing of one. Unlike the owner scopes it is authorized: the caller must be able to read the property (a member, anyone for a public property, or the holder of its share token), otherwise 403. Combine with `filter[unassigned]` and `filter[operation_type][]` for the Unassigned group.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "filter[unassigned]",
            "in": "query",
            "required": false,
            "description": "`true` narrows the listing to the rows that name no mob or paddock (`asset_id` is null): the same rows as `has_asset=false`, named for the Unassigned group it feeds. `filter[farm_id]=…&filter[unassigned]=true& filter[operation_type][]=ai` is a property's PurpleGrain AI records still waiting for the formatter to place them.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "filter[operation_type][]",
            "in": "query",
            "required": false,
            "description": "Only rows whose `operation_type` is one of these (`ai` for a PurpleGrain AI record waiting for the formatter, or a real type such as `treatment`). Narrows whichever entry scope was chosen. A value no task has returns no rows. The MCP tool sends this parameter as `filter_operation_type`.",
            "style": "form",
            "explode": true,
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "$ref": "#/components/parameters/IncludeTask"
          }
        ],
        "responses": {
          "200": {
            "description": "The listing in board order — `position` ascending with unplaced rows (null position) after the placed ones, then creation time — except `roadmap=true`, which stays oldest-first. When the `include` query parameter is used, the response may contain an `included` array (JSON:API) or inlined related resources (application/json).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksIndexResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksIndexResponse"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createTask",
        "summary": "Create Task",
        "description": "Create a task or a record. An animal treatment (drench, vaccination,\npour-on, jetting, injection) on a mob should be `createTreatmentRecord`\n(`category: \"drench\"` for a drench), which sets the producer from the\nproperty's owner and stores the products.\nUse this operation when you also need `operator`, `record_notes`,\n`record_summary` or `tags`, or when the asset is a paddock.\n\nOne asset per record. A treatment after the fact is `status: completed`,\n`created_as: record`, with `drench.products` set. A planned treatment is\n`status: pending`, `created_as: task`. Back-date `completed_at` with an\nAustralian offset (`2025-12-23T09:00:00+11:00` is stored as\n`2025-12-22T22:00:00Z`). Use 09:00 local when only a date is known.\nAEDT is +11:00 (first Sunday of October to first Sunday of April);\nAEST is +10:00.\n\n**Producer.** For a property owned by a business, set `producer` to that\norganization (`type: organization`). Omitting it returns 403 Forbidden,\ndetail \"Unauthorized\", with `source.pointer`\n`/request/headers/authorization`. That is a missing producer, not a bad\ntoken. A 403 whose `code` is `insufficient_access` is a real access\nrefusal: its `detail` names the business and the access that is missing.\n\n**Broadcasting is paused for agents.** A request\nfrom an API or MCP client whose body names `broadcast` or `share_scope`\n— true or false — is refused with 403 and nothing is written; leave both\nout. First-party web and mobile requests are unaffected.\n\n**PurpleGrain AI record.** The composer's AI mode creates a task with\n`operation_type: ai`, `capture_source: voice` or `typed`,\n`status: completed`, `created_as: record`, `description` set to the\nfarmer's own words, and no typed child. The asset (a mob or paddock) is\noptional; without one, send the `farm` relationship so the record still\nbelongs to a property (you must be able to edit that property). Photos go\non afterwards with `POST /tasks/{task_id}/images`. The formatter\n(`AiFormatJob`) picks the record up after about 30 seconds, reads the\nwords, the photos and, unless the organisation switched\n`ai_format_enabled` off, the farm's records, then writes the typed child,\nlinks the asset and sets `ai_format_done_at`. It never invents a batch,\ndose, expiry, WHP or ESI: a value the words and photos do not support is\nlisted in `ai_format_meta.unconfirmed`. A record it cannot place keeps no\nasset and lists the candidates in `ai_format_meta.candidates`; attach one\nwith `updateTask` (below). Not a way to create tasks with AI: the\npaused `POST /tasks/ai` is unchanged.\n\n**PurpleGrain AI records are first-party.** The\nformatter is for a farmer's untyped words. An MCP client already knows\nwhat the record is, so a request from one whose body sets\n`operation_type: ai` is refused with 403 and nothing is written; write\nthe typed record instead (`treatment`, `husbandry`, `spray`,\n`fertiliser`, `sowing` or `fieldwork`, or the matching `create*Record`\noperation) with `capture_source: agent`. API keys and the app's own\nrequests are unaffected.\n\n**Photos.** A file on disk is staged with `createImageUpload`, whose `curl_example` PUTs the bytes, and attached with `image_upload_ids`. Never\nbase64-encode a local photo into a tool call. A public http(s) URL:\npass `image_urls`. Only a tiny generated image (under about 100 KB\ndecoded) uses `image_base64s`. One upload can be attached to several\nrecords until it expires. On update, `image_mode` defaults to `append`\n(add to the existing photos); `replace` swaps them all out;\n`remove_image_ids` deletes specific ones. If any image fails, nothing\nis saved. An `Idempotency-Key` header makes a retried create return the\noriginal task. The attaching call still needs `confirm: true`.\n\nThe MCP tool is refused until it is called again with `confirm: true`.\nIf the call errors or times out, read the asset's records\n(`listTasks` with `asset_id`) before retrying.\n\nExample, organization-owned farm (`application/vnd.api+json`):\n\n```json\n{\n  \"data\": {\n    \"type\": \"task\",\n    \"attributes\": {\n      \"operation_type\": \"drench\",\n      \"status\": \"completed\",\n      \"created_as\": \"record\",\n      \"capture_source\": \"agent\",\n      \"completed_at\": \"2025-12-23T09:00:00+11:00\",\n      \"operator\": \"AH\",\n      \"description\": \"Drench rams\",\n      \"record_summary\": \"Drenched Rams (11 hd): Startect 20 mL/hd. WHP until 6 Jan 2026, ESI until 20 Jan 2026\",\n      \"record_notes\": \"Expiry: not recorded. Route: oral.\",\n      \"tags\": [\"drench\", \"startect\", \"whp-until-2026-01-06\", \"esi-until-2026-01-20\"],\n      \"drench\": {\n        \"head\": 11,\n        \"products\": [\n          { \"name\": \"Startect\", \"batch\": \"728661\", \"dose\": \"20 mL per head\", \"whp_days\": 14, \"esi_days\": 28 }\n        ]\n      }\n    },\n    \"relationships\": {\n      \"asset\": { \"data\": { \"type\": \"mob\", \"id\": \"<mob id>\" } },\n      \"farm\": { \"data\": { \"type\": \"farm\", \"id\": \"<farm id>\" } },\n      \"producer\": { \"data\": { \"type\": \"organization\", \"id\": \"<owning organization id>\" } }\n    }\n  }\n}\n```\n",
        "x-mcp-description": "Create a task or a record. An animal treatment (drench, vaccination,\npour-on, jetting, injection) on a mob is `createTreatmentRecord`\n(`category: \"drench\"` for a drench), which sets the producer from the\nproperty's owner and stores the products. This operation is for a\nrecord that also needs `operator`, `record_notes`, `record_summary` or\n`tags`, or a plain task, or work on a paddock.\n\nOne asset per record. A treatment after the fact is `status: completed`,\n`created_as: record`, with `drench.products` set. A planned treatment is\n`status: pending`, `created_as: task`. `completed_at` takes an Australian\noffset (`2025-12-23T09:00:00+11:00` is stored as `2025-12-22T22:00:00Z`);\n09:00 local stands in when only a date is known. AEDT is +11:00 (first\nSunday of October to first Sunday of April); AEST is +10:00.\n\n**Producer.** For a property owned by a business, `producer` is that\nbusiness (`type: organization`). Without it the answer is 403\nForbidden, detail \"Unauthorized\", with `source.pointer`\n`/request/headers/authorization`: a missing producer, not a bad token.\nA 403 whose `code` is `insufficient_access` is a real access refusal;\nits `detail` names the business and the access that is missing.\n\n**Fields an assistant cannot set.** A body from an MCP client that names\n`broadcast` or `share_scope` (any value), or sets `operation_type: ai`, is refused with 403\nand nothing is written: broadcasting is paused for agents, and a\nPurpleGrain AI record is the app's own. A record is written with its own type\n(`treatment`, `husbandry`, `spray`, `fertiliser`, `sowing` or\n`fieldwork`, or the matching `create*Record` operation) and\n`capture_source: agent`.\n\n**Photos.** A file on disk is staged with `createImageUpload`, whose\n`curl_example` PUTs the bytes, and attached with `image_upload_ids`.\nA public http(s) URL goes in `image_urls`; `image_base64s` is only for a\ntiny generated image (under about 100 KB decoded). On a later update,\n`image_mode` `append` (the default) adds photos, `replace` swaps them all\nand `remove_image_ids` drops specific ones. If any image fails, nothing\nis saved. An `Idempotency-Key` header makes a retried create return the\noriginal task.\n\nThe tool is refused until it is called with `confirm: true`. A call that\nerrors or times out may still have written; the asset's records\n(`listTasks` with `asset_id`) show whether it did.\n",
        "tags": [
          "Tasks"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "$ref": "#/components/parameters/IncludeTask"
          }
        ],
        "responses": {
          "200": {
            "description": "Success. When the `include` query parameter is used, the response may contain an `included` array (JSON:API) or inlined related resources (application/json).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Errors"
          },
          "403": {
            "description": "Not allowed to create for the requested owner, or an API or MCP client naming `broadcast` / `share_scope` (paused for agents), or an MCP client setting `operation_type: ai` (PurpleGrain AI records are first-party)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TaskCreateModelAttributes"
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/TaskCreateModel"
              }
            }
          },
          "description": "Create task attributes"
        }
      }
    },
    "/tasks/reorder": {
      "post": {
        "operationId": "reorderTasks",
        "summary": "Reorder Tasks",
        "description": "One write per drag. Send the ids of one sibling set — a parent's\nsub-tasks, or an owner's top-level tasks — in their new order, and the\nserver rewrites `position` as 1…N inside a single transaction. Nothing\nelse moves: status and completed_at are untouched. Every task must be\neditable by the caller (its grower or assigned contractor); all must sit\nunder the given `parent` (or all be top-level tasks of one owner when no\nparent is given), every id must resolve and none may repeat — otherwise\nnothing is written. Responds with the reordered tasks in board order.\nSend the whole sibling set: a partial list only rewrites the rows named,\nand their positions can then collide with siblings left out.\n",
        "tags": [
          "Tasks"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IncludeTask"
          }
        ],
        "responses": {
          "200": {
            "description": "The reordered tasks, board ordered. When the `include` query parameter is used, the response may contain an `included` array (JSON:API) or inlined related resources (application/json).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksIndexResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksIndexResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated"
          },
          "403": {
            "description": "The caller may not edit at least one of the tasks",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "422": {
            "description": "Not one sibling set — an empty or repeated list, an unknown id, a task under a different parent, or top-level tasks from more than one owner",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TaskReorderModelAttributes"
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/TaskReorderModel"
              }
            }
          },
          "description": "The sibling set in its new order, and the parent it belongs to"
        }
      }
    },
    "/tasks/ai": {
      "post": {
        "summary": "Create a task with AI",
        "operationId": "createTaskWithAi",
        "x-web-or-mobile-only": true,
        "description": "**Paused for agents.** Create with AI is switched\noff in the product: API and MCP clients receive 403 and the MCP server\ndoes not list this operation. Create the task as written with\n`createTask` (`POST /tasks`). First-party web and mobile requests still\nwork, so installed apps keep working.\n\nCreate with AI — the server runs the analysis and owns the write. The\nclient sends the grower's description plus structured context\n(pre-uploaded attachment ids, farm, tags, category, location, owner) and\nthe server sends the photos + description to the AI model, then creates\nthe task from the result — returned exactly as GET /tasks/{id} would.\nThe model produces only a description (opening with one short plain\nsentence — tasks have no title) and a category constrained to the\nexisting category list; tasks carry no date and no price. Farm, location,\nthe posting business and any populated tags ride in the prompt as known\nfacts. Soft failure — when the AI provider is unavailable the task is\ncreated as typed, so the grower's text is never lost.\nFirst-party (web/mobile) channel only: API and MCP clients compose the\ndescription themselves and use POST /tasks instead — other channels\nreceive 403.\n",
        "tags": [
          "Tasks"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [],
        "responses": {
          "201": {
            "description": "The created task.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Not allowed to create for the requested owner, or an API or MCP client (Create with AI is paused for agents — use POST /tasks)"
          },
          "422": {
            "description": "Blank description",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "description": {
                    "type": "string",
                    "description": "The grower's free-form task description (hashtags already lifted out client-side)."
                  },
                  "attachment_ids": {
                    "type": "array",
                    "description": "Ids of photos pre-uploaded via POST /attachments.",
                    "items": {
                      "type": "string"
                    }
                  },
                  "farm": {
                    "type": "string",
                    "description": "Farm name — an explicit"
                  },
                  "tags": {
                    "type": "array",
                    "description": "Free hashtags, without the leading hash.",
                    "items": {
                      "type": "string"
                    }
                  },
                  "category": {
                    "type": "string",
                    "description": "Category name picked client-side (parity with plain create)."
                  },
                  "location": {
                    "type": "string",
                    "description": "Explicit location; defaults to the resolved farm's."
                  },
                  "owner": {
                    "type": "object",
                    "description": "The task's owner — explicit, never inferred from an active org.",
                    "properties": {
                      "type": {
                        "type": "string",
                        "enum": [
                          "user",
                          "organization"
                        ]
                      },
                      "id": {
                        "type": "string"
                      }
                    }
                  },
                  "broadcast": {
                    "type": "boolean",
                    "description": "Share the task with local contractors after creation (free — sets share_scope to broadcast)."
                  },
                  "idempotency_key": {
                    "type": "string",
                    "description": "Client-generated key to make retries safe."
                  }
                },
                "required": [
                  "description"
                ]
              }
            }
          },
          "required": true,
          "description": "The description and structured context to create from"
        }
      }
    },
    "/tasks/{id}": {
      "get": {
        "operationId": "getTask",
        "summary": "Get a task or completed farm record by ID",
        "description": "Retrieve one existing task or completed farm record using its task ID. Returns its description, status, completion details, photo URLs, record notes and any saved typed record data. Use include to expand supported relationships such as producer, provider, farm, asset, parent and children. This lookup does not update the task, mark it complete or notify anyone.",
        "tags": [
          "Tasks"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the record",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IncludeTask"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful. When the `include` query parameter is used, the response may contain an `included` array (JSON:API) or inlined related resources (application/json).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponse"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      },
      "put": {
        "operationId": "updateTask",
        "summary": "Update a task",
        "description": "A partial write: send only the fields that change. To change a drench's\nhead or products, prefer `updateTreatmentRecord` (`treatment.id`, not the\ntask id). `drench.products` here also replaces the set. `created_as` is\nignored on update. Back-dating uses `completed_at` with an Australian\noffset; see `createTask`.\n\n**Attaching a PurpleGrain AI record.** To place a record with no asset\n(`filter[unassigned]=true` on `listTasks`), send the `asset` relationship\n(a `mob` or `paddock` you can edit). The task's `farm` is set from the\nasset. This works before and after the formatter has run, and on a\nrecord with no typed child. A treatment or husbandry record takes a mob;\nspray, fertiliser, sowing and field work a paddock. Editing a record\nnever queues it for the formatter again.\n\n**PurpleGrain AI records are first-party.** An MCP\nclient cannot turn a task into one: a request from one that sets\n`operation_type: ai` on a task that is not already an AI record is\nrefused with 403 and nothing is written. Sending an AI record's own\ntype back while attaching its asset still passes.\n\n**Broadcasting is paused for agents.** A request\nfrom an API or MCP client whose body names `broadcast` or `share_scope`\n— true or false — is refused with 403 and nothing is written; leave both\nout. First-party web and mobile requests are unaffected.\n\n**Photos.** A file on disk is staged with `createImageUpload`, whose `curl_example` PUTs the bytes, and attached with `image_upload_ids`. Never\nbase64-encode a local photo into a tool call. A public http(s) URL:\npass `image_urls`. Only a tiny generated image (under about 100 KB\ndecoded) uses `image_base64s`. One upload can be attached to several\nrecords until it expires. `image_mode` defaults to `append` (add to the\nexisting photos); `replace` swaps them all out; `remove_image_ids`\ndeletes specific ones (ids from `image_ids`). If any image fails, the\nupdate is rolled back. The attaching call still needs `confirm: true`.\n\nThe MCP tool is refused until it is called again with `confirm: true`.\nIf the call errors or times out, read the task back before retrying.\n",
        "tags": [
          "Tasks"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the record",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IncludeTask"
          }
        ],
        "responses": {
          "200": {
            "description": "Update. When the `include` query parameter is used, the response may contain an `included` array (JSON:API) or inlined related resources (application/json).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Errors"
          },
          "403": {
            "description": "Not allowed to edit this task, or an API or MCP client naming `broadcast` / `share_scope` (paused for agents), or an MCP client turning a task into a PurpleGrain AI record with `operation_type: ai`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TaskUpdateModelAttributes"
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/TaskUpdateModel"
              }
            }
          },
          "description": "Update task attributes"
        }
      },
      "delete": {
        "operationId": "deleteTask",
        "summary": "Delete an existing task or farm record",
        "description": "Delete the task identified by id. A task may be pending work or a completed farm record, so deleting it removes that record rather than marking work complete. Read the task first to identify it and obtain the person's explicit confirmation before deletion. The caller must have permission to act for the task's producer or provider, and an MCP connection must allow writes. Returns 204 with no response body on success. Use updateTask to change a task or its status without deleting it.",
        "tags": [
          "Tasks"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the record",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Delete"
          },
          "401": {
            "description": "Unauthenticated"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "NotFound"
          }
        }
      }
    },
    "/tasks/{id}/update-image": {
      "post": {
        "operationId": "updateTaskImage",
        "summary": "Replace a task's photos with one image",
        "description": "Legacy. Prefer `addTaskImages` to append photos, or `updateTask` with\n`image_mode: replace`. This endpoint removes the current gallery and\nreplaces it with one image; it does not append. Requires edit rights\non the task and confirmation before replacing photos.\n\nProvide `image_url` for a publicly reachable HTTP(S) image, or\n`image_base64` only for a tiny generated image (under about 100 KB\ndecoded). For a local photo, stage it with `createImageUpload`, then\nuse `updateTask` with `image_upload_ids` and `image_mode: replace`.\nUse `addTaskImages` to keep existing photos and append instead.\nReturns the updated task.\n",
        "tags": [
          "Tasks"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the record",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IncludeTask"
          }
        ],
        "responses": {
          "200": {
            "description": "Success. When the `include` query parameter is used, the response may contain an `included` array (JSON:API) or inlined related resources (application/json).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "image_url": {
                    "type": "string",
                    "format": "uri",
                    "description": "Publicly reachable http(s) URL of the image to fetch and attach. Prefer this when the image already lives somewhere addressable."
                  },
                  "image_base64": {
                    "type": "string",
                    "description": "Base64-encoded image bytes (optionally a \"data:...;base64,\" URI). Use when you hold raw bytes and have no URL."
                  }
                },
                "description": "Provide either image_url or image_base64."
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "image": {
                    "type": "string",
                    "format": "binary"
                  }
                }
              }
            }
          },
          "required": true,
          "description": "Provide one of image_url / image_base64 (JSON) or an image file (multipart)."
        }
      }
    },
    "/tasks/{id}/images": {
      "post": {
        "operationId": "addTaskImages",
        "summary": "Add Task Images",
        "description": "Append one or more photos to the task's gallery. The first gallery photo\nis the cover (exposed as the singular image_url). A legacy single image\nis migrated into the gallery (keeping its id) before appending.\n\nA file on disk is staged with `createImageUpload`, whose `curl_example` PUTs the bytes, and attached with `image_upload_ids`. Never base64-encode a local\nphoto into a tool call. A public http(s) URL: pass `image_urls`. Only a\ntiny generated image (under about 100 KB decoded) uses `image_base64s`.\nOne upload can be attached to several records until it expires. This\ncall always appends. To swap the gallery or drop specific photos, use\n`updateTask` with `image_mode: replace` or `remove_image_ids`.\n",
        "tags": [
          "Tasks"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the record",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IncludeTask"
          }
        ],
        "responses": {
          "200": {
            "description": "Success. When the `include` query parameter is used, the response may contain an `included` array (JSON:API) or inlined related resources (application/json).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request — no image source provided or an invalid source",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "image_upload_ids": {
                    "$ref": "#/components/schemas/ImageUploadIdList"
                  },
                  "image_urls": {
                    "type": "array",
                    "description": "Publicly reachable http(s) URLs of images to fetch and attach, in gallery order. Use this when the image is already at a public URL.",
                    "items": {
                      "type": "string",
                      "format": "uri"
                    }
                  },
                  "image_base64s": {
                    "type": "array",
                    "description": "Base64-encoded image payloads (each optionally a \"data:...;base64,\" URI), in gallery order. Only for a tiny generated image under about 100 KB decoded. A local photo uses image_upload_ids.",
                    "items": {
                      "type": "string"
                    }
                  },
                  "image_url": {
                    "type": "string",
                    "format": "uri",
                    "description": "Single-image convenience form of image_urls."
                  },
                  "image_base64": {
                    "type": "string",
                    "description": "Single-image convenience form of image_base64s."
                  }
                },
                "description": "Provide image_upload_ids, image_urls, image_base64s, or their singular forms. A file on disk uses image_upload_ids."
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "images": {
                    "type": "array",
                    "description": "One or more image files, in gallery order.",
                    "items": {
                      "type": "string",
                      "format": "binary"
                    }
                  },
                  "image": {
                    "type": "string",
                    "format": "binary",
                    "description": "Single-file convenience form of images."
                  }
                }
              }
            }
          },
          "required": true,
          "description": "Provide image files (multipart) and/or image_upload_ids / image_urls / image_base64s (JSON)."
        }
      },
      "delete": {
        "operationId": "deleteTaskImages",
        "summary": "Delete all of a task's photos",
        "description": "Removes the whole gallery in one call, including any legacy single image.\nThe path `id` is the task id. To remove one photo, use `deleteTaskImage`\nwith `image_id` from the task's `image_ids`.\n",
        "tags": [
          "Tasks"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the task.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IncludeTask"
          }
        ],
        "responses": {
          "200": {
            "description": "Success — the task with an empty gallery. When the `include` query parameter is used, the response may contain an `included` array (JSON:API) or inlined related resources (application/json).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/tasks/{id}/images/{image_id}": {
      "delete": {
        "operationId": "deleteTaskImage",
        "summary": "Delete one task photo",
        "description": "Removes one photo from the task's gallery. The path `id` is the task id,\nnot the photo. `image_id` comes from the task's `image_ids` (aligned with\n`image_urls`). To remove the whole gallery, use `deleteTaskImages`.\n",
        "tags": [
          "Tasks"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the task, not the photo.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "image_id",
            "in": "path",
            "required": true,
            "description": "Id of the gallery attachment to remove (see the task's image_ids)",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IncludeTask"
          }
        ],
        "responses": {
          "200": {
            "description": "Success — the task with the remaining gallery. When the `include` query parameter is used, the response may contain an `included` array (JSON:API) or inlined related resources (application/json).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/tasks/{id}/broadcast": {
      "post": {
        "summary": "Broadcast Task",
        "operationId": "broadcastTask",
        "x-web-or-mobile-only": true,
        "description": "**Paused for agents.** Sharing with local\ncontractors is switched off in the product: API and MCP clients receive\n403 and the MCP server does not list this operation. First-party web and\nmobile requests still work, so installed apps keep working.\n\nShare the task with local contractors — sets `share_scope` to\n`broadcast` (`broadcast: true`). Free; nothing is billed and the legacy\npayment fields are not written. Kept as an alias of\n`PUT /tasks/{id}` with `{ broadcast: true }`; to un-share, send that PUT\nwith `{ broadcast: false }`.\n",
        "tags": [
          "Tasks"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the record",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IncludeTask"
          }
        ],
        "responses": {
          "200": {
            "description": "Success. When the `include` query parameter is used, the response may contain an `included` array (JSON:API) or inlined related resources (application/json).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "No edit rights on the task, or an API or MCP client (broadcasting is paused for agents)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/tasks/search": {
      "get": {
        "operationId": "searchTasks",
        "summary": "Search Tasks",
        "description": "Free-text search. `term` matches tags case-insensitively, so\n`whp-until-2026` or `import-<source-id>` finds a group of records that\nwere tagged that way. A drench saved with `createTreatmentRecord` has no\ntags unless a later `updateTask` added them; its product names live on\n`treatment.products`, which this search does not read. For one mob's\nhistory use `listTasks`.\n",
        "tags": [
          "Tasks"
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "category",
            "in": "query",
            "required": false,
            "description": "Category name to filter on — a single, exact `Category.name` (see `GET /categories`). This is the only category filter; ids and multiple values are not accepted. An unknown name returns no results.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "latitude",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "longitude",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "radius",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "term",
            "in": "query",
            "required": false,
            "description": "Free-text filter matched case-insensitively against name, description, category name and tags. Pass a tag such as `whp-until-2026` or `import-<source-id>` to find that group. Does not search `treatment.products`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Filter on a single task status. Omit to return tasks in every status.",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "cancelled",
                "completed"
              ]
            }
          },
          {
            "$ref": "#/components/parameters/IncludeTask"
          }
        ],
        "responses": {
          "200": {
            "description": "Success. When the `include` query parameter is used, the response may contain an `included` array (JSON:API) or inlined related resources (application/json).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksIndexResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksIndexResponse"
                }
              }
            }
          }
        }
      }
    },
    "/liked-contractors": {
      "get": {
        "summary": "List Liked Contractors",
        "operationId": "listLikedContractors",
        "description": "The caller's liked contractors (most recent first). Pass owner_id/owner_type\nto read an organization's list (requires edit rights on the organization).\nPass contractor_id/contractor_type to answer \"is this contractor liked?\".\n",
        "tags": [
          "LikedContractors"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "owner_id",
            "in": "query",
            "required": false,
            "description": "Owner id to scope the list to (defaults to the current user).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "owner_type",
            "in": "query",
            "required": false,
            "description": "Owner type, \"User\" or \"Organization\".",
            "schema": {
              "type": "string",
              "enum": [
                "User",
                "Organization"
              ]
            }
          },
          {
            "name": "contractor_id",
            "in": "query",
            "required": false,
            "description": "Filter to a single contractor (liked-state lookup).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "contractor_type",
            "in": "query",
            "required": false,
            "description": "Contractor type, \"User\" or \"Organization\".",
            "schema": {
              "type": "string",
              "enum": [
                "User",
                "Organization"
              ]
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Select within paginated results",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Number results to return",
            "schema": {
              "type": "integer"
            }
          },
          {
            "$ref": "#/components/parameters/IncludeLikedContractor"
          }
        ],
        "responses": {
          "200": {
            "description": "Liked contractors for the requested owner. When the `include` query parameter is used, the response may contain an `included` array (JSON:API) or inlined related resources (application/json).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LikedContractorsIndexResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/LikedContractorsIndexResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          }
        }
      },
      "post": {
        "summary": "Like a Contractor",
        "operationId": "createLikedContractor",
        "description": "Like a contractor (User or Organization). The owner is the current user;\nan Organization owner is accepted when the caller can edit that\norganization. One like per (owner, contractor) pair.\n",
        "tags": [
          "LikedContractors"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IncludeLikedContractor"
          }
        ],
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LikedContractorsShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/LikedContractorsShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "422": {
            "description": "Unprocessable Entity (duplicate like, or liking yourself)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LikedContractorCreateModelAttributes"
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/LikedContractorCreateModel"
              }
            }
          },
          "required": true,
          "description": "The contractor to like"
        }
      }
    },
    "/liked-contractors/{id}": {
      "delete": {
        "summary": "Unlike a contractor",
        "operationId": "deleteLikedContractor",
        "description": "Removes the like. It does not delete the contractor or their records.\n",
        "tags": [
          "LikedContractors"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the record",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/locations/{id}": {
      "get": {
        "operationId": "getLocation",
        "summary": "Get saved coordinates by location ID",
        "description": "Retrieve an existing PurpleGrain location using its location ID. Returns latitude (lat), longitude (lon) and accuracy. Use the location ID associated with an address, city or other resource; this is a saved-coordinate lookup, not an address search. It does not change data.",
        "tags": [
          "Locations"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "ID of an existing PurpleGrain location.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LocationsShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/LocationsShowResponse"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/mobs": {
      "get": {
        "summary": "List Mobs",
        "operationId": "listMobs",
        "description": "The mobs of one property (`farm_id`) or of every property in a farm\nbusiness (`organization_id`), ordered by name, case-insensitively.\nWith neither parameter the list is only the signed-in user's\npersonal-property mobs. A business's mobs are missing until you pass\n`farm_id` or `organization_id`. `getFarmPicture` is every mob on one\nproperty in one call.\n\nA mob belongs to its property, not to a paddock: each one carries\n`current_paddock_id`, the paddock it is in now (derived from its latest\ncompleted move, null while it has never been placed). Move it with\n`moveMob`.\n\nLive mobs only by default — pass `archived=true` for the archive drawer\nor `archived=all` for both. The default page holds a whole property's\nmobs (200) so a client can render \"2 placed · 1 unplaced\" without a\nsecond request; `meta` carries no total, so a truncated page is a wrong\nnumber on screen rather than a missing row.\n\nAny accepted member of the owning organization can list, a viewer\nincluded.\n",
        "tags": [
          "Mobs"
        ],
        "security": [
          {},
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "farm_id",
            "in": "query",
            "required": false,
            "description": "One property's mobs. Supply either this or `organization_id`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "organization_id",
            "in": "query",
            "required": false,
            "description": "Every mob across the business's properties — the organization-wide board and the composer's cross-farm list. With neither this nor `farm_id`, every mob across the signed-in user's personal properties.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "archived",
            "in": "query",
            "required": false,
            "description": "`false` (the default) for live mobs, `true` for archived ones only, `all` for both.",
            "schema": {
              "type": "string",
              "example": "false"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "$ref": "#/components/parameters/IncludeMob"
          }
        ],
        "responses": {
          "200": {
            "description": "Mobs for the property or the business.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MobsIndexResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/MobsIndexResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found — neither `farm_id` nor `organization_id` names a record the caller can see.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create Mob",
        "operationId": "createMob",
        "description": "A name is a valid mob — the head count arrives when someone counts them.\n`head_count` is optional. There are no species, class or breed fields\nyet; put those in the name (for example \"MxBL rams\").\n\nNames are unique per property, case-insensitively, across live AND\narchived mobs: reusing the name of one that was archived is rejected\nhere, where a name is being typed, rather than stranding the archived mob\nwith no way to be restored.\n\nA new mob is not in a paddock. Placement is never set here — it is\nderived from the mob's records, so the first `moveMob` is what puts it\nsomewhere. If this call errors or times out, list the property's mobs\nbefore retrying. Requires organization edit rights.\n",
        "tags": [
          "Mobs"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IncludeMob"
          }
        ],
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MobsShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/MobsShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "422": {
            "description": "A missing name, a name already used on the property (live or archived), or a negative head count",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MobCreateModelAttributes"
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/MobCreateModel"
              }
            }
          },
          "required": true,
          "description": "Create mob attributes"
        }
      }
    },
    "/mobs/{id}": {
      "get": {
        "summary": "Get a mob",
        "operationId": "getMob",
        "description": "One mob: its name, `head_count`, archive state, diary counters\n(`record_count`, `open_task_count`), and `current_paddock_id` — the\npaddock the mob is in now, derived from its newest completed record that\nnames a destination, and null while it has never been placed. Resolve\nthe paddock with `getPaddock` or `listPaddocks`. The owning property is\nthe `farm` relationship.\n\nMove the mob with `moveMob`. Everything filed on it — the property, its\ncurrent paddock, and its tasks and records — is `listAssetResources`\nwith `asset_type=mob`. The task board alone is `listTasks` with\n`asset_id`. Any accepted member of the owning organization can read; a\npersonal property is its owner's. `withholding` is the meat withholding\nperiod and the export slaughter interval still ahead, from the mob's\ncompleted treatment and husbandry records, or null when the mob is clear.\nThe same fact for every asset on the property is `getFarmStatus`.\n",
        "tags": [
          "Mobs"
        ],
        "security": [
          {},
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the record",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IncludeMob"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MobsShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/MobsShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      },
      "put": {
        "summary": "Update Mob",
        "operationId": "updateMob",
        "description": "Renaming a mob, or changing its head count, writes a record on its\ntimeline — \"Renamed from Merino wethers · Head count set to 1,240 — was\n980\" — composed on the server, because three write surfaces edit a mob\nand only the server sees before and after atomically. An update that\nchanges neither field writes nothing.\n\n**Archive and restore are this endpoint**: `archived: true` hides the mob\nfrom the map and the lists while its records stay on the property;\n`archived: false` brings it back. There is no `/archive` route.\nArchiving writes no record.\n\nThe owning property cannot be changed — a `farm` relationship sent here\nis ignored, so a mob can never be re-parented and take its records with\nit. Where the mob IS cannot be set here either; see `moveMob`.\n\nRequires organization edit rights.\n",
        "tags": [
          "Mobs"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the record",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IncludeMob"
          }
        ],
        "responses": {
          "200": {
            "description": "Updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MobsShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/MobsShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "422": {
            "description": "A blank name, a name already used on the property (live or archived), or a negative head count",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MobUpdateModelAttributes"
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/MobUpdateModel"
              }
            }
          },
          "required": true,
          "description": "Update mob attributes"
        }
      },
      "delete": {
        "summary": "Delete Mob",
        "operationId": "deleteMob",
        "description": "Removes the mob for good — and only while nothing is filed on it.\n\n**A mob with any record or open task cannot be removed: the request is\nrefused with a `409`** whose `errors[0].detail` names the counts\n(\"Merino wethers has 4 records and 0 open tasks on it — archive this mob\ninstead of removing it\") and whose `errors[0].code` is `asset_in_use`.\nThe counts are read inside the delete's own transaction, not from the\nnumbers a client read earlier, so a mob that acquired history between a\nclient's check and its `DELETE` is kept rather than silently unfiled.\nArchive it instead (`PUT { archived: true }`): its records — every move\nit ever made included — stay on the property, and the mob stays\nopenable from the archive drawer.\n\nA client offers \"Remove this mob\" only while `record_count` and\n`open_task_count` are both zero and \"Archive this mob\" otherwise — and\ntreats a `409` as the archive face, because the numbers moved under it.\n",
        "tags": [
          "Mobs"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the record",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "409": {
            "description": "Conflict — the mob has records or open tasks on it, so it can be\narchived but not removed. `errors[0].detail` names both counts and\nsays to archive instead; `errors[0].code` is `asset_in_use`. Nothing\nwas changed.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/mobs/{id}/move": {
      "post": {
        "summary": "Move Mob",
        "operationId": "moveMob",
        "description": "Put a mob in a paddock.\n\n**A move is a record, not a field.** There is no writable location\nanywhere on a mob: this endpoint writes one completed `move_stock` record\ncarrying the origin, the destination and the composed line \"Moved the\nmerino wethers from Middle Run to Top 40\", and `current_paddock_id` is\nderived from that record. So the property's diary can never disagree with\nits map, reopening the record un-places the mob, and removing the\ndestination paddock un-places it too.\n\nA POST rather than a PUT because it creates something. The origin is\nresolved server-side from wherever the mob is now, and may be none — the\nfirst move of a mob that has never been placed is the prototype's\n\"Put in a paddock\", and its sentence simply omits the \"from\" clause.\n\nThe response is the **mob** at its derived placement, with its counters\nalready updated. The record itself is read back with the mob's board\n(a task read), which is what a client refetches anyway.\n\n`completed_at` records a move after the fact. The record is always\nwritten, but the placement is still whatever the newest completed record\nsays — so back-dating a move behind an existing newer one files the diary\nentry and leaves the pin where it was. The rejection below compares\nagainst the mob's placement *today*, not its placement on the supplied\ndate.\n\nThe destination must be a live paddock on the mob's own property, and not\nthe one the mob is currently in. Requires organization edit rights.\n",
        "tags": [
          "Mobs"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the record",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IncludeMob"
          }
        ],
        "responses": {
          "200": {
            "description": "The mob at its derived placement — the new paddock, unless an older `completed_at` put the move behind an existing one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MobsShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/MobsShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "422": {
            "description": "A missing destination, a paddock on another property, an archived paddock, or the paddock the mob is already in",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MobMoveModelAttributes"
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/MobMoveModel"
              }
            }
          },
          "required": true,
          "description": "The destination paddock, plus the date and capture source of the move"
        }
      }
    },
    "/organization-roles": {
      "get": {
        "operationId": "listOrganizationRoles",
        "summary": "List business memberships",
        "description": "The signed-in user's own memberships. `user_id` must be that user\n(`fetchCurrentUser`); any other value is 403. There is no filter by\nbusiness. With `include=organization`, each row is a business they\nbelong to and the permission on it (`owner`, `manager`, and the rest).\nA business's properties are `listFarms` with that business's id; they\nare not in the personal property list.\n",
        "tags": [
          "Organization Roles"
        ],
        "parameters": [
          {
            "name": "user_id",
            "in": "query",
            "required": false,
            "description": "The user whose memberships to list. Use the id from `fetchCurrentUser`, with `include=organization`, to find the businesses that user belongs to.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Select within paginated results",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Number results to return",
            "schema": {
              "type": "integer"
            }
          },
          {
            "$ref": "#/components/parameters/IncludeOrganizationRole"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful. When the `include` query parameter is used, the response may contain an `included` array (JSON:API) or inlined related resources (application/json).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationRolesIndexResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationRolesIndexResponse"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createOrganizationRole",
        "summary": "Invite or add a person to a business",
        "description": "Create a business membership or invitation with the requested permission\nand status. Supply the `organization` relationship and identify the\nperson through a `user` relationship, `invited_email`, or\n`invited_mobile`. Requires edit rights on the business; the assigned\npermission cannot exceed the caller's own role.\n\nInviting by email or mobile may send an email or SMS. A matching\nexisting account is linked to the role; otherwise a pending invitation\ncarries a seven-day link. Confirm the business, person and permission\nbefore creating the role. Returns the created membership or invitation.\nThis does not create a user account or accept an invitation for them.\n",
        "tags": [
          "Organization Roles"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IncludeOrganizationRole"
          }
        ],
        "responses": {
          "201": {
            "description": "Created. When the `include` query parameter is used, the response may contain an `included` array (JSON:API) or inlined related resources (application/json).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationRolesShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationRolesShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated"
          },
          "403": {
            "description": "Forbidden"
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OrganizationRoleCreateModelAttributes"
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/OrganizationRoleCreateModel"
              }
            }
          },
          "description": "Create organization role attributes"
        }
      }
    },
    "/organization-roles/{id}": {
      "get": {
        "operationId": "getOrganizationRole",
        "summary": "Get an organization membership or invitation by ID",
        "description": "Retrieve one organization membership or invitation using its role record ID. Returns the permission, membership status, invitation details and timestamps; include can expand the organization and user. The caller may read their own membership, or another member's role when they can manage the organization and their assistant grant permits write access. This lookup does not accept an invitation, change permissions or send a notification.",
        "tags": [
          "Organization Roles"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the record",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IncludeOrganizationRole"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful. When the `include` query parameter is used, the response may contain an `included` array (JSON:API) or inlined related resources (application/json).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationRolesShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationRolesShowResponse"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      },
      "put": {
        "operationId": "updateOrganizationRole",
        "summary": "Change a member's permission or answer your own invite",
        "description": "Change a member's permission, or let an invitee accept or decline their\nown invite. The role's own user may set `status` alone to `accepted` or\n`declined`. Anything else, including `permission`, needs edit rights on\nthe business. A new permission cannot be more privileged than the\ncaller's own role; an owner may assign any.\n",
        "tags": [
          "Organization Roles"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the record",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IncludeOrganizationRole"
          }
        ],
        "responses": {
          "200": {
            "description": "Update. When the `include` query parameter is used, the response may contain an `included` array (JSON:API) or inlined related resources (application/json).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationRolesShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationRolesShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OrganizationRoleUpdateModelAttributes"
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/OrganizationRoleUpdateModel"
              }
            }
          },
          "description": "Update organization role attributes"
        }
      },
      "delete": {
        "operationId": "deleteOrganizationRole",
        "summary": "Remove a business membership or invitation",
        "description": "Permanently delete the membership or invitation identified by `id`.\nThis removes that membership's access to the business or invalidates\nits pending invitation. The person may remove their own membership;\nremoving another person's role requires ownership rights on the\nbusiness and the connection's permitted access.\n\nConfirm the person and business before deletion. This does not delete\nthe person's account, the business, or its farm records. Returns no\ncontent on success. Use `updateOrganizationRole` to change a role's\npermission or invitation status instead of deleting it.\n",
        "tags": [
          "Organization Roles"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the record",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "No Content"
          },
          "401": {
            "description": "Unauthenticated"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/organization-roles/{organization_role_id}/transfer-ownership": {
      "post": {
        "operationId": "transferOrganizationOwnership",
        "summary": "Transfer business ownership",
        "description": "This cannot be undone from this call. The target accepted member becomes\nthe owner and the caller's own role is demoted to `manager`, in one\ntransaction, so there is never a two-owner or zero-owner window. Only an\nowner of the business may call this. The target role must be accepted\nwith a user attached, must not already be an owner, and must not be the\ncaller's own role.\n",
        "tags": [
          "Organization Roles"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "organization_role_id",
            "in": "path",
            "required": true,
            "description": "Id of the target organization role (the new owner's role)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Ownership transferred — the updated target role.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationRolesShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationRolesShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated"
          },
          "403": {
            "description": "Forbidden — caller is not an owner of the organization."
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "422": {
            "description": "Target role is not transferable (pending invite, already owner, or the caller's own role).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/organization-roles/{organization_role_id}/resend-invite": {
      "post": {
        "operationId": "resendOrganizationRoleInvite",
        "summary": "Re-send a team invite",
        "description": "Re-delivers a pending or declined invite (SMS or email) to the invited\nidentifier: the role flips back to `pending`, the 7-day expiry window\nrestarts (`invite_sent_at` is refreshed), and the invite keeps the\n**same** token — the link in the original message keeps working. Only\nan owner of the role's organization may call this (same bar as\ntransfer-ownership). Resends are throttled per invite (60s cooldown);\ninside the cooldown the call is a no-op that still returns the role.\n",
        "tags": [
          "Organization Roles"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "organization_role_id",
            "in": "path",
            "required": true,
            "description": "Id of the invited organization role.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Invite re-sent (or cooldown no-op) — the updated role.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationRolesShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationRolesShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated"
          },
          "403": {
            "description": "Forbidden — caller is not an owner of the organization."
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "422": {
            "description": "Role is not a resendable invite (already attached to a member).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/organizations": {
      "post": {
        "operationId": "createOrganization",
        "summary": "Create a business",
        "description": "Creates a business and makes the signed-in person its owner. `name` is\nrequired. The slug is generated from the name when it is missing or too\nshort.\n",
        "tags": [
          "Organizations"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IncludeOrganization"
          }
        ],
        "responses": {
          "201": {
            "description": "Created. When the `include` query parameter is used, the response may contain an `included` array (JSON:API) or inlined related resources (application/json).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationsShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationsShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated"
          },
          "403": {
            "description": "Forbidden"
          },
          "422": {
            "description": "Invalid request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OrganizationCreateModelAttributes"
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/OrganizationCreateModel"
              }
            }
          },
          "description": "Create organization attributes"
        }
      }
    },
    "/organizations/{id}": {
      "get": {
        "operationId": "getOrganization",
        "summary": "Get a business",
        "description": "Public business profile. Accepts either the business's id (uuid) or its\nslug — the slug is what the public web profile is addressed by\n(`/organizations/{slug}`), so a client holding only a URL can resolve the business.\n",
        "tags": [
          "Organizations"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id (uuid) or slug of the record",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IncludeOrganization"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful. When the `include` query parameter is used, the response may contain an `included` array (JSON:API) or inlined related resources (application/json).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationsShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationsShowResponse"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      },
      "put": {
        "operationId": "updateOrganization",
        "summary": "Update a business",
        "description": "Updates the business's name, slug, place, contact details, or\n`ai_format_enabled` (\"Allow PurpleGrain AI to read my records\"). Needs\nedit rights on the business.\n",
        "tags": [
          "Organizations"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the record",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IncludeOrganization"
          }
        ],
        "responses": {
          "200": {
            "description": "Update. When the `include` query parameter is used, the response may contain an `included` array (JSON:API) or inlined related resources (application/json).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationsShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationsShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OrganizationUpdateModelAttributes"
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/OrganizationUpdateModel"
              }
            }
          },
          "description": "Update organization attributes"
        }
      },
      "delete": {
        "operationId": "deleteOrganization",
        "x-mcp-excluded": true,
        "summary": "Delete a business",
        "description": "Permanently deletes the business and, with it, its memberships, join\nlinks, services, subscriptions, likes, clients and properties, and the\ntasks it produced. Tasks it was assigned as a provider are kept, with\nthat link cleared. This cannot be undone. Only an owner may call this.\nAn AI assistant cannot call this. A signed-in owner in the app still can.\n",
        "tags": [
          "Organizations"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the record",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "No Content"
          },
          "401": {
            "description": "Unauthenticated"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/organizations/{id}/update-avatar": {
      "post": {
        "summary": "Update a business avatar",
        "operationId": "updateOrganizationAvatar",
        "description": "Replaces the business's avatar. Send a file, `avatar_url`, or\n`avatar_base64`. Needs edit rights on the business.\n",
        "tags": [
          "Organizations"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the record",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IncludeOrganization"
          }
        ],
        "responses": {
          "200": {
            "description": "Success. When the `include` query parameter is used, the response may contain an `included` array (JSON:API) or inlined related resources (application/json).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationsShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationsShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "avatar_url": {
                    "type": "string",
                    "format": "uri",
                    "description": "Publicly reachable http(s) URL of the logo image to fetch and attach. Prefer this when the image already lives somewhere addressable."
                  },
                  "avatar_base64": {
                    "type": "string",
                    "description": "Base64-encoded image bytes (optionally a \"data:...;base64,\" URI). Use when you hold raw bytes and have no URL."
                  }
                },
                "description": "Provide either avatar_url or avatar_base64."
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "avatar": {
                    "type": "string",
                    "format": "binary"
                  }
                }
              }
            }
          },
          "required": true
        }
      }
    },
    "/paddocks": {
      "get": {
        "summary": "List Paddocks",
        "operationId": "listPaddocks",
        "description": "List paddocks for one property (`farm_id`), every property in a business\n(`organization_id`), or, with neither, the acting account's properties\n(the signed-in user's personal properties for an assistant connection).\n`farm_id` takes precedence when both are supplied. Results are ordered\nby name, case-insensitively, and access follows the property's visibility\nand the caller's membership and connection grants.\n\nReturns names, areas, land use, crop, boundaries, archive state,\nrecord/task counters and `block_id`: the block the paddock sits in, null\nfor the home block. The blocks themselves (name, kind, members, area) are\nthe property's `blocks` (`getFarm`). Live paddocks only by default; `archived=true`\nselects archived ones and `archived=all` includes both. Use `page` and\n`per_page` (default 200), following `meta.next_page`; no total is returned.\nUse `include` to expand the owning property. This lookup does not change data.\n",
        "tags": [
          "Paddocks"
        ],
        "security": [
          {},
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "farm_id",
            "in": "query",
            "required": false,
            "description": "One property's paddocks. Takes precedence over `organization_id`; both owner filters may be omitted for the acting account's properties.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "organization_id",
            "in": "query",
            "required": false,
            "description": "Every paddock across the business's properties — the organization-wide board and the composer's cross-farm list. With neither this nor `farm_id`, every paddock across the signed-in user's personal properties.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "archived",
            "in": "query",
            "required": false,
            "description": "`false` (the default) for live paddocks, `true` for archived ones only, `all` for both.",
            "schema": {
              "type": "string",
              "example": "false"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "$ref": "#/components/parameters/IncludePaddock"
          }
        ],
        "responses": {
          "200": {
            "description": "Paddocks for the requested property, business or acting account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaddocksIndexResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/PaddocksIndexResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found — neither `farm_id` nor `organization_id` names a record the caller can see.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create Paddock",
        "operationId": "createPaddock",
        "description": "A name is a valid paddock — everything else arrives later, and the map\nlists a paddock with no ring as \"by name\" rather than as incomplete.\n\nNames are unique per property, case-insensitively, across live AND\narchived paddocks: reusing the name of one that was archived is rejected\nhere, where a name is being typed, rather than stranding the archived\npaddock with no way to be restored.\n\n`boundary` is not accepted — the ring is written through\n`updatePaddockBoundary`. Requires organization edit rights.\n",
        "tags": [
          "Paddocks"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IncludePaddock"
          }
        ],
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaddocksShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/PaddocksShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "422": {
            "description": "A missing name, a name already used on the property (live or archived), or an unknown land use",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PaddockCreateModelAttributes"
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/PaddockCreateModel"
              }
            }
          },
          "required": true,
          "description": "Create paddock attributes"
        }
      }
    },
    "/paddocks/{id}": {
      "get": {
        "summary": "Get a paddock",
        "operationId": "getPaddock",
        "description": "One paddock: its name, `area_ha` with its `area_source`, land use, crop,\narchive state, its diary counters (`record_count`, `open_task_count`),\nand its outline. The owning property is the `farm` relationship, and\n`block_id` is the block it sits in inside that property (null for the\nhome block; the block's name and members are the property's `blocks`).\n`withholding` is the grazing or harvest withholding period still ahead,\nfrom the paddock's completed sprays and from the seed treatments of a\ncompleted sowing, or null when the paddock is clear. Fertiliser and\nfield work do not change it. The same fact for every asset on the\nproperty is `getFarmStatus`.\n\n`boundary` is an array of closed rings, each an array of\n`[longitude, latitude]` pairs — **longitude first**, never a GeoJSON\nobject — or null while the paddock is known by name only\n(`has_boundary: false`); `boundary_source` says how the ring was\ncaptured. Write it with `updatePaddockBoundary`. Everything filed on it\n— the property, its tasks and records, moves that name it, and the mobs\nstanding in it — is `listAssetResources` with `asset_type=paddock`. The\ntask board alone is `listTasks` with `asset_id`. Any accepted member of\nthe owning organization can read; a personal property is its owner's.\n",
        "tags": [
          "Paddocks"
        ],
        "security": [
          {},
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the record",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IncludePaddock"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaddocksShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/PaddocksShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      },
      "put": {
        "summary": "Update Paddock",
        "operationId": "updatePaddock",
        "description": "Renaming a paddock, or changing its area, land use or crop, writes a\nrecord on its timeline — \"Renamed from North Block · Area set to 90 ha —\nwas 84 ha\" — composed on the server, because three write surfaces edit a\npaddock and only the server sees before and after atomically. An update\nthat changes none of those four fields writes nothing.\n\n**Archive and restore are this endpoint**: `archived: true` hides the\npaddock from the map and the lists while its records stay on the\nproperty; `archived: false` brings it back. There is no `/archive` route.\nArchiving writes no record.\n\n**`block_id` moves the paddock between blocks**: a live block of the\nsame property puts it in that block, `null` takes it out to the home\nblock. It writes no record. To regroup several paddocks at once use\n`updateBlock` with `paddock_ids`.\n\nThe owning property cannot be changed — a `farm` relationship sent here\nis ignored, so a paddock can never be re-parented and take its records\nwith it. `boundary` is not accepted; see `updatePaddockBoundary`.\n\nRequires organization edit rights.\n",
        "tags": [
          "Paddocks"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the record",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IncludePaddock"
          }
        ],
        "responses": {
          "200": {
            "description": "Updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaddocksShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/PaddocksShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "422": {
            "description": "A blank name, a name already used on the property (live or archived), or an unknown land use",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PaddockUpdateModelAttributes"
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/PaddockUpdateModel"
              }
            }
          },
          "required": true,
          "description": "Update paddock attributes"
        }
      },
      "delete": {
        "summary": "Delete Paddock",
        "operationId": "deletePaddock",
        "description": "Removes the paddock for good — and only while nothing is filed on it.\n\n**A paddock with any record or open task cannot be removed: the request\nis refused with a `409`** whose `errors[0].detail` names the counts\n(\"North Block has 3 records and 1 open task on it — archive this paddock\ninstead of removing it\") and whose `errors[0].code` is `asset_in_use`.\nThe counts are read inside the delete's own transaction, not from the\nnumbers a client read earlier, so a paddock that acquired history\nbetween a client's check and its `DELETE` is kept rather than silently\nunfiled. Archive it instead (`PUT { archived: true }`): its records stay\non the property, and the paddock stays openable from the archive drawer.\n\nA client offers \"Remove this paddock\" only while `record_count` and\n`open_task_count` are both zero and \"Archive this paddock\" otherwise —\nand treats a `409` as the archive face, because the numbers moved under\nit. The movement pointers on other assets' records (`from_paddock_id` /\n`to_paddock_id`) are nullified when an empty paddock is removed, so no\nmob is left pinned to a paddock that no longer exists.\n",
        "tags": [
          "Paddocks"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the record",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "409": {
            "description": "Conflict — the paddock has records or open tasks on it, so it can be\narchived but not removed. `errors[0].detail` names both counts and\nsays to archive instead; `errors[0].code` is `asset_in_use`. Nothing\nwas changed.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/paddocks/{id}/boundary": {
      "put": {
        "summary": "Update Paddock Boundary",
        "operationId": "updatePaddockBoundary",
        "description": "Save (or clear) a paddock's outline.\n\nA dedicated endpoint rather than attributes on `updatePaddock`, because\nit is the single place two rules are enforced:\n\n* **A typed area is never overwritten by a drawn one.**\n  `area_source: \"drawn\"` recomputes `area_ha` from the ring;\n  `area_source: \"typed\"` saves the ring and leaves `area_ha` exactly as\n  it is. A request that names no source keeps the paddock's own, so\n  omitting the field can never clobber a typed number; a paddock that has\n  never had one gets `drawn`.\n* **Geometry is repaired before it is measured.** Rings are validated\n  (at least 4 positions, closed, finite, latitude within ±90 and\n  longitude within ±180), repaired with `ST_MakeValid`, wound\n  counter-clockwise and stored as a MultiPolygon — so a self-crossing\n  ring cannot report a plausible-but-wrong hectare figure. The geometry\n  read back may therefore be reordered or split into parts.\n\n* **A paddock ring must be near its property.** A ring whose centroid\n  lies more than 25 km from the property's map point (`centroid_lat` /\n  `centroid_lon` on the farm) is refused with a 422 that names the\n  distance — with no imagery under the editor, that is the ring drawn\n  over the wrong ground. A property with no map point yet is not\n  guarded.\n\nArea is computed by PostGIS as `ST_Area(geography)` in hectares. A\npaddock carries no map point of its own — it inherits its property's, and\nthe point its name is drawn at is the ring's mean.\n\n**Provenance.** `boundary_source` (traced | grid | gps | imported) is\nthe client's claim about how the ring was captured and travels with it;\n`boundary_captured_by` is stamped server-side from the acting user and\nis never accepted from the request. Both are cleared with the ring.\n\n**The diary.** When the save changes `area_ha` — a first ring measured\nunder `drawn`, a redraw that measures differently, or a cleared ring\nthat takes a measured area with it — one completed observation is filed\non the paddock in the same transaction (\"Boundary redrawn — area 70 ha,\nwas 84 ha\"; \"Boundary drawn — area 70 ha\"; \"Boundary removed — was\n84 ha\"), through the same path a rename or a typed area change uses. A\nring that measures the same, or one saved over a typed area, writes\nnothing. Requires organization edit rights.\n",
        "tags": [
          "Paddocks"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the record",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IncludePaddock"
          }
        ],
        "responses": {
          "200": {
            "description": "The paddock with its saved boundary.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaddocksShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/PaddocksShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "422": {
            "description": "A ring with fewer than 4 positions, an unclosed ring, a coordinate out of range, geometry that cannot be repaired into a polygon, a ring whose centroid is more than 25 km from the property's map point, or an unknown `boundary_source`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PaddockBoundaryModelAttributes"
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/PaddockBoundaryModel"
              }
            }
          },
          "required": true,
          "description": "The paddock outline, plus the area source and the capture provenance that travel with it"
        }
      }
    },
    "/predictions/{id}": {
      "get": {
        "operationId": "getPrediction",
        "summary": "Resolve a Google place ID to a PurpleGrain place",
        "description": "Resolve a Google Places place ID into a PurpleGrain address or city. Returns the supplied place ID, the lookup time as created_at, and the resolved place relationship; use include=place to embed that place. Sends the place ID to Google Places and may create local address, city and location reference records. This is a place-resolution operation with possible writes, rather than a read of an existing prediction record.",
        "tags": [
          "Predictions"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Google Places place ID to resolve, such as a place_id from a place search result.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IncludePrediction"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful. When the `include` query parameter is used, the response may contain an `included` array (JSON:API) or inlined related resources (application/json).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PredictionsShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/PredictionsShowResponse"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/products": {
      "get": {
        "operationId": "listProducts",
        "summary": "List Products",
        "description": "The product reference list the composer's parse matches a sentence against (\"Drenched the\newes with First Mectin…\"): name, label active constituent and the withholding period (WHP) and\nexport slaughter interval (ESI) on its label. `kind` narrows the list: `drench`, `herbicide` (the\nsprays a spray record is parsed against), `fertiliser`, `seed`, `seed_treatment` or `inoculant`;\nan unknown kind is an empty list.\n\n**Read the intervals by their own names, never by `whp_days` and `esi_days`**, which are stored\ncolumns whose meaning depends on `kind` (for a herbicide `whp_days` is the GRAZING period and\n`esi_days` the HARVEST period). Every product carries `meat_whp_days`, `export_esi_days`,\n`grazing_whp_days`, `harvest_whp_days` and `egi_days`, each null where its kind does not have\none. A **fertiliser** has no interval at all: it carries `analysis` (nutrient percentages) and an\n`advisory_note`, which is advice to pass on and never a status.\n This is a short catalogue, not the APVMA ESI list — ESIs change, so\nconfirm the label with the farmer and do not override their figure silently. Signed-in callers only.\n",
        "tags": [
          "Products"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "description": "`drench`, `herbicide`, `fertiliser`, `seed`, `seed_treatment` or `inoculant`.",
            "schema": {
              "type": "string",
              "example": "drench"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProductsIndexResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProductsIndexResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated"
          }
        }
      }
    },
    "/services": {
      "get": {
        "operationId": "listServices",
        "summary": "List one provider's service listings",
        "description": "List agricultural service listings for `owner_id`. `owner_type` is `User` or `Organization` and defaults to a user; any other value is treated as a user. Without `owner_id` the result is empty. Optional filter[id][] and filter[status][] narrow that owner's listings. Returns descriptions, advertised costs, currencies, tags and image URLs; include expands supported owner, category and place details. Use page and per_page, following meta.next_page. This lookup does not book or change services.",
        "tags": [
          "Services"
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Select within paginated results",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Number results to return",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "owner_id",
            "in": "query",
            "required": false,
            "description": "Owner ID to filter on",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "owner_type",
            "in": "query",
            "required": false,
            "description": "Whose listings to list. Send `User` or `Organization`. Anything else is treated as a user. Defaults to a user.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "filter[id][]",
            "in": "query",
            "required": false,
            "description": "ID to filter on",
            "style": "form",
            "explode": true,
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "filter[status][]",
            "in": "query",
            "required": false,
            "description": "Status to filter on",
            "style": "form",
            "explode": true,
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "active",
                  "inactive"
                ]
              }
            }
          },
          {
            "$ref": "#/components/parameters/IncludeService"
          }
        ],
        "responses": {
          "200": {
            "description": "Success. When the `include` query parameter is used, the response may contain an `included` array (JSON:API) or inlined related resources (application/json).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServicesIndexResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ServicesIndexResponse"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createService",
        "summary": "Create a service listing",
        "description": "Creates a service listing. Requires a name, an owner (a user, or a\nbusiness the caller can edit), a place, and a three-letter currency\ncode. `radius`, when sent, is an integer number of metres from 10000\nto 2000000. The slug follows the name.\n",
        "tags": [
          "Services"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IncludeService"
          }
        ],
        "responses": {
          "200": {
            "description": "Success. When the `include` query parameter is used, the response may contain an `included` array (JSON:API) or inlined related resources (application/json).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServicesShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ServicesShowResponse"
                }
              }
            }
          },
          "422": {
            "description": "Invalid request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ServiceCreateModelAttributes"
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/ServiceCreateModel"
              }
            }
          },
          "description": "Create service attributes"
        }
      }
    },
    "/services/search": {
      "get": {
        "operationId": "searchServices",
        "summary": "Search active agricultural service listings",
        "description": "Search active service listings that have a saved location. Filter by an exact category name, a term matching names, descriptions, categories or tags, and optionally latitude, longitude and radius in metres. Valid coordinates enable nearest-first ordering; otherwise results are newest first. Returns matching service details and optional included relationships. Use page and per_page, following meta.next_page. This search does not book services or contact providers.",
        "tags": [
          "Services"
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Select within paginated results",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Number results to return",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "category",
            "in": "query",
            "required": false,
            "description": "Category name to filter on — a single, exact `Category.name` (see `GET /categories`). This is the only category filter; ids and multiple values are not accepted. An unknown name returns no results.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "latitude",
            "in": "query",
            "required": false,
            "description": "Search latitude, example, -36.66892826470023",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "longitude",
            "in": "query",
            "required": false,
            "description": "Search longitude, example, 141.50273631693733",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "radius",
            "in": "query",
            "required": false,
            "description": "Search radius in meters",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "term",
            "in": "query",
            "required": false,
            "description": "Free-text filter matched case-insensitively against name, description, category name and tags. Pass a tag to aggregate every service labelled with it (hashtag-style).",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IncludeService"
          }
        ],
        "responses": {
          "200": {
            "description": "Success. When the `include` query parameter is used, the response may contain an `included` array (JSON:API) or inlined related resources (application/json).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServicesIndexResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ServicesIndexResponse"
                }
              }
            }
          }
        }
      }
    },
    "/services/{id}": {
      "get": {
        "operationId": "getService",
        "summary": "Get an agricultural service listing by ID",
        "description": "Retrieve one existing agricultural service listing using its PurpleGrain service ID. Returns its name, description, status, advertised cost and currency, tags, service radius and image URLs. Use include to expand the owner, category or place and location details. This lookup does not book a service, contact its provider or change the listing.",
        "tags": [
          "Services"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the record",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IncludeService"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful. When the `include` query parameter is used, the response may contain an `included` array (JSON:API) or inlined related resources (application/json).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServicesShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ServicesShowResponse"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      },
      "put": {
        "operationId": "updateService",
        "summary": "Update an existing contractor service listing",
        "description": "Change the service listing identified by id, such as its name, description, active or inactive status, quoted cost and currency, service radius, tags or related place. Send only the requested changes in data; omitted fields stay unchanged. Read the listing first and confirm the changes with the person. The caller must be able to act for the listing's owner, and an MCP connection must allow writes. Returns the updated service. This does not book a contractor, send a message or take payment. Use updateServiceImage for the listing's photo.",
        "tags": [
          "Services"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the record",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IncludeService"
          }
        ],
        "responses": {
          "200": {
            "description": "Update. When the `include` query parameter is used, the response may contain an `included` array (JSON:API) or inlined related resources (application/json).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServicesShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ServicesShowResponse"
                }
              }
            }
          },
          "422": {
            "description": "Errors",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ServiceUpdateModelAttributes"
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/ServiceUpdateModel"
              }
            }
          },
          "description": "Update service attributes"
        }
      },
      "delete": {
        "operationId": "deleteService",
        "summary": "Delete a contractor service listing",
        "description": "Permanently remove the service listing identified by `id` from the\ncontractor catalogue. Requires edit rights on the listing's owner\n(a person or business) and the connection's permitted write access.\nConfirm the listing and owner before deletion.\n\nThe listing's image is removed and existing tasks lose their service\nlink; the tasks themselves are kept. This does not delete the\ncontractor's account or business. Returns no content on success.\nUse `updateService` to edit the listing instead of deleting it.\n",
        "tags": [
          "Services"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the record",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Delete"
          },
          "401": {
            "description": "Unauthenticated"
          },
          "403": {
            "description": "Forbidden"
          }
        }
      }
    },
    "/services/{id}/update-image": {
      "post": {
        "operationId": "updateServiceImage",
        "summary": "Replace a service listing's image",
        "description": "Replaces the service listing's image. Send a file, `image_url`, or\n`image_base64`.\n",
        "tags": [
          "Services"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the record",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IncludeService"
          }
        ],
        "responses": {
          "200": {
            "description": "Success. When the `include` query parameter is used, the response may contain an `included` array (JSON:API) or inlined related resources (application/json).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServicesShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ServicesShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "image_url": {
                    "type": "string",
                    "format": "uri",
                    "description": "Publicly reachable http(s) URL of the image to fetch and attach. Prefer this when the image already lives somewhere addressable."
                  },
                  "image_base64": {
                    "type": "string",
                    "description": "Base64-encoded image bytes (optionally a \"data:...;base64,\" URI). Use when you hold raw bytes and have no URL."
                  }
                },
                "description": "Provide either image_url or image_base64."
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "image": {
                    "type": "string",
                    "format": "binary"
                  }
                }
              }
            }
          },
          "required": true,
          "description": "Provide one of image_url / image_base64 (JSON) or an image file (multipart)."
        }
      }
    },
    "/paddocks/{paddock_id}/spray-records": {
      "post": {
        "operationId": "createSprayRecord",
        "summary": "Create Spray Record",
        "description": "Create a task with a spray record on the existing paddock identified by paddock_id.\nReturns the created task, including its spray child and product rows. Requires permission\nand connection write access for the paddock's owner, which may be a person or business.\n\nAccepts date, description, area_ha, up to ten products, application method as method,\nequipment settings and weather_readings. Product fields include batch, expiry, rate,\ngrazing_whp_days, harvest_whp_days and export intervals. Never invent a batch, expiry,\nrate or withholding period. Supply only values the farmer or label states. Missing\narea_ha defaults to the paddock's area; date defaults to today. A past or today's date\ncreates a completed record at midday in the application time zone and requires at least\none product row. A future date creates a pending task with no completed_at.\n\nThe response derives grazing and harvest dates from the completion date and the longest\npositive withholding period among named products; absent qualifying values produce null\ndates. It stores supplied spray details and does not verify chemical suitability or label\ncompliance. Each successful call creates a new task; check for a same-date, same-product\nduplicate before submitting or retrying an uncertain result. Confirm the proposed write\nwith the farmer, then call with confirm: true. An insufficient_access error means the\nconnection lacks the required access; show its detail instead of retrying the write.\n**Photos.** A file on disk is staged with `createImageUpload`, whose `curl_example` PUTs the bytes, and attached with `image_upload_ids`. Never base64-encode a local photo into a tool call. A public http(s) URL: pass `image_urls`. Only a tiny generated image (under about 100 KB decoded) uses `image_base64s`. One upload can be attached to several records until it expires. On update, `image_mode` defaults to `append` (add to the existing photos); `replace` swaps them all out; `remove_image_ids` deletes specific ones. If any image fails, the whole create or update is rolled back. The attaching call still needs `confirm: true`. `createImageUpload` and its PUT do not.\n",
        "tags": [
          "Spray records"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "paddock_id",
            "in": "path",
            "required": true,
            "description": "The paddock that was sprayed.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Created. The task, with its `spray`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated"
          },
          "403": {
            "description": "Not allowed to write on this paddock. For an assistant the body names the business, the level and where to change it (`insufficient_access`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such paddock."
          },
          "422": {
            "description": "Invalid request."
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SprayRecordCreateModelAttributes"
              },
              "example": {
                "data": {
                  "date": "2026-09-28",
                  "area_ha": 72,
                  "description": "Spray Big Dam for thistles",
                  "products": [
                    {
                      "name": "Lontrel",
                      "batch": "LT7731",
                      "rate": "300 mL / ha",
                      "grazing_whp_days": 7,
                      "harvest_whp_days": 28
                    }
                  ]
                }
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/SprayRecordCreateModel"
              },
              "example": {
                "data": {
                  "type": "spray-record",
                  "attributes": {
                    "date": "2026-09-28",
                    "area_ha": 72,
                    "description": "Spray Big Dam for thistles",
                    "products": [
                      {
                        "name": "Lontrel",
                        "batch": "LT7731",
                        "rate": "300 mL / ha",
                        "grazing_whp_days": 7,
                        "harvest_whp_days": 28
                      }
                    ]
                  }
                }
              }
            }
          },
          "description": "Spray record attributes"
        }
      }
    },
    "/spray-records/{id}": {
      "put": {
        "operationId": "updateSprayRecord",
        "summary": "Update Spray Record",
        "description": "Change any supplied spray details, including area, products, application method, equipment\nsettings and weather readings (`products` replaces the set; omitted fields stay unchanged). On a done\nrecord this never reopens the task; on an open task the same child is updated. Answers with\nthe task exactly as `GET /tasks/{id}` would. `id` is the SprayRecord id (`spray.id` on the\ntask), not the task id. A done record with no product row is refused (422, \"A spray needs\nat least one product\"). Never invent a batch, rate or withholding period. If the call errors\nor times out, read the record back before retrying. Needs a grant that allows writing in the\nrecord's business. A 403 whose `code` is `insufficient_access` is that refusal; its `detail` names the business and the access that is missing.\n**Photos.** A file on disk is staged with `createImageUpload`, whose `curl_example` PUTs the bytes, and attached with `image_upload_ids`. Never base64-encode a local photo into a tool call. A public http(s) URL: pass `image_urls`. Only a tiny generated image (under about 100 KB decoded) uses `image_base64s`. One upload can be attached to several records until it expires. On update, `image_mode` defaults to `append` (add to the existing photos); `replace` swaps them all out; `remove_image_ids` deletes specific ones. If any image fails, the whole create or update is rolled back. The attaching call still needs `confirm: true`. `createImageUpload` and its PUT do not.\n",
        "tags": [
          "Spray records"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The SprayRecord id (`spray.id` on the task).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success. The task, with its `spray`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated"
          },
          "403": {
            "description": "Not allowed to write on this record.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such record."
          },
          "422": {
            "description": "Invalid request."
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SprayRecordUpdateModelAttributes"
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/SprayRecordUpdateModel"
              }
            }
          },
          "description": "Spray record attributes"
        }
      }
    },
    "/subscriptions": {
      "get": {
        "summary": "List an account's existing subscriptions",
        "operationId": "listSubscriptions",
        "description": "List existing subscription records for the required `owner_id` and `owner_type` (`users` or `organizations`). Requires permission to manage that account, including write access in an assistant connection's grant. Returns plan, status, recorded price, interval and subscription dates; include=subscribable embeds the owner. Use page and per_page, following meta.next_page. Reading this list does not start, renew or cancel subscriptions or charge a payment.",
        "tags": [
          "Subscriptions"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "owner_id",
            "in": "query",
            "required": true,
            "description": "Owner (subscribable) id to scope the subscriptions to.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "owner_type",
            "in": "query",
            "required": true,
            "description": "Owner type. Send `users` or `organizations`.",
            "schema": {
              "type": "string",
              "enum": [
                "users",
                "organizations"
              ]
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Select within paginated results",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Number results to return",
            "schema": {
              "type": "integer"
            }
          },
          {
            "$ref": "#/components/parameters/IncludeSubscription"
          }
        ],
        "responses": {
          "200": {
            "description": "Subscriptions for the requested owner. When the `include` query parameter is used, the response may contain an `included` array (JSON:API) or inlined related resources (application/json).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SubscriptionsIndexResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/SubscriptionsIndexResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          }
        }
      },
      "post": {
        "summary": "Create Subscription",
        "operationId": "createSubscription",
        "x-web-or-mobile-only": true,
        "tags": [
          "Subscriptions"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IncludeSubscription"
          }
        ],
        "responses": {
          "201": {
            "description": "Created. The first subscription for an owner that has not used its free trial starts in `trialing`; otherwise it starts `active`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SubscriptionsShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/SubscriptionsShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden — first-party only; refused to an API key or an assistant's token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Entity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SubscriptionCreateModelAttributes"
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/SubscriptionCreateModel"
              }
            }
          },
          "required": true,
          "description": "Create subscription attributes"
        }
      }
    },
    "/subscriptions/{id}": {
      "get": {
        "summary": "Get an existing subscription record by ID",
        "operationId": "getSubscription",
        "description": "Retrieve an existing subscription by its PurpleGrain subscription ID. Returns its plan, status, recorded price and interval, service limit, trial and billing-period dates, and start or cancellation dates. Use include=subscribable to embed the owning user or organization. Requires permission to manage that owner, including write access in an assistant connection's grant. Reading the record does not start, renew or cancel a subscription or charge a payment.",
        "tags": [
          "Subscriptions"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the record",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IncludeSubscription"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful. When the `include` query parameter is used, the response may contain an `included` array (JSON:API) or inlined related resources (application/json).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SubscriptionsShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/SubscriptionsShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      },
      "put": {
        "summary": "Subscription",
        "operationId": "updateSubscription",
        "x-web-or-mobile-only": true,
        "tags": [
          "Subscriptions"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the record",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IncludeSubscription"
          }
        ],
        "responses": {
          "200": {
            "description": "Updated. Setting `status` to `cancelled` stamps `cancelled_at`. When the `include` query parameter is used, the response may contain an `included` array (JSON:API) or inlined related resources (application/json).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SubscriptionsShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/SubscriptionsShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden — first-party only; refused to an API key or an assistant's token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SubscriptionUpdateModelAttributes"
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/SubscriptionUpdateModel"
              }
            }
          },
          "required": true,
          "description": "Update subscription attributes"
        }
      }
    },
    "/users": {
      "post": {
        "operationId": "createUser",
        "x-web-or-mobile-only": true,
        "summary": "Create User",
        "description": "Email registration. Returns 204; a confirmation email follows. With the\noptional top-level `property_draft` (the landing page's map, design\nhandoff 29) the property — and the business, when `owner_kind` is\n`business` — is created in the same transaction as the account, its\nring saved and measured exactly as `updateFarmBoundary` would. An\ninvalid draft fails the whole request with 422 and nothing is created.\n",
        "tags": [
          "Public",
          "Registrations"
        ],
        "parameters": [],
        "responses": {
          "204": {
            "description": "Created — the account, and with `property_draft` the property (and business) too."
          },
          "403": {
            "description": "Forbidden — first-party only; refused to an API key or an assistant's token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "422": {
            "description": "Invalid account fields, or an invalid `property_draft` (errors point at `/data/attributes/property_draft.<field>`); nothing is created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "user": {
                    "type": "object",
                    "properties": {
                      "email": {
                        "type": "string",
                        "format": "email"
                      },
                      "mobile": {
                        "type": "string",
                        "description": "Optional companion mobile (AU, any common format;\nnormalized to E.164). Stored unverified; an SMS\nverification code is sent immediately (see\n/users/mobile-verifications). Phone-only accounts are\ncreated via /users/phone-sessions instead.\n"
                      },
                      "password": {
                        "type": "string"
                      },
                      "password_confirmation": {
                        "type": "string"
                      },
                      "account_mode": {
                        "type": "string",
                        "enum": [
                          "grower",
                          "contractor"
                        ],
                        "description": "Grower (hiring) vs Contractor (working); defaults to grower."
                      }
                    }
                  },
                  "property_draft": {
                    "$ref": "#/components/schemas/RegistrationPropertyDraft"
                  }
                }
              }
            }
          },
          "required": true,
          "description": "User registration parameters (email or mobile, plus password), and optionally the property mapped before registering"
        }
      },
      "get": {
        "operationId": "listUsers",
        "summary": "Look up user profiles by IDs or email addresses",
        "description": "Look up existing user profiles using filter[id][] or filter[email][]. Requires authentication and returns an empty list when no filter is supplied; an email filter takes precedence when both arrays are supplied. Returns profile fields visible to the caller, with optional included services or place details. Use page and per_page, following meta.next_page. This lookup does not change accounts or send messages.",
        "tags": [
          "Users"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Select within paginated results",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Number results to return",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "filter[id][]",
            "in": "query",
            "required": false,
            "description": "Filter returned results on ids",
            "style": "form",
            "explode": true,
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "filter[email][]",
            "in": "query",
            "required": false,
            "description": "Filter returned results on emails",
            "style": "form",
            "explode": true,
            "allowReserved": true,
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "$ref": "#/components/parameters/IncludeUser"
          }
        ],
        "responses": {
          "200": {
            "description": "Success. When the `include` query parameter is used, the response may contain an `included` array (JSON:API) or inlined related resources (application/json).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UsersIndexResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/UsersIndexResponse"
                }
              }
            }
          }
        }
      }
    },
    "/users/{id}": {
      "get": {
        "operationId": "getUser",
        "summary": "Get a PurpleGrain user profile by ID",
        "description": "Retrieve one existing user's profile using their PurpleGrain user ID. Returns their name, about text, handle, avatar URLs and the contact or account fields visible to the caller under the profile's visibility rules. Use include to expand services or place and location details. This lookup does not change the profile or send a message to the user.",
        "tags": [
          "Users"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the record",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IncludeUser"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful. When the `include` query parameter is used, the response may contain an `included` array (JSON:API) or inlined related resources (application/json).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UsersShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/UsersShowResponse"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      },
      "put": {
        "operationId": "updateUser",
        "summary": "Update your profile and account preferences",
        "description": "Update the signed-in person's own profile, identified by `id` from\n`fetchCurrentUser`. Requires that person's permitted write access;\nanother person's profile cannot be edited through this operation.\n\nSend only the fields the person wants changed: `name`, `about`,\n`handle`, `mobile`, `contact_info`, `account_mode`,\n`default_record_mode`, or the `place` relationship. Profile and\ncontact changes may affect what other people see. Confirm the\nrequested changes before writing. Returns the updated user.\n\nThis does not change the login email, password, avatar, or SMS\nnotification preferences, and changing a mobile here does not verify\nit. Password and phone verification flows stay on PurpleGrain's own\npages; use `updateUserAvatar` for the profile picture and\n`updateSmsPreferences` for SMS notification preferences.\n",
        "tags": [
          "Users"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the record",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IncludeUser"
          }
        ],
        "responses": {
          "200": {
            "description": "Update. When the `include` query parameter is used, the response may contain an `included` array (JSON:API) or inlined related resources (application/json).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UsersShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/UsersShowResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UserUpdateModelAttributes"
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/UserUpdateModel"
              }
            }
          },
          "description": "Update user attributes"
        }
      },
      "delete": {
        "operationId": "deleteUser",
        "x-web-or-mobile-only": true,
        "summary": "User",
        "tags": [
          "Users"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the record",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Delete"
          },
          "401": {
            "description": "Unauthenticated"
          },
          "403": {
            "description": "Forbidden — first-party only; refused to an API key or an assistant's token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/users/access-tokens": {
      "post": {
        "operationId": "createAccessToken",
        "x-web-or-mobile-only": true,
        "summary": "Create access token (omniauth)",
        "description": "Signs in with an identity **the provider vouches for**. `credentials.token` must be the provider's own token\n(Google ID token, Facebook user access token, Apple identity token); the API checks it with the provider, checks it\nwas issued to Purple Grain, and signs in only the account whose email **the provider** returned, which must equal\n`info.email` and `uid`. Any other claim, a missing token, or a provider the API cannot verify is a **403**. Returns a\n**Doorkeeper** access token in JSON (same shape as `POST /oauth/token`,\nplus **`user_id`**). Use **`Authorization: Bearer &lt;access_token&gt;`** on `/api/v1/*`. Prefer **`POST /oauth/token`**\nwhen you have a registered OAuth client and want strict RFC 6749 behavior.\n",
        "tags": [
          "Public",
          "AccessTokens"
        ],
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success — Doorkeeper token response including `user_id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DoorkeeperAccessTokenResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/DoorkeeperAccessTokenResponse"
                }
              }
            }
          },
          "403": {
            "description": "Invalid request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "provider": {
                    "type": "string"
                  },
                  "uid": {
                    "type": "string"
                  },
                  "info": {
                    "type": "object",
                    "properties": {
                      "email": {
                        "type": "string",
                        "format": "email"
                      },
                      "name": {
                        "type": "string"
                      }
                    }
                  },
                  "credentials": {
                    "type": "object",
                    "properties": {
                      "scope": {
                        "type": "string",
                        "nullable": true
                      },
                      "token": {
                        "type": "string",
                        "nullable": true
                      },
                      "refresh_token": {
                        "type": "string",
                        "nullable": true
                      },
                      "expires_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                      }
                    }
                  },
                  "extra": {
                    "type": "object",
                    "properties": {
                      "id_token": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "required": true,
          "description": "Create access token parameters"
        }
      }
    },
    "/users/confirmation": {
      "post": {
        "operationId": "createConfirmation",
        "x-web-or-mobile-only": true,
        "summary": "Create Confirmation",
        "tags": [
          "Public",
          "Confirmations"
        ],
        "parameters": [],
        "responses": {
          "204": {
            "description": "Success"
          },
          "403": {
            "description": "Forbidden — first-party only; refused to an API key or an assistant's token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "422": {
            "description": "Invalid",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "user": {
                    "type": "object",
                    "properties": {
                      "email": {
                        "type": "string",
                        "format": "email"
                      }
                    }
                  }
                }
              }
            }
          },
          "required": true,
          "description": "Create confirmation parameters"
        }
      },
      "get": {
        "operationId": "confirmUser",
        "x-mcp-excluded": true,
        "summary": "Confirm",
        "tags": [
          "Public",
          "Confirmations"
        ],
        "parameters": [
          {
            "name": "confirmation_token",
            "in": "query",
            "description": "Confirmation token parameter",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Success"
          },
          "422": {
            "description": "Invalid request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/users/password": {
      "post": {
        "operationId": "createPasswordReset",
        "x-web-or-mobile-only": true,
        "summary": "Create Password Reset",
        "tags": [
          "Public",
          "Passwords"
        ],
        "parameters": [],
        "responses": {
          "204": {
            "description": "Send New Password Reset Instructions"
          },
          "403": {
            "description": "Forbidden — first-party only; refused to an API key or an assistant's token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "422": {
            "description": "Cannot Send New Password Reset Instructions If User Does Not Exist",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "user": {
                    "type": "object",
                    "properties": {
                      "email": {
                        "type": "string",
                        "format": "email"
                      }
                    }
                  }
                }
              }
            }
          },
          "required": true,
          "description": "Reset password parameters"
        }
      },
      "put": {
        "operationId": "updatePassword",
        "x-web-or-mobile-only": true,
        "summary": "Update Password With Reset Token",
        "tags": [
          "Public",
          "Passwords"
        ],
        "parameters": [],
        "responses": {
          "204": {
            "description": "Can Confirm New User"
          },
          "403": {
            "description": "Forbidden — first-party only; refused to an API key or an assistant's token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "422": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "user": {
                    "type": "object",
                    "properties": {
                      "reset_password_token": {
                        "type": "string"
                      },
                      "password": {
                        "type": "string"
                      },
                      "password_confirmation": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "required": true,
          "description": "Password reset token parameter"
        }
      }
    },
    "/users/sign-in": {
      "post": {
        "operationId": "signIn",
        "x-web-or-mobile-only": true,
        "summary": "Sign in (first-party)",
        "description": "**First-party** email/password login. Returns a **Doorkeeper** access token in JSON — same fields as\n**`POST /oauth/token`** with `grant_type=password`, plus **`user_id`**. Does not require `client_id` / `client_secret`\nin the request (the server uses the configured first-party OAuth application). For third-party or strict OAuth\nclients, call **`POST /oauth/token`** on the host root instead (see **OAuth** tag).\n",
        "tags": [
          "Public",
          "Sessions"
        ],
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success — Doorkeeper access token; includes `user_id` for convenience.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DoorkeeperAccessTokenResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/DoorkeeperAccessTokenResponse"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — first-party only; refused to an API key or an assistant's token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "422": {
            "description": "Invalid request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "user": {
                    "type": "object",
                    "properties": {
                      "email": {
                        "type": "string",
                        "format": "email"
                      },
                      "password": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "required": true,
          "description": "User sign-in parameters (email + password; for phone sign-in see /users/phone-sessions)"
        }
      }
    },
    "/users/sign-out": {
      "delete": {
        "operationId": "signOut",
        "summary": "Sign out (revoke access token)",
        "description": "Revokes only this connection's own bearer token, the **Doorkeeper** access token sent in **`Authorization: Bearer`**.\nEquivalent to **`POST /oauth/revoke`** for that token. The person's other sessions stay signed in.\nReturns **400** if the bearer token is missing or not a valid/active Doorkeeper access token.\n",
        "tags": [
          "Public",
          "Sessions"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "responses": {
          "204": {
            "description": "Token revoked."
          },
          "400": {
            "description": "Missing or invalid access token."
          }
        }
      }
    },
    "/users/validate-access-token": {
      "post": {
        "operationId": "validateAccessToken",
        "x-mcp-excluded": true,
        "summary": "Validate access token",
        "description": "Optional helper: checks whether an **opaque Doorkeeper access token** (the token string, not a JWT) is present,\nnot revoked, and not expired. Use the same value you would send in **`Authorization: Bearer`**. For new clients,\nvalidating by calling a lightweight authenticated endpoint is often sufficient.\n",
        "tags": [
          "Users"
        ],
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "valid": {
                      "type": "boolean"
                    },
                    "user_id": {
                      "type": "string",
                      "format": "uuid",
                      "nullable": true
                    }
                  }
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "valid": {
                      "type": "boolean"
                    },
                    "user_id": {
                      "type": "string",
                      "format": "uuid",
                      "nullable": true
                    }
                  }
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "token": {
                    "type": "string"
                  }
                }
              }
            }
          },
          "required": true,
          "description": "Access token string to validate"
        }
      }
    },
    "/users/{id}/update-avatar": {
      "post": {
        "summary": "Update the user's avatar",
        "operationId": "updateUserAvatar",
        "description": "Replaces this user's avatar. Send a file, `avatar_url`, or `avatar_base64`.\n",
        "tags": [
          "Users"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the record",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IncludeUser"
          }
        ],
        "responses": {
          "200": {
            "description": "Success. When the `include` query parameter is used, the response may contain an `included` array (JSON:API) or inlined related resources (application/json).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UsersShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/UsersShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "avatar_url": {
                    "type": "string",
                    "format": "uri",
                    "description": "Publicly reachable http(s) URL of the avatar image to fetch and attach. Prefer this when the image already lives somewhere addressable."
                  },
                  "avatar_base64": {
                    "type": "string",
                    "description": "Base64-encoded image bytes (optionally a \"data:...;base64,\" URI). Use when you hold raw bytes and have no URL."
                  }
                },
                "description": "Provide either avatar_url or avatar_base64."
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "avatar": {
                    "type": "string",
                    "format": "binary"
                  }
                }
              }
            }
          },
          "required": true,
          "description": "Provide one of avatar_url / avatar_base64 (JSON) or an avatar file (multipart)."
        }
      }
    },
    "/users/handle": {
      "get": {
        "summary": "Get a user profile by public handle",
        "operationId": "getUserByHandle",
        "description": "Retrieve one existing user's profile using the handle from their public PurpleGrain profile address. Returns their name, about text, avatar URLs and contact or account fields visible to the caller. Use include to expand services or place and location details. Returns not found for an unknown handle. This lookup does not change the profile or contact the user.",
        "tags": [
          "Public",
          "Users"
        ],
        "parameters": [
          {
            "name": "handle",
            "in": "query",
            "required": true,
            "description": "The user's handle (slug).",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IncludeUser"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful. When the `include` query parameter is used, the response may contain an `included` array (JSON:API) or inlined related resources (application/json).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UsersShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/UsersShowResponse"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/users/fetch-current-user": {
      "get": {
        "operationId": "fetchCurrentUser",
        "summary": "Get the signed-in user",
        "description": "The signed-in user. Start here when looking up a property: take this\nuser's id to `listOrganizationRoles` (with `include=organization`) for\nthe businesses they belong to, then `listFarms` with no\n`organization_id` for personal properties and with each\n`organization_id` for a business's.\n",
        "tags": [
          "Users"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IncludeUser"
          }
        ],
        "responses": {
          "200": {
            "description": "Success. When the `include` query parameter is used, the response may contain an `included` array (JSON:API) or inlined related resources (application/json).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UsersShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/UsersShowResponse"
                }
              }
            }
          }
        }
      }
    },
    "/users/mobile-verifications": {
      "post": {
        "operationId": "sendMobileVerification",
        "x-web-or-mobile-only": true,
        "summary": "Send mobile verification code",
        "description": "Sends (or re-sends) a 6-digit SMS one-time code to the account with the\ngiven mobile number. Used after a mobile-first sign-up and when changing\nthe mobile in settings. Always returns **204** — the response never\nreveals whether a mobile number has an account. Re-sends are rate-limited\n(60s cooldown); codes expire after 10 minutes.\n",
        "tags": [
          "Public",
          "MobileVerifications"
        ],
        "parameters": [],
        "responses": {
          "204": {
            "description": "Accepted (a code was sent if the mobile matches an unverified account)."
          },
          "403": {
            "description": "Forbidden — first-party only; refused to an API key or an assistant's token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "mobile": {
                    "type": "string",
                    "description": "Australian mobile in any common format."
                  }
                },
                "required": [
                  "mobile"
                ]
              }
            }
          },
          "required": true,
          "description": "The mobile number to verify"
        }
      }
    },
    "/users/mobile-verifications/confirm": {
      "post": {
        "operationId": "confirmMobileVerification",
        "x-mcp-excluded": true,
        "summary": "Confirm mobile verification code",
        "description": "Verifies the SMS one-time code. On success the mobile is marked verified\nand the account is confirmed (mobile-first accounts can then sign in).\nCodes expire after 10 minutes and allow 5 attempts.\n",
        "tags": [
          "Public",
          "MobileVerifications"
        ],
        "parameters": [],
        "responses": {
          "204": {
            "description": "Verified."
          },
          "422": {
            "description": "Incorrect, expired, or too many attempts.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "mobile": {
                    "type": "string"
                  },
                  "code": {
                    "type": "string",
                    "description": "The 6-digit code from the SMS."
                  }
                },
                "required": [
                  "mobile",
                  "code"
                ]
              }
            }
          },
          "required": true,
          "description": "Mobile number and code"
        }
      }
    },
    "/users/phone-sessions": {
      "post": {
        "operationId": "sendPhoneSessionCode",
        "x-web-or-mobile-only": true,
        "summary": "Start phone sign-in (send SMS code)",
        "description": "Passwordless phone sign-in, step 1: sends a 6-digit SMS one-time code\n(self-issued) to the given Australian mobile. Works for numbers with\nor without an existing account — verifying a new number creates the\naccount (phone sign-in is also phone sign-up). Always returns **204**;\nthe response never reveals whether a number has an account. Re-sends are\nrate-limited per number and per IP.\n",
        "tags": [
          "Public",
          "PhoneSessions"
        ],
        "parameters": [],
        "responses": {
          "204": {
            "description": "Accepted (a code was sent if the number and rate limits allow)."
          },
          "403": {
            "description": "Forbidden — first-party only; refused to an API key or an assistant's token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "mobile": {
                    "type": "string",
                    "description": "Australian mobile in any common format."
                  }
                },
                "required": [
                  "mobile"
                ]
              }
            }
          },
          "required": true,
          "description": "The mobile number to sign in with"
        }
      }
    },
    "/users/phone-sessions/verify": {
      "post": {
        "operationId": "verifyPhoneSession",
        "x-web-or-mobile-only": true,
        "summary": "Complete phone sign-in (verify code)",
        "description": "Passwordless phone sign-in, step 2: checks the SMS code. On success the\nmobile is marked verified, the account is **created if new** (with an\nunusable random password; `account_mode` applies then, default grower),\nand a first-party Doorkeeper access token is returned — the same payload\nas `POST /users/sign-in`.\n\nWith the optional `property_draft` (the landing page's map, design\nhandoff 29) and a number that has no account yet, the property — and the\nbusiness, when `owner_kind` is `business` — is created in the same\ntransaction as the account. An existing account signs in and the draft\nis ignored. The draft is checked before the code for every number, so an\ninvalid one is a 422 that spends no code.\n",
        "tags": [
          "Public",
          "PhoneSessions"
        ],
        "parameters": [],
        "responses": {
          "200": {
            "description": "Signed in — Doorkeeper access token; includes `user_id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DoorkeeperAccessTokenResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/DoorkeeperAccessTokenResponse"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — first-party only; refused to an API key or an assistant's token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "422": {
            "description": "Incorrect or expired code, or an invalid `property_draft` (errors point at `/data/attributes/property_draft.<field>`; nothing is created and no code is spent).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "mobile": {
                    "type": "string"
                  },
                  "code": {
                    "type": "string",
                    "description": "The 6-digit code from the SMS."
                  },
                  "account_mode": {
                    "type": "string",
                    "enum": [
                      "grower",
                      "contractor"
                    ],
                    "description": "Applied only when the account is created by this verification; defaults to grower."
                  },
                  "property_draft": {
                    "$ref": "#/components/schemas/RegistrationPropertyDraft"
                  }
                },
                "required": [
                  "mobile",
                  "code"
                ]
              }
            }
          },
          "required": true,
          "description": "Mobile number and code"
        }
      }
    },
    "/farms/{farm_id}/treatment-register": {
      "get": {
        "operationId": "getTreatmentRegister",
        "summary": "Get property treatment, spray or paddock-input registers",
        "description": "A register of a property as data, of one `kind`:\n\n* `register` (the default): the LPA Section 2 treatment register, one row per PRODUCT of each\n  COMPLETED treatment or husbandry record on the chosen mobs in the date range, oldest first,\n  plus the same rows grouped per mob and a `summary`. A husbandry record with no product has no\n  row. `expiry` and `route` are what was entered; `not recorded` where nobody did.\n* `nvd`: only treatments still under a withholding period or export interval today, grouped per\n  mob, for the National Vendor Declaration treatment question.\n* `spray`: the chemical use register, one row per spray product on the chosen paddocks, with the\n  crop, area, rate, water rate, method, applicator, the first weather reading and the grazing and\n  harvest end dates, grouped per paddock.\n* `paddock-inputs`: one row per product line of each fertiliser and sowing on the chosen paddocks:\n  the rate, total, method or placement, analysis and who applied it. Fertiliser never holds a\n  paddock back; only a seed treatment shows a grazing end date.\n\nThe preview, the PDF, the CSV and the shared page are all drawn from these rows. A paddock register\ncarries `columns` (the table's headings) and `paddocks`; the others carry `mobs`. Every value a\nrecord should carry and does not is the literal text `not recorded`, never empty (a genuinely\noptional value is null). `property.pic` is null (the property has no PIC field).\n\nPlain JSON, not JSON:API. Needs read access to the property's records: any member of the\nowning business (a viewer included) or the person on a personal property, and an assistant\nwithin its read grant. A property shared publicly or by link does NOT share its treatment\nregister. Not cached.\n",
        "tags": [
          "Treatment register"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "farm_id",
            "in": "path",
            "required": true,
            "description": "The property.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "scope",
            "in": "query",
            "required": false,
            "description": "`property` (every mob or paddock, the default), `mobs` (only `mob_ids`, for the treatment register and the NVD list) or `paddocks` (only `paddock_ids`, for the spray and paddock inputs registers).",
            "schema": {
              "type": "string",
              "enum": [
                "property",
                "mobs",
                "paddocks"
              ]
            }
          },
          {
            "name": "mob_ids",
            "in": "query",
            "required": false,
            "description": "The chosen mobs (repeat the parameter). Sending it implies `scope=mobs`.",
            "style": "form",
            "explode": true,
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "paddock_ids",
            "in": "query",
            "required": false,
            "description": "The chosen paddocks (repeat the parameter). Sending it implies `scope=paddocks`.",
            "style": "form",
            "explode": true,
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "First treatment date, `YYYY-MM-DD`. Defaults to a year before `to`.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Last treatment date, `YYYY-MM-DD`. Defaults to today.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "description": "`register` (every treatment and husbandry product in range, the default), `nvd` (only treatments still under WHP/ESI today), `spray` (the chemical use register) or `paddock-inputs` (fertiliser and sowing).",
            "schema": {
              "type": "string",
              "enum": [
                "register",
                "nvd",
                "spray",
                "paddock-inputs"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The register.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TreatmentRegisterResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/TreatmentRegisterResponse"
                }
              }
            }
          },
          "400": {
            "description": "A malformed date, an unknown kind or scope, or an end date before the start.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated"
          },
          "403": {
            "description": "Not a member of this property's business.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such property."
          }
        }
      }
    },
    "/farms/{farm_id}/record-exports": {
      "post": {
        "operationId": "createRecordExport",
        "x-web-or-mobile-only": true,
        "summary": "Request an export of a property's treatment register",
        "description": "Stores the request (which mobs, dates, kind and format) and answers with it and its\n`summary`. The file itself is rendered on demand by `GET /record-exports/{id}/file`; nothing\nis stored. Needs read access to the property's records (any member, a viewer included).\n\nFirst-party web and mobile only: an assistant or API key is refused with a 403 and the\noperation is not an MCP tool.\n",
        "tags": [
          "Record exports"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "farm_id",
            "in": "path",
            "required": true,
            "description": "The property.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "description": "The register to export.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RecordExportRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecordExportCreateResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/RecordExportCreateResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated"
          },
          "403": {
            "description": "Not a member of this property's business, or an assistant / API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such property."
          },
          "422": {
            "description": "A mob that is not on the property, an empty `mob_ids` with `scope=mobs`, a date range that runs backwards or is over ten years, or an unknown kind, scope or format.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/record-exports/{id}/file": {
      "get": {
        "operationId": "downloadRecordExport",
        "x-web-or-mobile-only": true,
        "summary": "Download an export as a PDF or CSV",
        "description": "Renders the export's register as a file, on request: a PDF (A4 landscape, LPA Section 2 table,\n`not recorded` cells in amber italic, \"Page n of N\") or a UTF-8 CSV (every cell quoted,\nformula-looking cells prefixed with a single quote). Sent as an attachment named like\n`treatment-register-yarrabee-2025-10-05-2026-10-05.pdf`, never cached. The rows are rebuilt\nfrom the records now, so a corrected record shows in the next download. Needs read access to\nthe property's records. First-party web and mobile only.\n",
        "tags": [
          "Record exports"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The export.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "description": "`pdf` or `csv`. Defaults to the format the export was requested in.",
            "schema": {
              "type": "string",
              "enum": [
                "pdf",
                "csv"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The file, as an attachment.",
            "content": {
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "description": "An unknown format."
          },
          "401": {
            "description": "Unauthenticated"
          },
          "403": {
            "description": "Not a member of the property's business, or an assistant / API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such export."
          }
        }
      }
    },
    "/record-exports/{record_export_id}/shares": {
      "post": {
        "operationId": "createRecordShare",
        "x-web-or-mobile-only": true,
        "summary": "Share an export by email, text or download",
        "description": "Hands an export to someone with a view-only link (`/s/<token>` on the web app) that opens the\nregister without an account until it expires or is revoked.\n\n* `email`: sends the file as an attachment with the link and the expiry, and an optional note.\n* `sms`: sends exactly \"{first name} from PurpleGrain shared drench records for {property}:\n  {link} (expires D/M/YY)\". (Outside production the text is logged, not sent.)\n* `download`: sends nothing; records the share so it appears in the sharing history.\n\nNeeds edit rights on the property (owner, admin or manager; a viewer is refused). First-party\nweb and mobile only: an assistant or API key is refused and there is no MCP tool, because this\nsends records off the platform. The recipient is returned masked; send `again_of` instead of\n`to` to send an earlier share again.\n\nIf the email or text fails, the share is stored as `failed` (its link then answers 404) and the\nresponse is a **502** with `errors[0].code` `email_bounced` or `text_not_delivered`, the same\n`share`, and Try again / Change address / Download instead as the client's choices.\n",
        "tags": [
          "Record shares"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "record_export_id",
            "in": "path",
            "required": true,
            "description": "The export to share.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "description": "Who to send it to.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RecordShareRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Shared.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecordShareResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/RecordShareResponse"
                }
              }
            }
          },
          "400": {
            "description": "A bad `via`, `to`, `expires_in_days` or `again_of`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated"
          },
          "403": {
            "description": "Not allowed to share on this property (a viewer), or an assistant / API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such export, or `again_of` is not a share on this property."
          },
          "502": {
            "description": "The share was stored but the email or text did not go.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecordShareFailureResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/RecordShareFailureResponse"
                }
              }
            }
          }
        }
      }
    },
    "/farms/{farm_id}/record-shares": {
      "get": {
        "operationId": "listRecordShares",
        "x-web-or-mobile-only": true,
        "summary": "The sharing history of a property",
        "description": "Every share made from this property's exports, newest first (at most 100), with what was shared\n(`export`: kind, scope, dates, format), who it went to (masked), how (`via`), its derived `state`\n(sent, opened, expired, revoked, failed), when it was made and when it expires, and the link.\nNeeds edit rights on the property, because each row carries a link that is a credential.\nFirst-party web and mobile only. Not cached.\n",
        "tags": [
          "Record shares"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "farm_id",
            "in": "path",
            "required": true,
            "description": "The property.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The history.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecordSharesResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/RecordSharesResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated"
          },
          "403": {
            "description": "Not allowed to share on this property, or an assistant / API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such property."
          }
        }
      }
    },
    "/record-shares/{id}/revoke": {
      "post": {
        "operationId": "revokeRecordShare",
        "x-web-or-mobile-only": true,
        "summary": "Revoke a share link",
        "description": "Stops the link working: it answers 410 (\"This link has been revoked.\") from now on. Idempotent:\nrevoking a revoked share keeps the first time and answers 200 again. Needs edit rights on the\nproperty. First-party web and mobile only.\n",
        "tags": [
          "Record shares"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The share.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The share, now `revoked`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecordShareResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/RecordShareResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated"
          },
          "403": {
            "description": "Not allowed to share on this property, or an assistant / API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such share."
          }
        }
      }
    },
    "/shares/{token}": {
      "get": {
        "operationId": "getSharedRecords",
        "x-web-or-mobile-only": true,
        "summary": "Read a shared treatment register",
        "description": "Public (no account): the token in the link IS the credential. Returns only the register the\nowner chose to export, per mob with an `under_withholding` / `clear` status and the safe dates,\nwho shared it and when the link expires. No ids, emails or phone numbers. The rows are rebuilt\nfrom the records as they are now. The first successful read stamps the share as opened.\n\n* **404**: not a link (unknown, malformed, or one whose email or text never went). One shape\n  for all of them.\n* **410**: a link that was real and has ended. Body `{ \"state\": \"expired\" | \"revoked\",\n  \"shared_by\": \"<name>\" }` and nothing else; the page says \"This link has expired.\" / \"This\n  link has been revoked.\" and \"Ask {shared_by} to send a new one.\"\n* **429**: rate limited per IP and per token.\n\nNever cached or indexed (`Cache-Control: no-store`, `X-Robots-Tag: noindex`). Refused (403) to\nan api or MCP bearer token; the web page and anonymous callers pass.\n",
        "tags": [
          "Public",
          "Shared records"
        ],
        "security": [
          {},
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "description": "The token from the link (`/s/<token>`).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The register.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SharedRecordsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/SharedRecordsResponse"
                }
              }
            }
          },
          "403": {
            "description": "An api or MCP bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "404": {
            "description": "Not a link.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "410": {
            "description": "The link has expired or been revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SharedRecordsGoneResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/SharedRecordsGoneResponse"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/shares/{token}/file": {
      "get": {
        "operationId": "downloadSharedFile",
        "x-web-or-mobile-only": true,
        "summary": "Download a shared treatment register as PDF or CSV",
        "description": "Public (no account): the same rows as `GET /shares/{token}`, as the PDF or CSV the recipient\nsees offered on the page. Same 404 / 410 / 429 rules, and a successful download also stamps the\nshare as opened. Never cached.\n",
        "tags": [
          "Public",
          "Shared records"
        ],
        "security": [
          {},
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "description": "The token from the link.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "description": "`pdf` or `csv`. Defaults to the format the export was made in.",
            "schema": {
              "type": "string",
              "enum": [
                "pdf",
                "csv"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The file, as an attachment.",
            "content": {
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "description": "An unknown format."
          },
          "403": {
            "description": "An api or MCP bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "404": {
            "description": "Not a link.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "410": {
            "description": "The link has expired or been revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SharedRecordsGoneResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/SharedRecordsGoneResponse"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api-credentials": {
      "get": {
        "summary": "List Api Credentials",
        "operationId": "listApiCredentials",
        "x-web-or-mobile-only": true,
        "description": "Server-to-server API credentials (api-kind OAuth applications) owned by\nthe acting entity — pass owner_id/owner_type to manage an organization's\ncredentials (requires edit rights). Secrets are never returned here.\nDenied to api-channel tokens (keys can't mint keys) and to tokens\nwithout the write scope.\n",
        "tags": [
          "ApiCredentials"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "owner_id",
            "in": "query",
            "required": false,
            "description": "Owner id to scope the list to (defaults to the acting entity).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "owner_type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "User",
                "Organization"
              ]
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Credentials for the requested owner (no secrets).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiCredentialsIndexResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiCredentialsIndexResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          }
        }
      },
      "post": {
        "summary": "Create Api Credential",
        "operationId": "createApiCredential",
        "x-web-or-mobile-only": true,
        "description": "Create a server-to-server credential for the acting entity (or an\norganization the caller can edit). The response includes client_secret\nexactly once — secrets are hashed at rest and cannot be re-read; use\nrotate to obtain a new one. Two ways to hold a token: exchange the\nsecret at POST /oauth/token (grant_type=client_credentials, 15-day\ntokens), or mint a long-lived token in the UI with\nPOST /api-credentials/{id}/tokens (show-once token string, no secret\nneeded). Either way tokens act strictly for the owning entity at\nedit-level authority.\n",
        "tags": [
          "ApiCredentials"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "responses": {
          "201": {
            "description": "Created — includes client_secret (show-once).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiCredentialsShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiCredentialsShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "422": {
            "description": "Unprocessable Entity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ApiCredentialCreateModelAttributes"
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/ApiCredentialCreateModel"
              }
            }
          },
          "required": true,
          "description": "Create api credential attributes"
        }
      }
    },
    "/api-credentials/{id}": {
      "get": {
        "summary": "Get Api Credential",
        "operationId": "getApiCredential",
        "x-web-or-mobile-only": true,
        "description": "One credential by client_id (uid). Never returns the secret.",
        "tags": [
          "ApiCredentials"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The OAuth client_id (application uid).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The credential (no secret).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiCredentialsShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiCredentialsShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found"
          }
        }
      },
      "delete": {
        "summary": "Delete Api Credential",
        "operationId": "deleteApiCredential",
        "x-web-or-mobile-only": true,
        "description": "Delete the credential and revoke every access token it ever minted\n(immediate). Requires owner-level rights on the owning organization\n(or being the owning user).\n",
        "tags": [
          "ApiCredentials"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted — all its tokens are dead."
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found"
          }
        }
      }
    },
    "/api-credentials/{id}/rotate": {
      "post": {
        "summary": "Rotate Api Credential Secret",
        "operationId": "rotateApiCredential",
        "x-web-or-mobile-only": true,
        "description": "Regenerate the client_secret. The old secret stops authenticating at\n/oauth/token immediately; access tokens already minted survive until\nexpiry — including long-lived UI-minted ones (revoke those via\nDELETE /oauth-tokens/{id}, or delete the credential to kill everything).\nThe response includes the new client_secret exactly once.\n",
        "tags": [
          "ApiCredentials"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Rotated — includes the new client_secret (show-once).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiCredentialsShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiCredentialsShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found"
          }
        }
      }
    },
    "/api-credentials/{id}/tokens": {
      "post": {
        "summary": "Mint Api Credential Token",
        "operationId": "createApiCredentialToken",
        "x-web-or-mobile-only": true,
        "description": "Mint a machine token for this credential without touching the\nclient_secret (the alternative to the POST /oauth/token\nclient_credentials exchange, which stays at 15-day tokens). The token\nacts strictly for the owning entity with the credential's scopes, and\nthe token string appears exactly once, in this response. Lifetime is\nchosen at mint: expires_in_days 30, 90 or 365 (default), or 0 for\nnon-expiring. Non-expiring tokens live until revoked\n(DELETE /oauth-tokens/{id}) or the credential is deleted — expiry is\nnot the cleanup path. Rotating the secret does NOT revoke these tokens.\n",
        "tags": [
          "ApiCredentials"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The OAuth client_id (application uid).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Minted — the token string is shown here and never again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MintedOauthTokenResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/MintedOauthTokenResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request — expires_in_days off the menu.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found"
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ApiCredentialTokenCreateModelAttributes"
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/ApiCredentialTokenCreateModel"
              }
            }
          },
          "required": true,
          "description": "Mint token attributes (label + lifetime menu)."
        }
      }
    },
    "/properties/{property_id}/blocks": {
      "post": {
        "operationId": "createBlock",
        "summary": "Group paddocks into a block",
        "description": "Names a block on the property and, with `paddock_ids`, puts those live paddocks of the property in it (\"Group into a block\"); each member's `block_id` becomes the block's id. A block is a label for where paddocks sit: it has no page and no records of its own, and grouping writes, moves or removes no record. A paddock carries one block, so a paddock already in another block moves to this one. Reading the property then lists it under `blocks`, with its area and outline derived from the members.\n\nRequires edit rights on the property. `/farms/{farm_id}/blocks` is a deprecated alias for one release.\n",
        "tags": [
          "Blocks"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "property_id",
            "in": "path",
            "required": true,
            "description": "The property's id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BlockCreateModelAttributes"
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/BlockCreateModel"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new block.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BlockResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/BlockResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — no session.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the caller may not edit this property.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such property, or no live block with that id on it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "422": {
            "description": "A missing or duplicate name, an unknown kind, or a `paddock_ids` entry that is not a live paddock of this property. Nothing is grouped.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/properties/{property_id}/blocks/{id}": {
      "put": {
        "operationId": "updateBlock",
        "summary": "Rename a block or change its paddocks",
        "description": "Renames a block, changes its kind or number, or (with `paddock_ids`) replaces its members: the listed live paddocks of the property move into it (out of any other block), and a member left out goes back to the home block (`block_id` null). Omitting `paddock_ids` leaves the members alone. Writes, moves or removes no record. To move one paddock, `updatePaddock` with `block_id` also works. Requires edit rights on the property.\n",
        "tags": [
          "Blocks"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "property_id",
            "in": "path",
            "required": true,
            "description": "The property's id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The block's id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BlockUpdateModelAttributes"
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/BlockUpdateModel"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated block.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BlockResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/BlockResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — no session.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the caller may not edit this property.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such property, or no live block with that id on it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "422": {
            "description": "A duplicate name, an unknown kind, or a `paddock_ids` entry that is not a live paddock of this property.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteBlock",
        "summary": "Ungroup a block",
        "description": "Ungroup: archives the block and takes the label off every member paddock, so each one's `block_id` is null (the home block). The paddocks, their boundaries and every record stay exactly as they were. Requires edit rights on the property.\n",
        "tags": [
          "Blocks"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "property_id",
            "in": "path",
            "required": true,
            "description": "The property's id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The block's id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Ungrouped."
          },
          "401": {
            "description": "Unauthorized — no session.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the caller may not edit this property.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such property, or no live block with that id on it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/connection": {
      "get": {
        "summary": "Read this connection's access",
        "operationId": "getConnection",
        "description": "What this AI-assistant connection may do, read by the connection itself:\nthe token's grants, and under `meta.access` one row per business the\nperson belongs to with their role there (`permission`), the access today\n(`none`, `read` or `write`), the most that role can grant (`most_allowed`)\nand, when that is less than write, the `reason`; the last row is the\nperson's own account. Call it before offering to change access, so you\ncan say what is possible: an owner, admin or manager can allow read and\nwrite; a contractor or viewer stays read only, and only a role change by\nthe business lifts that.\n\nA connection starts with read and write wherever the person's role allows\nit (since 2026-10-09). Read-only access means the person narrowed it, or\nconnected before that date. There is no id: a token reads itself only. A\nsession or an API key is a 404 here.\n",
        "tags": [
          "Connection"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The connection, with its grants and the access overview in `meta`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConnectionResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ConnectionResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "The bearer is a session or an API key, not an AI-assistant connection."
          }
        }
      },
      "patch": {
        "summary": "Change this connection's access",
        "operationId": "updateConnectionAccess",
        "description": "Change what this AI-assistant connection may do, from the chat, after the\nperson has said yes. Send `grants`, one per business to change\n(`organization_id`, or null for the person's own account) with `access`\n`none`, `read` or `write`, and or `new_organization_access` for businesses\nthe person joins later (`none`, `read`, or `role`). Applies at once; the\nconnection stays live. The person is emailed what changed, with the link\nto undo it.\n\nUse it when a write was refused with code `insufficient_access` and the\nperson wants to allow it: offer to switch this connection to read and\nwrite in that business, and call this only once they agree in the chat.\nA grant can never exceed the person's own role in that business (owner,\nadmin and manager can allow writes; contractor and viewer only reads); a\n422 says which. Every gate a write passes applies here too: a connection\nwithout the `write` scope (one made before 2026-10-09, or a client that\nasked for less) is refused with code `insufficient_access`, and the person\nwidens it from the `manage_url` link instead. There is no id: a token\nchanges itself only. Only the grants named change; the others stand.\n",
        "tags": [
          "Connection"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OauthTokenUpdateAttributes"
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/OauthTokenUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The connection, with its grants as they now stand and the access overview in `meta`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConnectionResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ConnectionResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "The bearer is a session or an API key, not an AI-assistant connection; or a grant names a business that does not exist."
          },
          "422": {
            "description": "A grant exceeds the person's role, or names a business they are not in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/paddocks/{paddock_id}/fertiliser-records": {
      "post": {
        "operationId": "createFertiliserRecord",
        "summary": "Create Fertiliser Record",
        "description": "Record fertiliser spread on one paddock: urea, MAP, super, lime, gypsum or manure. One pass across a paddock is one record. Creates the one task that is the record, with its typed fertiliser child, and answers with that task exactly as `GET /tasks/{id}` would. The server sets the producer from the farm's owner, `operation_type` to `fertiliser`, `capture_source` to `agent` and `operator` from the signed-in user. `area_ha` defaults to the paddock's area. `date` defaults to today; a future date creates the open task instead of a completed record.\n\n**Fertiliser has no withholding period and never changes a paddock's status.** There are no dates on the record, and `getPaddock` and `getFarmStatus` ignore it. A line's `advisory_note` (urea: wait three to four weeks for nitrate risk) is advice to pass on to the farmer, not a status.\n\nSend one `products` entry per product, up to 10. Type the rate as the farmer said it in `rate` (\"100 kg/ha\"); it is kept verbatim and split into `rate_value`, `rate_unit` and `rate_basis`. **A rate with a number and no unit (\"100\") is refused (422, \"needs a unit, e.g. 100 kg/ha\"): ask the farmer for the unit.** `analysis` is nutrient percentages, e.g. `{ \"N\": 46 }`; `nutrients_applied` (kg/ha) and each line's `total_kg` are worked out when read. A completed record with no product row is refused. A fertiliser record belongs to a paddock; a mob id is a 404 here.\n\nBefore calling, check `listTasks` with this paddock's `asset_id` and `filter[status][]=completed` for a same-date duplicate, and confirm the summary with the farmer. The MCP tool is refused until it is called again with `confirm: true`. If the call errors or times out, read the paddock's records back before retrying: a 502 or 504 can arrive after the write succeeded. Do not send `broadcast` or `share_scope`. Needs a grant that allows writing in the paddock's business. A 403 whose `code` is `insufficient_access` is that refusal; its `detail` names the business and the access that is missing.\n**Photos.** A file on disk is staged with `createImageUpload`, whose `curl_example` PUTs the bytes, and attached with `image_upload_ids`. Never base64-encode a local photo into a tool call. A public http(s) URL: pass `image_urls`. Only a tiny generated image (under about 100 KB decoded) uses `image_base64s`. One upload can be attached to several records until it expires. On update, `image_mode` defaults to `append` (add to the existing photos); `replace` swaps them all out; `remove_image_ids` deletes specific ones. If any image fails, the whole create or update is rolled back. The attaching call still needs `confirm: true`. `createImageUpload` and its PUT do not.\n",
        "tags": [
          "Fertiliser records"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "paddock_id",
            "in": "path",
            "required": true,
            "description": "The paddock the fertiliser was done on.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Created. The task, with its `fertiliser`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated"
          },
          "403": {
            "description": "Not allowed to write on this paddock. For an assistant the body names the business, the level and where to change it (`insufficient_access`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such paddock."
          },
          "422": {
            "description": "Invalid request."
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FertiliserRecordCreateModelAttributes"
              },
              "example": {
                "data": {
                  "date": "2026-08-14",
                  "method": "broadcast",
                  "description": "Top-dress the barley with urea",
                  "products": [
                    {
                      "name": "Urea",
                      "analysis": {
                        "N": 46
                      },
                      "rate": "100 kg/ha",
                      "advisory_note": "Wait three to four weeks before grazing: nitrate risk."
                    }
                  ]
                }
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/FertiliserRecordCreateModel"
              },
              "example": {
                "data": {
                  "type": "fertiliser-record",
                  "attributes": {
                    "date": "2026-08-14",
                    "method": "broadcast",
                    "description": "Top-dress the barley with urea",
                    "products": [
                      {
                        "name": "Urea",
                        "analysis": {
                          "N": 46
                        },
                        "rate": "100 kg/ha",
                        "advisory_note": "Wait three to four weeks before grazing: nitrate risk."
                      }
                    ]
                  }
                }
              }
            }
          }
        }
      }
    },
    "/fertiliser-records/{id}": {
      "put": {
        "operationId": "updateFertiliserRecord",
        "summary": "Update Fertiliser Record",
        "description": "Change what a fertiliser carries after the fact: any of the attributes below, and `products`, which REPLACES the whole product set (position is the array index, at most 10). An attribute that is not sent is left alone. Answers with the task, exactly as `GET /tasks/{id}` would, with its `fertiliser`. Needs a grant that allows editing the task. Never invent a batch, expiry, rate or withholding period. The id is the `fertiliser.id` of the task, not the task's id.\n**Photos.** A file on disk is staged with `createImageUpload`, whose `curl_example` PUTs the bytes, and attached with `image_upload_ids`. Never base64-encode a local photo into a tool call. A public http(s) URL: pass `image_urls`. Only a tiny generated image (under about 100 KB decoded) uses `image_base64s`. One upload can be attached to several records until it expires. On update, `image_mode` defaults to `append` (add to the existing photos); `replace` swaps them all out; `remove_image_ids` deletes specific ones. If any image fails, the whole create or update is rolled back. The attaching call still needs `confirm: true`. `createImageUpload` and its PUT do not.\n",
        "tags": [
          "Fertiliser records"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The fertiliser record's id (`fertiliser.id` on the task).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The task, with its `fertiliser`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated"
          },
          "403": {
            "description": "Not allowed to edit this task.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such fertiliser record."
          },
          "422": {
            "description": "Invalid request."
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FertiliserRecordUpdateModelAttributes"
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/FertiliserRecordUpdateModel"
              }
            }
          }
        }
      }
    },
    "/paddocks/{paddock_id}/fieldwork-records": {
      "post": {
        "operationId": "createFieldworkRecord",
        "summary": "Create Field Work Record",
        "description": "Record work done on one paddock that is not an input: cultivation, harvest, hay or silage cutting, slashing or mowing, rolling, burning or irrigation. Creates the one task that is the record, with its typed field-work child, and answers with that task exactly as `GET /tasks/{id}` would. The server sets the producer from the farm's owner, `operation_type` to `fieldwork`, `capture_source` to `agent` and `operator` from the signed-in user. `area_ha` defaults to the paddock's area. `date` defaults to today; a future date creates the open task instead of a completed record.\n\n`kind` is `cultivate`, `harvest`, `hay_silage`, `slash`, `roll`, `burn` or `irrigate`. `outcomes` is one block per kind: `harvest` (`crop`, `tonnes`, `yield_t_ha`, `moisture_pct`, `protein_pct`, `grade`), `hay_silage` (`bales`, `bale_type`), `irrigate` (`ml`). Figures are numbers except `crop`, `bale_type` and `grade`; a figure the kind does not carry is refused. `products` is optional (a hay preservative).\n\nField work has no withholding of its own and never changes a paddock's status, but it is **checked**: the task's `warnings` say when a harvest is before a harvest withholding period ends, or hay or silage is cut before a grazing one ends (the fodder then carries that interval for a vendor declaration). A warning never blocks the write; read it from the response and tell the farmer. A field work record belongs to a paddock; a mob id is a 404 here.\n\nBefore calling, check `listTasks` with this paddock's `asset_id` and `filter[status][]=completed` for a same-date duplicate, and confirm the summary with the farmer. The MCP tool is refused until it is called again with `confirm: true`. If the call errors or times out, read the paddock's records back before retrying: a 502 or 504 can arrive after the write succeeded. Do not send `broadcast` or `share_scope`. Needs a grant that allows writing in the paddock's business. A 403 whose `code` is `insufficient_access` is that refusal; its `detail` names the business and the access that is missing.\n**Photos.** A file on disk is staged with `createImageUpload`, whose `curl_example` PUTs the bytes, and attached with `image_upload_ids`. Never base64-encode a local photo into a tool call. A public http(s) URL: pass `image_urls`. Only a tiny generated image (under about 100 KB decoded) uses `image_base64s`. One upload can be attached to several records until it expires. On update, `image_mode` defaults to `append` (add to the existing photos); `replace` swaps them all out; `remove_image_ids` deletes specific ones. If any image fails, the whole create or update is rolled back. The attaching call still needs `confirm: true`. `createImageUpload` and its PUT do not.\n",
        "tags": [
          "Field work records"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "paddock_id",
            "in": "path",
            "required": true,
            "description": "The paddock the field work was done on.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Created. The task, with its `fieldwork`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated"
          },
          "403": {
            "description": "Not allowed to write on this paddock. For an assistant the body names the business, the level and where to change it (`insufficient_access`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such paddock."
          },
          "422": {
            "description": "Invalid request."
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FieldworkRecordCreateModelAttributes"
              },
              "example": {
                "data": {
                  "date": "2025-12-02",
                  "kind": "harvest",
                  "implement": "Case 8250",
                  "outcomes": {
                    "harvest": {
                      "crop": "Barley",
                      "tonnes": 399,
                      "yield_t_ha": 4.2,
                      "moisture_pct": 11.8,
                      "protein_pct": 10.1,
                      "grade": "Malt 1"
                    }
                  }
                }
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/FieldworkRecordCreateModel"
              },
              "example": {
                "data": {
                  "type": "fieldwork-record",
                  "attributes": {
                    "date": "2025-12-02",
                    "kind": "harvest",
                    "implement": "Case 8250",
                    "outcomes": {
                      "harvest": {
                        "crop": "Barley",
                        "tonnes": 399,
                        "yield_t_ha": 4.2,
                        "moisture_pct": 11.8,
                        "protein_pct": 10.1,
                        "grade": "Malt 1"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/fieldwork-records/{id}": {
      "put": {
        "operationId": "updateFieldworkRecord",
        "summary": "Update Field Work Record",
        "description": "Change what a field work carries after the fact: any of the attributes below, and `products`, which REPLACES the whole product set (position is the array index, at most 10). An attribute that is not sent is left alone. Answers with the task, exactly as `GET /tasks/{id}` would, with its `fieldwork`. Needs a grant that allows editing the task. Never invent a batch, expiry, rate or withholding period. The id is the `fieldwork.id` of the task, not the task's id. Read `warnings` on the response: a harvest or a hay cut before a withholding period ends is flagged there.\n**Photos.** A file on disk is staged with `createImageUpload`, whose `curl_example` PUTs the bytes, and attached with `image_upload_ids`. Never base64-encode a local photo into a tool call. A public http(s) URL: pass `image_urls`. Only a tiny generated image (under about 100 KB decoded) uses `image_base64s`. One upload can be attached to several records until it expires. On update, `image_mode` defaults to `append` (add to the existing photos); `replace` swaps them all out; `remove_image_ids` deletes specific ones. If any image fails, the whole create or update is rolled back. The attaching call still needs `confirm: true`. `createImageUpload` and its PUT do not.\n",
        "tags": [
          "Field work records"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The field work record's id (`fieldwork.id` on the task).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The task, with its `fieldwork`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated"
          },
          "403": {
            "description": "Not allowed to edit this task.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such field work record."
          },
          "422": {
            "description": "Invalid request."
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FieldworkRecordUpdateModelAttributes"
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/FieldworkRecordUpdateModel"
              }
            }
          }
        }
      }
    },
    "/mobs/{mob_id}/husbandry-records": {
      "post": {
        "operationId": "createHusbandryRecord",
        "summary": "Create Husbandry Record",
        "description": "Record something done TO one mob: marking, mulesing, shearing, crutching, eID tagging, weaning, pregnancy scanning, classing, weighing, dehorning or spaying. Marking day is ONE record: procedures `mark`, `tail_dock` and `castrate` on 412 lambs, with product lines for the vaccine and the pain relief. Creates the one task that is the record, with its typed husbandry child, and answers with that task exactly as `GET /tasks/{id}` would. The server sets the producer from the farm's owner, `operation_type` to `husbandry`, `capture_source` to `agent` and `operator` from the signed-in user. `head` defaults to the mob's head count. `date` defaults to today; a future date creates the open task instead of a completed record.\n\n`procedures` is a list of `mark`, `eid_tag`, `tail_dock`, `castrate`, `mules`, `shear`, `crutch`, `wean`, `scan`, `class`, `weigh`, `dehorn`, `spay`, `other`. `outcomes` is a small block per procedure: `mark` (`lambs_marked`, `ewes_joined`), `scan` (`singles`, `twins`, `triplets`, `dry`), `shear` (`shearer` as text, `bales`, `wool_kg`), `wean` (`weaned`, `avg_kg`), `weigh` (`avg_kg`, `lightest_kg`, `heaviest_kg`). A figure the procedure does not carry is refused. `marking_percent` is worked out when read, never stored.\n\n`products` is optional: pain relief, a vaccine at marking, jetting fluid. The same line as a treatment's, and it feeds the mob's sale status exactly like one, so a product with a withholding period matters. Never invent a batch, expiry or withholding period. A husbandry record belongs to a mob; a paddock id is a 404 here.\n\nBefore calling, check `listTasks` with this mob's `asset_id` and `filter[status][]=completed` for a same-date duplicate, and confirm the summary with the farmer. The MCP tool is refused until it is called again with `confirm: true`. If the call errors or times out, read the mob's records back before retrying: a 502 or 504 can arrive after the write succeeded. Do not send `broadcast` or `share_scope`. Needs a grant that allows writing in the mob's business. A 403 whose `code` is `insufficient_access` is that refusal; its `detail` names the business and the access that is missing.\n**Photos.** A file on disk is staged with `createImageUpload`, whose `curl_example` PUTs the bytes, and attached with `image_upload_ids`. Never base64-encode a local photo into a tool call. A public http(s) URL: pass `image_urls`. Only a tiny generated image (under about 100 KB decoded) uses `image_base64s`. One upload can be attached to several records until it expires. On update, `image_mode` defaults to `append` (add to the existing photos); `replace` swaps them all out; `remove_image_ids` deletes specific ones. If any image fails, the whole create or update is rolled back. The attaching call still needs `confirm: true`. `createImageUpload` and its PUT do not.\n",
        "tags": [
          "Husbandry records"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "mob_id",
            "in": "path",
            "required": true,
            "description": "The mob the husbandry was done on.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Created. The task, with its `husbandry`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated"
          },
          "403": {
            "description": "Not allowed to write on this mob. For an assistant the body names the business, the level and where to change it (`insufficient_access`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such mob."
          },
          "422": {
            "description": "Invalid request."
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/HusbandryRecordCreateModelAttributes"
              },
              "example": {
                "data": {
                  "date": "2025-09-12",
                  "procedures": [
                    "mark",
                    "tail_dock",
                    "castrate"
                  ],
                  "head": 412,
                  "outcomes": {
                    "mark": {
                      "lambs_marked": 412,
                      "ewes_joined": 350
                    }
                  },
                  "products": [
                    {
                      "name": "6-in-1",
                      "dose": "1 mL SC"
                    },
                    {
                      "name": "Pain relief gel",
                      "whp_days": 10
                    }
                  ]
                }
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/HusbandryRecordCreateModel"
              },
              "example": {
                "data": {
                  "type": "husbandry-record",
                  "attributes": {
                    "date": "2025-09-12",
                    "procedures": [
                      "mark",
                      "tail_dock",
                      "castrate"
                    ],
                    "head": 412,
                    "outcomes": {
                      "mark": {
                        "lambs_marked": 412,
                        "ewes_joined": 350
                      }
                    },
                    "products": [
                      {
                        "name": "6-in-1",
                        "dose": "1 mL SC"
                      },
                      {
                        "name": "Pain relief gel",
                        "whp_days": 10
                      }
                    ]
                  }
                }
              }
            }
          }
        }
      }
    },
    "/husbandry-records/{id}": {
      "put": {
        "operationId": "updateHusbandryRecord",
        "summary": "Update Husbandry Record",
        "description": "Change what a husbandry carries after the fact: any of the attributes below, and `products`, which REPLACES the whole product set (position is the array index, at most 10). An attribute that is not sent is left alone. Answers with the task, exactly as `GET /tasks/{id}` would, with its `husbandry`. Needs a grant that allows editing the task. Never invent a batch, expiry, rate or withholding period. The id is the `husbandry.id` of the task, not the task's id.\n**Photos.** A file on disk is staged with `createImageUpload`, whose `curl_example` PUTs the bytes, and attached with `image_upload_ids`. Never base64-encode a local photo into a tool call. A public http(s) URL: pass `image_urls`. Only a tiny generated image (under about 100 KB decoded) uses `image_base64s`. One upload can be attached to several records until it expires. On update, `image_mode` defaults to `append` (add to the existing photos); `replace` swaps them all out; `remove_image_ids` deletes specific ones. If any image fails, the whole create or update is rolled back. The attaching call still needs `confirm: true`. `createImageUpload` and its PUT do not.\n",
        "tags": [
          "Husbandry records"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The husbandry record's id (`husbandry.id` on the task).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The task, with its `husbandry`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated"
          },
          "403": {
            "description": "Not allowed to edit this task.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such husbandry record."
          },
          "422": {
            "description": "Invalid request."
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/HusbandryRecordUpdateModelAttributes"
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/HusbandryRecordUpdateModel"
              }
            }
          }
        }
      }
    },
    "/join/{join_code}": {
      "get": {
        "operationId": "getJoinLink",
        "x-mcp-excluded": true,
        "summary": "Preview a business join link",
        "description": "Public (unauthenticated) preview of a business's shareable join link —\nthe unguessable code in the URL is the credential. Returns the business,\nwho shared the link and the role the visitor will land in. A code that is\nunknown, switched off, reset or expired all return the **same** 404, so a\nstranger cannot tell which. A client that keeps presenting codes that do\nnot resolve is answered with 404 for everything for the rest of the hour.\n",
        "tags": [
          "Public",
          "Join links"
        ],
        "parameters": [
          {
            "name": "join_code",
            "in": "path",
            "required": true,
            "description": "The code from the join link.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The join link preview.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JoinLinkPreviewResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/JoinLinkPreviewResponse"
                }
              }
            }
          },
          "404": {
            "description": "The link cannot be used (unknown, switched off, reset or expired).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/join/{join_code}/verify": {
      "post": {
        "operationId": "verifyJoinLink",
        "x-web-or-mobile-only": true,
        "summary": "Text a verification code to the visitor's mobile",
        "description": "Sends a self-issued one-time code (purpose `invite`) to the Australian\nmobile the visitor typed. Rate-limited per number (60 s cooldown), per IP\nand per link; always returns **204** for a usable link and a valid mobile,\nthrottled or not, so the response leaks nothing about send state or about\nwhether the number has an account.\n",
        "tags": [
          "Public",
          "Join links"
        ],
        "parameters": [
          {
            "name": "join_code",
            "in": "path",
            "required": true,
            "description": "The code from the join link.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "mobile": {
                    "type": "string",
                    "description": "An Australian mobile in any common format (`0412 345 678`, `+61 412 345 678`, `412345678`)."
                  }
                },
                "required": [
                  "mobile"
                ]
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Accepted (a code was sent if rate limits allow)."
          },
          "403": {
            "description": "Forbidden — first-party only; refused to an API key or an assistant's token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "404": {
            "description": "The link cannot be used.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "422": {
            "description": "The mobile is not a valid Australian mobile number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/join/{join_code}/accept": {
      "post": {
        "operationId": "acceptJoinLink",
        "x-web-or-mobile-only": true,
        "summary": "Join the business (verify the code)",
        "description": "Checks the one-time code against the mobile. On success the account is\n**created if new** (unusable random password, default grower — exactly like\nan SMS invite or phone sign-in), the mobile is marked verified, and the\nperson becomes a member at the link's role. An existing account on that\nnumber is signed in and added straight away; someone who is already a\nmember is left exactly as they were (a link never changes a role), and a\npending invitation they held is accepted. Returns the standard\nfirst-party Doorkeeper token payload (same shape as\n`POST /users/phone-sessions/verify`).\n",
        "tags": [
          "Public",
          "Join links"
        ],
        "parameters": [
          {
            "name": "join_code",
            "in": "path",
            "required": true,
            "description": "The code from the join link.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "mobile": {
                    "type": "string",
                    "description": "The same mobile the code was sent to."
                  },
                  "code": {
                    "type": "string",
                    "description": "The 6-digit code from the SMS."
                  }
                },
                "required": [
                  "mobile",
                  "code"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Joined — Doorkeeper access token; includes `user_id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DoorkeeperAccessTokenResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/DoorkeeperAccessTokenResponse"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — first-party only; refused to an API key or an assistant's token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "404": {
            "description": "The link cannot be used.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "422": {
            "description": "Incorrect or expired code, or an invalid mobile.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/oauth-tokens": {
      "get": {
        "summary": "List Oauth Tokens",
        "operationId": "listOauthTokens",
        "x-web-or-mobile-only": true,
        "description": "Active access tokens as metadata (never the token strings): your\nweb/mobile sessions, MCP (AI-agent) connections, and tokens minted by\napi credentials you own. Pass owner_type=Organization&owner_id to list\nan organization's api-key tokens instead (requires edit rights;\norganizations can only own api-kind applications, so members' personal\nsessions never appear there). Filter with kind and application_id.\n",
        "tags": [
          "OauthTokens"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "web_or_mobile",
                "api",
                "mcp"
              ]
            }
          },
          {
            "name": "application_id",
            "in": "query",
            "required": false,
            "description": "Filter to one application by its client_id (uid).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "owner_type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "Organization"
              ]
            }
          },
          {
            "name": "owner_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Active tokens, newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OauthTokensIndexResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/OauthTokensIndexResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          }
        }
      }
    },
    "/oauth-tokens/{id}": {
      "delete": {
        "summary": "Revoke Oauth Token",
        "operationId": "revokeOauthToken",
        "x-web-or-mobile-only": true,
        "description": "Revoke one token: it stops authenticating immediately and its refresh\ntoken dies with it. Sign out a device, disconnect an AI agent, or kill\na token an api credential minted.\n",
        "tags": [
          "OauthTokens"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Revoked."
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found"
          }
        },
        "x-codegen-request-body-name": "body"
      },
      "get": {
        "summary": "Get Oauth Token",
        "operationId": "getOauthToken",
        "x-web-or-mobile-only": true,
        "description": "One of your tokens as metadata — for an AI-assistant (MCP) connection,\nwith its per-business grants and its default for businesses you join\nlater (the Manage connection view). Only the token's own person may;\nan organization's api-key tokens are listed, never shown one by one.\n",
        "tags": [
          "OauthTokens"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OauthTokenResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/OauthTokenResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found"
          }
        }
      },
      "patch": {
        "summary": "Update Oauth Token",
        "operationId": "updateOauthToken",
        "x-web-or-mobile-only": true,
        "description": "Change what an AI-assistant (MCP) connection may do (Manage\nconnection): `grants`, one per business — `organization_id` null is\nyour own account — each `none`, `read` or `write`, and\n`new_organization_access`, its default in a business you join later\n(`none`, `read`, or `role`: read and write where your role allows it,\nread only elsewhere). Applies at once; the connection stays live.\n\nA grant can never exceed your own role in that business (owner, admin\nand manager can grant writes; contractor and viewer only reads); a 422 says\nwhich. A self-registered assistant is issued `public read write` and its\ngrants are seeded at consent to match your role (read and write where you\nare an owner, admin or manager, read only elsewhere, read and write on your\nown account); what the grants say is what it can do, from its next request.\nA connection consented without `write` (made before 2026-10-09, or a client\nthat asked for less) gains the scope with its first `write` grant and loses\nit with the last. Only the\nconnection's own person may call this, and only for an `mcp` token.\nA connection made before grants existed gets its first grants seeded\nfrom your roles on the first update.\n",
        "tags": [
          "OauthTokens"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OauthTokenUpdateAttributes"
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/OauthTokenUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The token, with its grants as they now stand.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OauthTokenResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/OauthTokenResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found"
          },
          "422": {
            "description": "A grant exceeds your role, or names a business you are not in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "x-codegen-request-body-name": "body"
      }
    },
    "/organizations/{organization_id}/join-link": {
      "get": {
        "operationId": "getOrganizationJoinLink",
        "summary": "Get the business's join link",
        "description": "Read the business's current join link. Anyone holding that link can join\nthe business in the link's role, after they confirm a mobile number on\nPurpleGrain's own page. This is the read to use before\n`updateOrganizationJoinLink` or `resetOrganizationJoinLink`.\n\nReturns the newest link that has not been reset, whether it is on, off\nor lapsed, so the card can still show how many people joined with it.\n404 when the business has never had one. Only an **owner** may call this.\nAPI keys are refused.\n",
        "tags": [
          "Organization Join Links"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "organization_id",
            "in": "path",
            "required": true,
            "description": "Id of the organization.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The current join link.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationJoinLinkShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationJoinLinkShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated"
          },
          "403": {
            "description": "Forbidden — caller is not an owner of the organization."
          },
          "404": {
            "description": "The business has no join link, or does not exist.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      },
      "put": {
        "operationId": "updateOrganizationJoinLink",
        "summary": "Turn the join link on or off, and set its role and expiry",
        "description": "Turns the business's join link on or off, and sets the role a holder\njoins with and when the link expires. Anyone holding the link can join\nthe business in that role, after they confirm a mobile number on\nPurpleGrain's own page.\n\n`enabled: true` turns it on. Enabling mints a new link with a fresh code\nwhen the business has no link, or the current link is off. Enabling a\nlink that is already on keeps the code. `enabled: false` turns the live\nlink off and keeps the row. `role` and `expiry` change the live link in\nplace: the code stays the same, and a new `expiry` restarts its clock\nfrom now. Switching off a business that has no link is a 404. Only an\n**owner** may call this. API keys are refused. Read the current link\nwith `getOrganizationJoinLink`.\n",
        "tags": [
          "Organization Join Links"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "organization_id",
            "in": "path",
            "required": true,
            "description": "Id of the organization.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OrganizationJoinLinkUpdateModelAttributes"
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/OrganizationJoinLinkUpdateModel"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The join link after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationJoinLinkShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationJoinLinkShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated"
          },
          "403": {
            "description": "Forbidden — caller is not an owner of the organization."
          },
          "404": {
            "description": "Switching off a business that has no join link, or the business does not exist.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "422": {
            "description": "An unknown role or expiry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/organizations/{organization_id}/join-link/reset": {
      "post": {
        "operationId": "resetOrganizationJoinLink",
        "summary": "Reset the join link",
        "description": "Revokes the old join URL immediately and issues a new code. The new link\nkeeps the same role and expiry, the clock restarts, and the join count\ngoes back to zero. People who already joined keep their access. Only an\n**owner** may call this. API keys are refused. Read the new link with\n`getOrganizationJoinLink`.\n",
        "tags": [
          "Organization Join Links"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "organization_id",
            "in": "path",
            "required": true,
            "description": "Id of the organization.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "The new join link.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationJoinLinkShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationJoinLinkShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated"
          },
          "403": {
            "description": "Forbidden — caller is not an owner of the organization."
          },
          "404": {
            "description": "The organization does not exist.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/organizations/{owner}/properties/{property}": {
      "get": {
        "operationId": "getOrganizationProperty",
        "summary": "Look up a business's property by slug",
        "description": "Look up one property of a business, by the business slug and the property slug, in one call:\nthe visitor view of the place, its mapped paddocks, its mobs, its tasks and its records.\nWorks with no credentials when the property is public or shared by link (pass `share`).\nA stranger with no credentials gets **404**. A signed-in caller who may not see it gets **403**.\n\nThe address is the web address with the API prefix, and the web address's `.json` form is\nthis call with the `include` below. `resolveLink` is the step that turns a pasted link\ninto these slugs.\n\n* Visibility is the property's own, as for `resolveLink`: members read it, anyone reads a\n  public property, and a link-shared one needs its `share` token. A caller with no credentials\n  who may not read it gets a **404**, the same as for one that does not exist; a signed-in caller\n  who may not gets a **403**.\n* Only paddocks **with a boundary** are listed, and only rows filed on a listed paddock or mob\n  (or on the property itself) are returned: nothing from archived or unmapped assets, nothing\n  from another property.\n* It follows the shared-link privacy rules: the place is the town and state, never the street\n  or postcode, and nothing says who a task was shared with. The treatment register is never in it.\n* Cacheable for a minute for callers with no credentials.\n",
        "tags": [
          "Public",
          "Public properties"
        ],
        "security": [
          {},
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "owner",
            "in": "path",
            "required": true,
            "description": "The business's slug.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "property",
            "in": "path",
            "required": true,
            "description": "The property's slug. A slug it used to have still finds it.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "include",
            "in": "query",
            "required": false,
            "description": "Comma-separated sections to return with the property: `paddocks`, `mobs`, `tasks`, `records`. The property alone when absent. Unknown names are ignored.",
            "schema": {
              "type": "string",
              "example": "paddocks,mobs,tasks,records"
            }
          },
          {
            "name": "share",
            "in": "query",
            "required": false,
            "description": "The property's share token, when it is shared by link.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The property, with the requested sections.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicPropertyResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicPropertyResponse"
                }
              }
            }
          },
          "403": {
            "description": "Signed in, but not allowed to see the property.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not visible to a caller with no credentials.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/users/{owner}/properties/{property}": {
      "get": {
        "operationId": "getUserProperty",
        "summary": "Look up a person's property by slug",
        "description": "Look up one property of a person, by their handle and the property slug, in one call:\nthe visitor view of the place, its mapped paddocks, its mobs, its tasks and its records.\nWorks with no credentials when the property is public or shared by link (pass `share`).\nA stranger with no credentials gets **404**. A signed-in caller who may not see it gets **403**.\n\nThe address is the web address with the API prefix, and the web address's `.json` form is\nthis call with the `include` below. `resolveLink` is the step that turns a pasted link\ninto these slugs.\n\n* Visibility is the property's own, as for `resolveLink`: members read it, anyone reads a\n  public property, and a link-shared one needs its `share` token. A caller with no credentials\n  who may not read it gets a **404**, the same as for one that does not exist; a signed-in caller\n  who may not gets a **403**.\n* Only paddocks **with a boundary** are listed, and only rows filed on a listed paddock or mob\n  (or on the property itself) are returned: nothing from archived or unmapped assets, nothing\n  from another property.\n* It follows the shared-link privacy rules: the place is the town and state, never the street\n  or postcode, and nothing says who a task was shared with. The treatment register is never in it.\n* Cacheable for a minute for callers with no credentials.\n",
        "tags": [
          "Public",
          "Public properties"
        ],
        "security": [
          {},
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "owner",
            "in": "path",
            "required": true,
            "description": "The person's handle.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "property",
            "in": "path",
            "required": true,
            "description": "The property's slug. A slug it used to have still finds it.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "include",
            "in": "query",
            "required": false,
            "description": "Comma-separated sections to return with the property: `paddocks`, `mobs`, `tasks`, `records`. The property alone when absent. Unknown names are ignored.",
            "schema": {
              "type": "string",
              "example": "paddocks,mobs,tasks,records"
            }
          },
          {
            "name": "share",
            "in": "query",
            "required": false,
            "description": "The property's share token, when it is shared by link.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The property, with the requested sections.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicPropertyResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicPropertyResponse"
                }
              }
            }
          },
          "403": {
            "description": "Signed in, but not allowed to see the property.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not visible to a caller with no credentials.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/resolve": {
      "get": {
        "operationId": "resolveLink",
        "summary": "Resolve a PurpleGrain link",
        "description": "The first step when someone pastes a PurpleGrain web link. Turns its slugs into\nthe business or person, property, paddock or mob it names, and says what the\ncaller may do there.\n\n`/organizations/hillcrest/properties/yarrabee/paddocks/north-block?share=k9f2qa` is\n`kind=organization&owner=hillcrest&property=yarrabee&paddock=north-block&share=k9f2qa`.\n`kind` `organization` is a business and `kind` `user` is a person. Send those\nwire values as they are. `resolved.owner.kind` comes back the same way.\n\n* `access.can_edit` comes from the session's role in the owner, never from the address.\n* A slug a property, paddock or mob used to have resolves to it with `redirect: true`\n  and the current `canonical_path`, so a link shared before a rename still arrives.\n* Public (credentials optional). A caller with no credentials who may not see the\n  property gets a **404**, the same as for one that does not exist, so a stranger\n  cannot confirm that a private property is there. A signed-in caller who may not\n  gets a **403** (`You don't have access…`).\n* `resolved.preview` carries the facts behind a link preview (an SMS or chat card, a\n  search snippet, the Share panel): the owner's name and, for the deepest thing the\n  address names, its counts, area and (a mob) withholding. It is built after the\n  access check, so a viewer and a link holder read the same facts; a stranger who\n  gets the 404 never sees any.\n",
        "tags": [
          "Public",
          "Resolve"
        ],
        "security": [
          {},
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "kind",
            "in": "query",
            "required": true,
            "description": "Whose link it is. `organization` is a business; `user` is a person.",
            "schema": {
              "type": "string",
              "enum": [
                "organization",
                "user"
              ]
            }
          },
          {
            "name": "owner",
            "in": "query",
            "required": true,
            "description": "The business slug or the person's handle.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "property",
            "in": "query",
            "required": false,
            "description": "The property slug under `owner`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "paddock",
            "in": "query",
            "required": false,
            "description": "A paddock slug under `property`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "mob",
            "in": "query",
            "required": false,
            "description": "A mob slug under `property`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "share",
            "in": "query",
            "required": false,
            "description": "The property's share token, when it is shared by link.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "What the address names and what the caller may do there.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResolveResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ResolveResponse"
                }
              }
            }
          },
          "403": {
            "description": "Signed in, but not allowed to see the property.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not visible to a caller with no credentials.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/property_setup_draft": {
      "get": {
        "operationId": "getPropertySetupDraft",
        "x-web-or-mobile-only": true,
        "summary": "Get my property setup draft",
        "description": "The caller's unsaved property for one owner — the name, place, corners\nand checklist row the map editor last wrote — or `data: null` when\nthere is none. One draft per person per owner; only the person who\nwrote it can read it. The owner must be the caller or a business where\nthe caller may add properties (the `createFarm` rule).\n",
        "tags": [
          "PropertySetupDrafts"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "owner_type",
            "in": "query",
            "required": true,
            "description": "Whose property the draft is for.",
            "schema": {
              "type": "string",
              "enum": [
                "User",
                "Organization"
              ]
            }
          },
          {
            "name": "owner_id",
            "in": "query",
            "required": true,
            "description": "The person's own id, or the business's.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The draft, or `data: null`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PropertySetupDraftResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/PropertySetupDraftResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — no session.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the owner is someone else, or a business where the caller may not add properties; or an API key or an assistant's token (first-party only).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such person or business.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "422": {
            "description": "`owner_type` is not User or Organization, or `owner_id` is not a uuid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      },
      "put": {
        "operationId": "updatePropertySetupDraft",
        "x-web-or-mobile-only": true,
        "summary": "Save my property setup draft",
        "description": "Creates or replaces the caller's draft for the owner named in the body.\nOnly the keys sent change; `null` clears one. The map editor writes on\nevery change (debounced), so this is an upsert keyed by the caller and\nthe owner. `ring` is an open array of up to 2000 `[lng, lat]` positions;\n`location` is `{ label, lng?, lat? }`; `step` is the checklist row.\nAnything else is refused with 422 on the field.\n",
        "tags": [
          "PropertySetupDrafts"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [],
        "responses": {
          "200": {
            "description": "The saved draft.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PropertySetupDraftResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/PropertySetupDraftResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — no session.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the owner is someone else, or a business where the caller may not add properties; or an API key or an assistant's token (first-party only).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such person or business.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "422": {
            "description": "A ring that is not `[lng, lat]` number pairs in range or holds more than 2000, a location without a label or with half a coordinate pair or other keys, an unknown `step`, a name over 255 characters, or an unknown owner type.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PropertySetupDraftUpdateModelAttributes"
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/PropertySetupDraftUpdateModel"
              }
            }
          },
          "required": true,
          "description": "The owner and the draft fields to write"
        }
      },
      "delete": {
        "operationId": "deletePropertySetupDraft",
        "x-web-or-mobile-only": true,
        "summary": "Delete my property setup draft",
        "description": "Removes the caller's draft for the owner (on save, or Start over). 204 whether or not there was one.",
        "tags": [
          "PropertySetupDrafts"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "owner_type",
            "in": "query",
            "required": true,
            "description": "Whose property the draft is for.",
            "schema": {
              "type": "string",
              "enum": [
                "User",
                "Organization"
              ]
            }
          },
          {
            "name": "owner_id",
            "in": "query",
            "required": true,
            "description": "The person's own id, or the business's.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted, or there was nothing to delete."
          },
          "401": {
            "description": "Unauthorized — no session.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the owner is someone else, or a business where the caller may not add properties; or an API key or an assistant's token (first-party only).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such person or business.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "422": {
            "description": "`owner_type` is not User or Organization, or `owner_id` is not a uuid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/farms/{farm_id}/boundary_draft": {
      "get": {
        "operationId": "getFarmBoundaryDraft",
        "x-web-or-mobile-only": true,
        "summary": "Get my boundary draft for a property",
        "description": "The caller's unsaved boundary on this property — the corners, and the place set before drawing, the map\neditor last wrote — or `data: null` when there is none. One per person\nper property, readable only by the person who wrote it, and only while\nthey may save the boundary (PUT /farms/{id}/boundary).\n",
        "tags": [
          "BoundaryDrafts"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "farm_id",
            "in": "path",
            "required": true,
            "description": "Id of the property",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The draft, or `data: null`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BoundaryDraftResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/BoundaryDraftResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — no session.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the caller may not edit this property's boundary; or an API key or an assistant's token (first-party only).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such property.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      },
      "put": {
        "operationId": "updateFarmBoundaryDraft",
        "x-web-or-mobile-only": true,
        "summary": "Save my boundary draft for a property",
        "description": "Creates or replaces the caller's draft for this property. Only the keys\nsent change; `null` clears one. A successful `PUT /farms/{id}/boundary` by the same\nperson deletes it.\n",
        "tags": [
          "BoundaryDrafts"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "farm_id",
            "in": "path",
            "required": true,
            "description": "Id of the property",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The saved draft.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BoundaryDraftResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/BoundaryDraftResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — no session.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the caller may not edit this property's boundary; or an API key or an assistant's token (first-party only).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such property.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "422": {
            "description": "A ring that is not `[lng, lat]` number pairs in range or holds more than 2000, or a location without a label, with half a coordinate pair or with other keys.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BoundaryDraftUpdateModelAttributes"
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/BoundaryDraftUpdateModel"
              }
            }
          },
          "required": true,
          "description": "The draft fields to write"
        }
      },
      "delete": {
        "operationId": "deleteFarmBoundaryDraft",
        "x-web-or-mobile-only": true,
        "summary": "Delete my boundary draft for a property",
        "description": "Removes the caller's draft for this property (Start over). 204 whether or not there was one.",
        "tags": [
          "BoundaryDrafts"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "farm_id",
            "in": "path",
            "required": true,
            "description": "Id of the property",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted, or there was nothing to delete."
          },
          "401": {
            "description": "Unauthorized — no session.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the caller may not edit this property's boundary; or an API key or an assistant's token (first-party only).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such property.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/paddocks/{paddock_id}/boundary_draft": {
      "get": {
        "operationId": "getPaddockBoundaryDraft",
        "x-web-or-mobile-only": true,
        "summary": "Get my boundary draft for a paddock",
        "description": "The caller's unsaved boundary on this paddock — the corners the map\neditor last wrote — or `data: null` when there is none. One per person\nper paddock, readable only by the person who wrote it, and only while\nthey may save the boundary (PUT /paddocks/{id}/boundary).\n",
        "tags": [
          "BoundaryDrafts"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "paddock_id",
            "in": "path",
            "required": true,
            "description": "Id of the paddock",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The draft, or `data: null`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BoundaryDraftResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/BoundaryDraftResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — no session.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the caller may not edit this paddock's boundary; or an API key or an assistant's token (first-party only).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such paddock.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      },
      "put": {
        "operationId": "updatePaddockBoundaryDraft",
        "x-web-or-mobile-only": true,
        "summary": "Save my boundary draft for a paddock",
        "description": "Creates or replaces the caller's draft for this paddock. Only the keys\nsent change; `null` clears one. A successful `PUT /paddocks/{id}/boundary` by the same\nperson deletes it.\n",
        "tags": [
          "BoundaryDrafts"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "paddock_id",
            "in": "path",
            "required": true,
            "description": "Id of the paddock",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The saved draft.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BoundaryDraftResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/BoundaryDraftResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — no session.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the caller may not edit this paddock's boundary; or an API key or an assistant's token (first-party only).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such paddock.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "422": {
            "description": "A ring that is not `[lng, lat]` number pairs in range or holds more than 2000, or a location without a label, with half a coordinate pair or with other keys.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BoundaryDraftUpdateModelAttributes"
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/BoundaryDraftUpdateModel"
              }
            }
          },
          "required": true,
          "description": "The draft fields to write"
        }
      },
      "delete": {
        "operationId": "deletePaddockBoundaryDraft",
        "x-web-or-mobile-only": true,
        "summary": "Delete my boundary draft for a paddock",
        "description": "Removes the caller's draft for this paddock (Start over). 204 whether or not there was one.",
        "tags": [
          "BoundaryDrafts"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "paddock_id",
            "in": "path",
            "required": true,
            "description": "Id of the paddock",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted, or there was nothing to delete."
          },
          "401": {
            "description": "Unauthorized — no session.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — the caller may not edit this paddock's boundary; or an API key or an assistant's token (first-party only).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such paddock.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/sitemap/services": {
      "get": {
        "operationId": "getServicesSitemap",
        "summary": "Sitemap — Services",
        "description": "Returns a cursor-paginated list of publicly indexable services (active only).\nIntended for sitemap generation; returns minimal data. No authentication required.\n",
        "tags": [
          "Sitemap"
        ],
        "parameters": [
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Opaque pagination cursor from the previous page's `next_cursor` field.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of items to return (default 5000, max 10000).",
            "schema": {
              "type": "integer",
              "default": 5000,
              "maximum": 10000
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of indexable services.",
            "headers": {
              "Cache-Control": {
                "description": "Public cache directive (e.g. `public, max-age=3600`).",
                "schema": {
                  "type": "string"
                }
              },
              "ETag": {
                "description": "Entity tag for conditional requests.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SitemapServicesResponse"
                }
              }
            }
          }
        }
      }
    },
    "/sitemap/users": {
      "get": {
        "operationId": "getUsersSitemap",
        "summary": "Sitemap — Users",
        "description": "Returns a cursor-paginated list of publicly indexable user profiles. Every user profile is public, so this includes every contractor that is a User.\nNo authentication required.\n",
        "tags": [
          "Sitemap"
        ],
        "parameters": [
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Opaque pagination cursor from the previous page's `next_cursor` field.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of items to return (default 5000, max 10000).",
            "schema": {
              "type": "integer",
              "default": 5000,
              "maximum": 10000
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of indexable users.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                }
              },
              "ETag": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SitemapUsersResponse"
                }
              }
            }
          }
        }
      }
    },
    "/sitemap/organizations": {
      "get": {
        "operationId": "getOrganizationsSitemap",
        "summary": "Sitemap — Organizations",
        "description": "Returns a cursor-paginated list of publicly indexable organizations. Every organization profile is public, so this includes every contractor that is an Organization.\nItems carry `slug` and no id because the public profile is slug-addressed: `/organizations/{slug}`.\nNo authentication required.\n",
        "tags": [
          "Sitemap"
        ],
        "parameters": [
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Opaque pagination cursor from the previous page's `next_cursor` field.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of items to return (default 5000, max 10000).",
            "schema": {
              "type": "integer",
              "default": 5000,
              "maximum": 10000
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of indexable organizations.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                }
              },
              "ETag": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SitemapOrganizationsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/sitemap/tasks": {
      "get": {
        "operationId": "getTasksSitemap",
        "summary": "Sitemap — Tasks",
        "description": "Returns a cursor-paginated list of open tasks that were shared with contractors.\nA private task is reachable by its `id` but is never listed here, so a crawler\ncannot enumerate it. Tasks use their UUID `id` as the URL key since they have no slug.\nNo authentication required.\n",
        "tags": [
          "Sitemap"
        ],
        "parameters": [
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Opaque pagination cursor from the previous page's `next_cursor` field.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of items to return (default 5000, max 10000).",
            "schema": {
              "type": "integer",
              "default": 5000,
              "maximum": 10000
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of indexable jobs.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                }
              },
              "ETag": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SitemapTasksResponse"
                }
              }
            }
          }
        }
      }
    },
    "/sitemap/properties": {
      "get": {
        "operationId": "getPropertiesSitemap",
        "summary": "Sitemap — Properties",
        "description": "Cursor-paginated list of publicly indexable properties, with the active paddocks and mobs on each (design handoff 22).\nOnly properties whose visibility is `public` appear: a property shared by link or kept private is never listed, and its pages say `noindex`.\nItems carry the address under the web app (`/organizations/{org}/properties/{property}[/paddocks/{paddock}|/mobs/{mob}]`).\nNo authentication required.\n",
        "tags": [
          "Sitemap"
        ],
        "parameters": [
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Opaque pagination cursor from the previous page's `next_cursor` field.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of properties to return (default 5000, max 10000).",
            "schema": {
              "type": "integer",
              "default": 5000,
              "maximum": 10000
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of indexable property, paddock and mob paths.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                }
              },
              "ETag": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SitemapPropertiesResponse"
                }
              }
            }
          }
        }
      }
    },
    "/paddocks/{paddock_id}/sowing-records": {
      "post": {
        "operationId": "createSowingRecord",
        "summary": "Create Sowing Record",
        "description": "Record a crop sown on one paddock. \"Sown with super\" is ONE record: a seed line and a fertiliser line, plus a seed-treatment and an inoculant line when used. Creates the one task that is the record, with its typed sowing child, and answers with that task exactly as `GET /tasks/{id}` would. The server sets the producer from the farm's owner, `operation_type` to `sow`, `capture_source` to `agent` and `operator` from the signed-in user. `area_ha` defaults to the paddock's area. `date` defaults to today; a future date creates the open task instead of a completed record.\n\nEach `products` line has a `role`: `seed` (variety, seed source, lot, germination, thousand-grain weight, rate), `fertiliser` (with `placement` `with_seed` or `banded`), `seed_treatment` (with `grazing_whp_days` and `esi_days`), `inoculant` (with `inoculant_group`, batch, expiry). **A seed treatment's grazing interval sets the paddock's grazing status exactly like a spray does**; fertiliser and inoculant lines never do. **Completing a sowing sets the paddock's current crop** to the seed line's variety, else `crop`. Never invent a variety, batch or withholding period.\n\nType each rate as the farmer said it in `rate` (\"80 kg/ha\", \"400 mL / 100 kg seed\"); it is kept verbatim and split into `rate_value`, `rate_unit` and `rate_basis`. A rate with a number and no unit is refused (422). A completed sowing with no seed line is refused (422, \"A sowing needs a seed line\"). A sowing belongs to a paddock; a mob id is a 404 here.\n\nBefore calling, check `listTasks` with this paddock's `asset_id` and `filter[status][]=completed` for a same-date duplicate, and confirm the summary with the farmer. The MCP tool is refused until it is called again with `confirm: true`. If the call errors or times out, read the paddock's records back before retrying: a 502 or 504 can arrive after the write succeeded. Do not send `broadcast` or `share_scope`. Needs a grant that allows writing in the paddock's business. A 403 whose `code` is `insufficient_access` is that refusal; its `detail` names the business and the access that is missing.\n**Photos.** A file on disk is staged with `createImageUpload`, whose `curl_example` PUTs the bytes, and attached with `image_upload_ids`. Never base64-encode a local photo into a tool call. A public http(s) URL: pass `image_urls`. Only a tiny generated image (under about 100 KB decoded) uses `image_base64s`. One upload can be attached to several records until it expires. On update, `image_mode` defaults to `append` (add to the existing photos); `replace` swaps them all out; `remove_image_ids` deletes specific ones. If any image fails, the whole create or update is rolled back. The attaching call still needs `confirm: true`. `createImageUpload` and its PUT do not.\n",
        "tags": [
          "Sowing records"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "paddock_id",
            "in": "path",
            "required": true,
            "description": "The paddock the sowing was done on.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Created. The task, with its `sowing`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated"
          },
          "403": {
            "description": "Not allowed to write on this paddock. For an assistant the body names the business, the level and where to change it (`insufficient_access`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such paddock."
          },
          "422": {
            "description": "Invalid request."
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SowingRecordCreateModelAttributes"
              },
              "example": {
                "data": {
                  "date": "2026-05-11",
                  "crop": "Barley",
                  "products": [
                    {
                      "role": "seed",
                      "name": "Spartacus CL barley",
                      "variety": "Spartacus CL",
                      "rate": "80 kg/ha"
                    },
                    {
                      "role": "fertiliser",
                      "name": "MAP",
                      "rate": "70 kg/ha",
                      "placement": "with_seed"
                    },
                    {
                      "role": "seed_treatment",
                      "name": "Gaucho 600",
                      "rate": "400 mL / 100 kg seed",
                      "grazing_whp_days": 70
                    }
                  ]
                }
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/SowingRecordCreateModel"
              },
              "example": {
                "data": {
                  "type": "sowing-record",
                  "attributes": {
                    "date": "2026-05-11",
                    "crop": "Barley",
                    "products": [
                      {
                        "role": "seed",
                        "name": "Spartacus CL barley",
                        "variety": "Spartacus CL",
                        "rate": "80 kg/ha"
                      },
                      {
                        "role": "fertiliser",
                        "name": "MAP",
                        "rate": "70 kg/ha",
                        "placement": "with_seed"
                      },
                      {
                        "role": "seed_treatment",
                        "name": "Gaucho 600",
                        "rate": "400 mL / 100 kg seed",
                        "grazing_whp_days": 70
                      }
                    ]
                  }
                }
              }
            }
          }
        }
      }
    },
    "/sowing-records/{id}": {
      "put": {
        "operationId": "updateSowingRecord",
        "summary": "Update Sowing Record",
        "description": "Change what a sowing carries after the fact: any of the attributes below, and `products`, which REPLACES the whole product set (position is the array index, at most 10). An attribute that is not sent is left alone. Answers with the task, exactly as `GET /tasks/{id}` would, with its `sowing`. Needs a grant that allows editing the task. Never invent a batch, expiry, rate or withholding period. The id is the `sowing.id` of the task, not the task's id.\n**Photos.** A file on disk is staged with `createImageUpload`, whose `curl_example` PUTs the bytes, and attached with `image_upload_ids`. Never base64-encode a local photo into a tool call. A public http(s) URL: pass `image_urls`. Only a tiny generated image (under about 100 KB decoded) uses `image_base64s`. One upload can be attached to several records until it expires. On update, `image_mode` defaults to `append` (add to the existing photos); `replace` swaps them all out; `remove_image_ids` deletes specific ones. If any image fails, the whole create or update is rolled back. The attaching call still needs `confirm: true`. `createImageUpload` and its PUT do not.\n",
        "tags": [
          "Sowing records"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The sowing record's id (`sowing.id` on the task).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The task, with its `sowing`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated"
          },
          "403": {
            "description": "Not allowed to edit this task.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such sowing record."
          },
          "422": {
            "description": "Invalid request."
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SowingRecordUpdateModelAttributes"
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/SowingRecordUpdateModel"
              }
            }
          }
        }
      }
    },
    "/mobs/{mob_id}/treatment-records": {
      "post": {
        "operationId": "createTreatmentRecord",
        "summary": "Create Treatment Record",
        "description": "Record a treatment given to one mob: a drench, fluke or lice treatment, a vaccine, a vitamin or mineral, an antibiotic, pain relief, an additive, a foot treatment, a reproductive treatment or an HGP implant. Creates the one task that is the record, with its typed treatment child, and answers with that task exactly as `GET /tasks/{id}` would. The server sets the producer from the farm's owner, `operation_type` to `treatment`, `capture_source` to `agent` and `operator` from the signed-in user. `head` defaults to the mob's head count. `date` (`YYYY-MM-DD`) defaults to today and is stored as midday in the application time zone; a future date creates the open task (`status: pending`, `created_as: task`) instead of a completed record.\n\n`category` is required: `drench`, `fluke`, `lice`, `flystrike`, `other_external_parasite`, `vaccine`, `vitamin_mineral`, `antibiotic`, `pain_relief`, `feed_water_additive`, `foot_treatment`, `reproductive`, `hgp` or `other`. `route` (`oral`, `injection`, `backline`, `jetting`, `dip`, `capsule`, `topical`, `in_feed_water`) is what the farmer said; leave it unset if they did not.\n\nSend one `products` entry per product given together, up to 10 (name, active, batch, `expiry`, dose, `whp_days`, `esi_days`, and the wool and milk intervals when the label has them). A product with no interval adds nothing to the mob's status. Never invent a batch, expiry, dose or withholding period; `listProducts` is a short catalogue to check the label against. A completed record with no product row is refused (422, \"A treatment needs at least one product\").\n\n`GET /mobs/{id}` and `getFarmStatus` report the mob's current status, derived from this record's products. A treatment belongs to a mob; a paddock id is a 404 here.\n\nBefore calling, check `listTasks` with this mob's `asset_id` and `filter[status][]=completed` for a same-date duplicate, and confirm the summary with the farmer. The MCP tool is refused until it is called again with `confirm: true`. If the call errors or times out, read the mob's records back before retrying: a 502 or 504 can arrive after the write succeeded. Do not send `broadcast` or `share_scope`. Needs a grant that allows writing in the mob's business. A 403 whose `code` is `insufficient_access` is that refusal; its `detail` names the business and the access that is missing.\n**Photos.** A file on disk is staged with `createImageUpload`, whose `curl_example` PUTs the bytes, and attached with `image_upload_ids`. Never base64-encode a local photo into a tool call. A public http(s) URL: pass `image_urls`. Only a tiny generated image (under about 100 KB decoded) uses `image_base64s`. One upload can be attached to several records until it expires. On update, `image_mode` defaults to `append` (add to the existing photos); `replace` swaps them all out; `remove_image_ids` deletes specific ones. If any image fails, the whole create or update is rolled back. The attaching call still needs `confirm: true`. `createImageUpload` and its PUT do not.\n",
        "tags": [
          "Treatment records"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "mob_id",
            "in": "path",
            "required": true,
            "description": "The mob the treatment was done on.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Created. The task, with its `treatment`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated"
          },
          "403": {
            "description": "Not allowed to write on this mob. For an assistant the body names the business, the level and where to change it (`insufficient_access`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such mob."
          },
          "422": {
            "description": "Invalid request."
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TreatmentRecordCreateModelAttributes"
              },
              "example": {
                "data": {
                  "date": "2026-10-05",
                  "category": "vaccine",
                  "route": "injection",
                  "head": 600,
                  "description": "Vaccinate the wethers",
                  "products": [
                    {
                      "name": "Glanvac 6 B12",
                      "batch": "GL2231",
                      "expiry": "2027-03-31",
                      "dose": "2 mL SC",
                      "dose_value": 2,
                      "dose_unit": "mL",
                      "dose_basis": "per_head",
                      "whp_days": 0
                    }
                  ]
                }
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/TreatmentRecordCreateModel"
              },
              "example": {
                "data": {
                  "type": "treatment-record",
                  "attributes": {
                    "date": "2026-10-05",
                    "category": "vaccine",
                    "route": "injection",
                    "head": 600,
                    "description": "Vaccinate the wethers",
                    "products": [
                      {
                        "name": "Glanvac 6 B12",
                        "batch": "GL2231",
                        "expiry": "2027-03-31",
                        "dose": "2 mL SC",
                        "dose_value": 2,
                        "dose_unit": "mL",
                        "dose_basis": "per_head",
                        "whp_days": 0
                      }
                    ]
                  }
                }
              }
            }
          }
        }
      }
    },
    "/treatment-records/{id}": {
      "put": {
        "operationId": "updateTreatmentRecord",
        "summary": "Update Treatment Record",
        "description": "Change what a treatment carries after the fact: any of the attributes below, and `products`, which REPLACES the whole product set (position is the array index, at most 10). An attribute that is not sent is left alone. Answers with the task, exactly as `GET /tasks/{id}` would, with its `treatment`. Needs a grant that allows editing the task. Never invent a batch, expiry, rate or withholding period. The id is the `treatment.id` of the task, not the task's id.\n**Photos.** A file on disk is staged with `createImageUpload`, whose `curl_example` PUTs the bytes, and attached with `image_upload_ids`. Never base64-encode a local photo into a tool call. A public http(s) URL: pass `image_urls`. Only a tiny generated image (under about 100 KB decoded) uses `image_base64s`. One upload can be attached to several records until it expires. On update, `image_mode` defaults to `append` (add to the existing photos); `replace` swaps them all out; `remove_image_ids` deletes specific ones. If any image fails, the whole create or update is rolled back. The attaching call still needs `confirm: true`. `createImageUpload` and its PUT do not.\n",
        "tags": [
          "Treatment records"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The treatment record's id (`treatment.id` on the task).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The task, with its `treatment`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/TasksShowResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated"
          },
          "403": {
            "description": "Not allowed to edit this task.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such treatment record."
          },
          "422": {
            "description": "Invalid request."
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TreatmentRecordUpdateModelAttributes"
              }
            },
            "application/vnd.api+json": {
              "schema": {
                "$ref": "#/components/schemas/TreatmentRecordUpdateModel"
              }
            }
          }
        }
      }
    },
    "/users/sms-preferences": {
      "put": {
        "operationId": "updateSmsPreferences",
        "summary": "Update SMS preferences",
        "description": "Turns SMS to the current user's account on or off — the in-app equivalent\nof replying **STOP** to a message.\n\nApplies to every number on the account (the canonical `mobile` and any\n`contact_info.mobile`), and is stored against the number rather than the\naccount, so it also silences messages sent to that number in another\ncontext.\n\nOpting out silences notifications and any marketing message. It does\n**not** silence one-time verification codes: those are requested by the\nuser in the moment, and blocking them would lock the account out of\nphone sign-in.\n\nActs on the authenticated user only — there is no id in the path. Call\nthis only when the person explicitly asks to turn SMS on or off.\n",
        "tags": [
          "Users",
          "SmsPreferences"
        ],
        "security": [
          {
            "bearer_auth": []
          }
        ],
        "parameters": [],
        "responses": {
          "200": {
            "description": "The updated user, with the new `sms_opted_out` value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UsersShowResponseAttributes"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/UsersShowResponse"
                }
              }
            }
          },
          "400": {
            "description": "`sms_opted_out` was missing or not a boolean.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              },
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorsResponse"
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "sms_opted_out": {
                    "type": "boolean",
                    "description": "`true` silences SMS to this account, `false` re-enables it.\nRequired — an absent or non-boolean value is rejected rather\nthan treated as \"no change\".\n"
                  }
                },
                "required": [
                  "sms_opted_out"
                ]
              }
            }
          },
          "required": true,
          "description": "The desired SMS opt-out state."
        }
      }
    }
  },
  "components": {
    "parameters": {
      "Accept": {
        "name": "Accept",
        "in": "header",
        "required": false,
        "description": "Response serialization format (ActiveModelSerializers adapter).\n- `application/json` — attributes adapter; root object `{ \"data\": ..., \"meta\"?: ... }` with resource attributes at top level of `data`.\n- `application/vnd.api+json` — JSON:API adapter; document with `data`, `type`, `attributes`, `relationships`.\nOmit or send `application/json` for plain JSON; send `application/vnd.api+json` for JSON:API.\n",
        "schema": {
          "type": "string"
        }
      },
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "Optional. Send the same value when retrying a create after a 502 or 504. The same user, path and key returns the original result instead of writing a second one. The key is kept for 24 hours.\n",
        "schema": {
          "type": "string",
          "maxLength": 200
        }
      },
      "ContentType": {
        "name": "Content-Type",
        "in": "header",
        "required": false,
        "description": "Request body format. Use `application/json` for attributes-adapter shape (root key or flat attributes); use `application/vnd.api+json` for JSON:API document shape.\n",
        "schema": {
          "type": "string"
        }
      },
      "IncludeService": {
        "name": "include",
        "in": "query",
        "required": false,
        "description": "Comma-separated snake_case relationship paths to embed (JSON:API `included` array, or inlined for `application/json`). Never embedded by default; unknown paths ignored; dot notation selects children; max 10 paths.\nAllowed paths: `owner`, `category`, `city`, `city.location`, `place`, `place.location`, `location`.\n",
        "schema": {
          "type": "string",
          "example": "owner,place.location"
        },
        "x-include-paths": [
          "owner",
          "category",
          "city",
          "city.location",
          "place",
          "place.location",
          "location"
        ]
      },
      "IncludeTask": {
        "name": "include",
        "in": "query",
        "required": false,
        "description": "Comma-separated snake_case relationship paths to embed (JSON:API `included` array, or inlined for `application/json`). Never embedded by default; unknown paths ignored; dot notation selects children; max 10 paths.\nAllowed paths: `producer`, `provider`, `category`, `place`, `place.location`, `service`, `service.owner`, `service.category`, `service.city`, `service.place`, `service.location`, `farm`, `parent`, `children`, `asset`.\n",
        "schema": {
          "type": "string",
          "example": "producer,provider,service"
        },
        "x-include-paths": [
          "producer",
          "provider",
          "category",
          "place",
          "place.location",
          "service",
          "service.owner",
          "service.category",
          "service.city",
          "service.place",
          "service.location",
          "farm",
          "parent",
          "children",
          "asset"
        ]
      },
      "IncludeUser": {
        "name": "include",
        "in": "query",
        "required": false,
        "description": "Comma-separated snake_case relationship paths to embed (JSON:API `included` array, or inlined for `application/json`). Never embedded by default; unknown paths ignored; dot notation selects children; max 10 paths.\nAllowed paths: `city`, `city.location`, `place`, `place.location`, `location`, `services`.\n",
        "schema": {
          "type": "string",
          "example": "services"
        },
        "x-include-paths": [
          "city",
          "city.location",
          "place",
          "place.location",
          "location",
          "services"
        ]
      },
      "IncludeOrganization": {
        "name": "include",
        "in": "query",
        "required": false,
        "description": "Comma-separated snake_case relationship paths to embed (JSON:API `included` array, or inlined for `application/json`). Never embedded by default; unknown paths ignored; dot notation selects children; max 10 paths.\nAllowed paths: `city`, `city.location`, `place`, `place.location`, `services`, `organization_roles`, `organization_roles.user`.\n",
        "schema": {
          "type": "string",
          "example": "organization_roles.user"
        },
        "x-include-paths": [
          "city",
          "city.location",
          "place",
          "place.location",
          "services",
          "organization_roles",
          "organization_roles.user"
        ]
      },
      "IncludeOrganizationRole": {
        "name": "include",
        "in": "query",
        "required": false,
        "description": "Comma-separated snake_case relationship paths to embed (JSON:API `included` array, or inlined for `application/json`). Never embedded by default; unknown paths ignored; max 10 paths.\nAllowed paths: `organization`, `user`.\n",
        "schema": {
          "type": "string",
          "example": "organization"
        },
        "x-include-paths": [
          "organization",
          "user"
        ]
      },
      "IncludeClient": {
        "name": "include",
        "in": "query",
        "required": false,
        "description": "Comma-separated snake_case relationship paths to embed (JSON:API `included` array, or inlined for `application/json`). Never embedded by default; unknown paths ignored; max 10 paths.\nAllowed paths: `owner`, `user`.\n",
        "schema": {
          "type": "string",
          "example": "owner"
        },
        "x-include-paths": [
          "owner",
          "user"
        ]
      },
      "IncludeLikedContractor": {
        "name": "include",
        "in": "query",
        "required": false,
        "description": "Comma-separated snake_case relationship paths to embed (JSON:API `included` array, or inlined for `application/json`). Never embedded by default; unknown paths ignored; max 10 paths.\nAllowed paths: `owner`, `contractor`.\n",
        "schema": {
          "type": "string",
          "example": "contractor"
        },
        "x-include-paths": [
          "owner",
          "contractor"
        ]
      },
      "IncludeSubscription": {
        "name": "include",
        "in": "query",
        "required": false,
        "description": "Comma-separated snake_case relationship paths to embed (JSON:API `included` array, or inlined for `application/json`). Never embedded by default; unknown paths ignored; max 10 paths.\nAllowed paths: `subscribable`.\n",
        "schema": {
          "type": "string",
          "example": "subscribable"
        },
        "x-include-paths": [
          "subscribable"
        ]
      },
      "IncludeFarm": {
        "name": "include",
        "in": "query",
        "required": false,
        "description": "Comma-separated snake_case relationship paths to embed (JSON:API `included` array, or inlined for `application/json`). Never embedded by default; unknown paths ignored; max 10 paths.\nAllowed paths: `organization`.\n",
        "schema": {
          "type": "string",
          "example": "organization"
        },
        "x-include-paths": [
          "organization"
        ]
      },
      "IncludeAssetResources": {
        "name": "include",
        "in": "query",
        "required": false,
        "description": "Comma-separated snake_case relationship paths to embed (JSON:API `included` array, or inlined for `application/json`). Never embedded by default; unknown paths ignored; max 10 paths.\nAllowed paths: `farm`, `tasks`, `mobs`, `paddock`. The `resources` attribute lists the same company without this parameter.\n",
        "schema": {
          "type": "string",
          "example": "farm,tasks,mobs,paddock"
        },
        "x-include-paths": [
          "farm",
          "tasks",
          "mobs",
          "paddock"
        ]
      },
      "IncludePaddock": {
        "name": "include",
        "in": "query",
        "required": false,
        "description": "Comma-separated snake_case relationship paths to embed (JSON:API `included` array, or inlined for `application/json`). Never embedded by default; unknown paths ignored; dot notation selects children; max 10 paths.\nAllowed paths: `farm`, `farm.organization`.\n",
        "schema": {
          "type": "string",
          "example": "farm.organization"
        },
        "x-include-paths": [
          "farm",
          "farm.organization"
        ]
      },
      "IncludeMob": {
        "name": "include",
        "in": "query",
        "required": false,
        "description": "Comma-separated snake_case relationship paths to embed (JSON:API `included` array, or inlined for `application/json`). Never embedded by default; unknown paths ignored; dot notation selects children; max 10 paths.\nAllowed paths: `farm`, `farm.organization`.\n",
        "schema": {
          "type": "string",
          "example": "farm.organization"
        },
        "x-include-paths": [
          "farm",
          "farm.organization"
        ]
      },
      "IncludeCity": {
        "name": "include",
        "in": "query",
        "required": false,
        "description": "Comma-separated snake_case relationship paths to embed (JSON:API `included` array, or inlined for `application/json`). Never embedded by default; unknown paths ignored; max 10 paths.\nAllowed paths: `location`.\n",
        "schema": {
          "type": "string",
          "example": "location"
        },
        "x-include-paths": [
          "location"
        ]
      },
      "IncludeAddress": {
        "name": "include",
        "in": "query",
        "required": false,
        "description": "Comma-separated snake_case relationship paths to embed (JSON:API `included` array, or inlined for `application/json`). Never embedded by default; unknown paths ignored; dot notation selects children; max 10 paths.\nAllowed paths: `city`, `city.location`, `location`.\n",
        "schema": {
          "type": "string",
          "example": "city.location"
        },
        "x-include-paths": [
          "city",
          "city.location",
          "location"
        ]
      },
      "IncludePrediction": {
        "name": "include",
        "in": "query",
        "required": false,
        "description": "Comma-separated snake_case relationship paths to embed (JSON:API `included` array, or inlined for `application/json`). Never embedded by default; unknown paths ignored; max 10 paths.\nAllowed paths: `place`.\n",
        "schema": {
          "type": "string",
          "example": "place"
        },
        "x-include-paths": [
          "place"
        ]
      }
    },
    "securitySchemes": {
      "bearer_auth": {
        "type": "http",
        "scheme": "bearer",
        "description": "**Doorkeeper OAuth 2.0 access token** (opaque). Obtain via `POST /oauth/token` or first-party\n`POST /api/v1/users/sign-in` / `POST /api/v1/users/access-tokens`. Send as header\n`Authorization: Bearer` plus the token value.\n"
      },
      "doorkeeper_oauth2": {
        "type": "oauth2",
        "description": "OAuth 2.0 flows implemented by Doorkeeper. **`tokenUrl` and `authorizationUrl` are path-absolute from the API host**\n(e.g. `https://api.purplegrain.com/oauth/token`), not under `/api/v1`. Tools that resolve URLs against the primary\nserver `https://{defaultHost}/api/v1` will still combine `/oauth/token` with the same host → correct root path.\n",
        "flows": {
          "password": {
            "tokenUrl": "/oauth/token",
            "refreshUrl": "/oauth/token",
            "scopes": {
              "public": "Default scope",
              "read": "Read access",
              "write": "Write access"
            }
          },
          "clientCredentials": {
            "tokenUrl": "/oauth/token",
            "scopes": {
              "public": "Default scope"
            }
          },
          "authorizationCode": {
            "authorizationUrl": "/oauth/authorize",
            "tokenUrl": "/oauth/token",
            "scopes": {
              "public": "Default scope",
              "read": "Read access",
              "write": "Write access"
            }
          }
        }
      }
    },
    "schemas": {
      "PaginationMeta": {
        "type": "object",
        "properties": {
          "current_page": {
            "type": "number"
          },
          "next_page": {
            "type": "number",
            "nullable": true
          },
          "prev_page": {
            "type": "number",
            "nullable": true
          }
        }
      },
      "ErrorsResponse": {
        "type": "object",
        "properties": {
          "errors": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "detail": {
                  "type": "string"
                },
                "title": {
                  "type": "string",
                  "example": "Oops, something went wrong."
                },
                "status": {
                  "type": "string"
                },
                "code": {
                  "type": "string",
                  "nullable": true,
                  "example": "bad_request",
                  "description": "Stable, machine-readable error code for programmatic clients (MCP servers, bots)."
                },
                "business": {
                  "type": "object",
                  "nullable": true,
                  "description": "`insufficient_access`: the business the connection was refused in (`{ id, name }`; id is null for the person's own account)."
                },
                "level": {
                  "type": "string",
                  "nullable": true,
                  "description": "`insufficient_access`: what the connection can do there (none | read; read when a connection without the write scope tried to change something)."
                },
                "required": {
                  "type": "string",
                  "nullable": true,
                  "description": "`insufficient_access`: what the request needed (read | write)."
                },
                "manage_url": {
                  "type": "string",
                  "nullable": true,
                  "description": "`insufficient_access`: where the person changes the connection's access."
                },
                "source": {
                  "type": "object",
                  "properties": {
                    "pointer": {
                      "type": "string",
                      "nullable": true
                    }
                  },
                  "nullable": true
                }
              }
            }
          }
        }
      },
      "AttributesShowEnvelope": {
        "type": "object",
        "description": "Response shape for Accept application/json (attributes adapter).\nRoot has `data` (single resource object) and optional `meta`. The `data` object has\n`id`, `type`, and all resource attribute fields at top level (flat); it does not use\nthe JSON:API structure with nested `attributes` and `relationships`.\nField names match the JSON:API resource attributes for this endpoint.\n",
        "properties": {
          "data": {
            "type": "object",
            "description": "Resource with id, type, and attribute fields at top level (flat structure)."
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta"
          }
        }
      },
      "AttributesIndexEnvelope": {
        "type": "object",
        "description": "Response shape for Accept application/json (attributes adapter).\nRoot has `data` (array of resource objects) and `meta` (pagination). Each element of\n`data` has `id`, `type`, and attribute fields at top level (flat); no nested\n`attributes` or `relationships` as in JSON:API.\n",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Array of resources; each object is flat (id, type, attributes at top level)."
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta"
          }
        }
      },
      "DoorkeeperAccessTokenResponse": {
        "type": "object",
        "description": "Access token issued by Doorkeeper. The same fields are returned from `POST /oauth/token` (RFC 6749).\nFirst-party endpoints (`POST /users/sign-in`, `POST /users/access-tokens`) add `user_id` for convenience.\n",
        "required": [
          "access_token",
          "token_type"
        ],
        "properties": {
          "access_token": {
            "type": "string",
            "description": "Opaque bearer token. Send as header Authorization: Bearer {token} on /api/v1/* routes."
          },
          "token_type": {
            "type": "string",
            "example": "Bearer"
          },
          "expires_in": {
            "type": "integer",
            "description": "Lifetime of the access token in seconds."
          },
          "refresh_token": {
            "type": "string",
            "nullable": true,
            "description": "Present when the client and grant type allow refresh tokens."
          },
          "scope": {
            "type": "string",
            "description": "Granted scopes (space-separated)."
          },
          "user_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "Only returned by first-party JSON endpoints (`/users/sign-in`, `/users/access-tokens`), not by `POST /oauth/token`."
          },
          "actor_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "The acting entity's id (first-party endpoints only). Equals `user_id`\ntoday; becomes the Organization id when a token is scoped to act for\nan organization.\n"
          },
          "actor_type": {
            "type": "string",
            "nullable": true,
            "enum": [
              "User",
              "Organization"
            ],
            "description": "Polymorphic type of `actor_id` (first-party endpoints only)."
          }
        }
      },
      "OAuthErrorResponse": {
        "type": "object",
        "description": "OAuth 2.0 error object (RFC 6749 §5.2 / RFC 7591 §3.2.2).",
        "properties": {
          "error": {
            "type": "string",
            "example": "invalid_redirect_uri"
          },
          "error_description": {
            "type": "string"
          }
        }
      },
      "ClientRegistrationRequest": {
        "type": "object",
        "description": "RFC 7591 Dynamic Client Registration request metadata.",
        "required": [
          "redirect_uris"
        ],
        "properties": {
          "client_name": {
            "type": "string",
            "description": "Human-readable client name. Defaults to \"MCP client\" when omitted."
          },
          "redirect_uris": {
            "type": "array",
            "description": "Redirect URIs (max 5). Each must be `https`, an RFC 8252 loopback `http`\nURI (localhost / 127.0.0.1 / ::1), or a native private-use scheme\n(e.g. `com.example.app:/cb`). Plain `http` on a remote host is rejected.\n",
            "items": {
              "type": "string",
              "format": "uri"
            }
          },
          "token_endpoint_auth_method": {
            "type": "string",
            "description": "`none` registers a public client (PKCE, no secret) — the default and the\nnorm for MCP clients. Any other value registers a confidential client and\na `client_secret` is returned.\n",
            "enum": [
              "none",
              "client_secret_basic",
              "client_secret_post"
            ]
          },
          "scope": {
            "type": "string",
            "description": "Space-separated requested scopes. `public`, `read` and `write` are\nhonored; unknown scopes are dropped. Defaults to `public read write`:\nan assistant connection starts with read and write where the person's\nrole allows it, limited by the per-business grants they can narrow.\n"
          },
          "application_type": {
            "type": "string"
          },
          "grant_types": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "response_types": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "ClientRegistrationResponse": {
        "type": "object",
        "description": "RFC 7591 Dynamic Client Registration response.",
        "properties": {
          "client_id": {
            "type": "string"
          },
          "client_id_issued_at": {
            "type": "integer",
            "description": "Issuance time (seconds since the Unix epoch)."
          },
          "client_secret": {
            "type": "string",
            "description": "Present only for confidential clients (token_endpoint_auth_method other than `none`)."
          },
          "client_secret_expires_at": {
            "type": "integer",
            "description": "0 = never expires (RFC 7591). Present only for confidential clients."
          },
          "redirect_uris": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uri"
            }
          },
          "token_endpoint_auth_method": {
            "type": "string"
          },
          "grant_types": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "response_types": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "client_name": {
            "type": "string"
          },
          "scope": {
            "type": "string"
          }
        }
      },
      "AuthorizationGrantRequest": {
        "type": "object",
        "description": "Authorize parameters for the headless grant. Identical to the query\nparameters of `GET /oauth/authorize`; sent as a POST body alongside the\nconsenting user's bearer token.\n",
        "required": [
          "client_id",
          "response_type",
          "redirect_uri",
          "code_challenge",
          "code_challenge_method"
        ],
        "properties": {
          "client_id": {
            "type": "string"
          },
          "response_type": {
            "type": "string",
            "enum": [
              "code"
            ]
          },
          "redirect_uri": {
            "type": "string",
            "format": "uri"
          },
          "scope": {
            "type": "string"
          },
          "state": {
            "type": "string"
          },
          "code_challenge": {
            "type": "string",
            "description": "PKCE code challenge (S256)."
          },
          "code_challenge_method": {
            "type": "string",
            "enum": [
              "S256"
            ]
          },
          "nonce": {
            "type": "string"
          },
          "resource": {
            "type": "string",
            "format": "uri",
            "description": "The exact MCP resource URI advertised by discovery. Omission uses that resource for existing clients."
          }
        }
      },
      "AuthorizationGrantResponse": {
        "type": "object",
        "description": "The redirect URI (carrying `code` and `state`) for the browser to follow back to the client.",
        "properties": {
          "redirect_uri": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "OAuthServerMetadata": {
        "type": "object",
        "description": "OAuth 2.0 Authorization Server Metadata (RFC 8414). Endpoints use the configured MCP resource origin.",
        "properties": {
          "issuer": {
            "type": "string",
            "format": "uri"
          },
          "authorization_endpoint": {
            "type": "string",
            "format": "uri",
            "description": "May live on a different origin than the issuer (the frontend hosts the\ninteractive consent screen, since sessions are disabled on the API).\n"
          },
          "token_endpoint": {
            "type": "string",
            "format": "uri"
          },
          "registration_endpoint": {
            "type": "string",
            "format": "uri"
          },
          "introspection_endpoint": {
            "type": "string",
            "format": "uri"
          },
          "revocation_endpoint": {
            "type": "string",
            "format": "uri"
          },
          "scopes_supported": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "response_types_supported": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "grant_types_supported": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "token_endpoint_auth_methods_supported": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "code_challenge_methods_supported": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "OAuthProtectedResourceMetadata": {
        "type": "object",
        "description": "OAuth 2.0 Protected Resource Metadata (RFC 9728). Points MCP clients back at this authorization server.",
        "properties": {
          "resource": {
            "type": "string",
            "format": "uri",
            "description": "The MCP resource server URL (issuer + `/mcp`)."
          },
          "authorization_servers": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uri"
            }
          },
          "scopes_supported": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "bearer_methods_supported": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "AttachmentModel": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "example": "attachment"
          },
          "attributes": {
            "type": "object",
            "properties": {
              "url": {
                "type": "string",
                "nullable": true,
                "description": "Public URL of the stored photo (served via /attachments/{id}.jpeg)."
              },
              "width": {
                "type": "integer",
                "nullable": true
              },
              "height": {
                "type": "integer",
                "nullable": true
              },
              "content_type": {
                "type": "string",
                "nullable": true
              },
              "created_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          }
        }
      },
      "AttachmentsShowResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/AttachmentModel"
          }
        }
      },
      "AttachmentModelAttributes": {
        "type": "object",
        "description": "Attachment in attributes-adapter shape (Accept application/json); fields at top level.",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "example": "attachment"
          },
          "url": {
            "type": "string",
            "nullable": true
          },
          "width": {
            "type": "integer",
            "nullable": true
          },
          "height": {
            "type": "integer",
            "nullable": true
          },
          "content_type": {
            "type": "string",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "AttachmentsShowResponseAttributes": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/AttachmentModelAttributes"
          }
        }
      },
      "ImageUploadIdList": {
        "type": "array",
        "maxItems": 10,
        "description": "Staged uploads from `createImageUpload`, in gallery order. A file on the agent's disk is staged with `createImageUpload`, whose `curl_example` PUTs the bytes, and attached with these ids. Never base64-encode a local photo into a tool call. One upload can be attached to several records (up to 20) until it expires; they share the same photo.\n",
        "items": {
          "type": "string"
        }
      },
      "ImageUrlList": {
        "type": "array",
        "maxItems": 10,
        "description": "Public http(s) URLs for the server to fetch, in gallery order. Use this when the image is already at a public URL.",
        "items": {
          "type": "string",
          "format": "uri"
        }
      },
      "ImageBase64List": {
        "type": "array",
        "maxItems": 10,
        "description": "Base64 image data, optionally a `data:image/...;base64,` URI, in gallery order. Only for a tiny generated image under about 100 KB decoded. A local photo uses `createImageUpload` instead.\n",
        "items": {
          "type": "string"
        }
      },
      "ImageMode": {
        "type": "string",
        "enum": [
          "append",
          "replace"
        ],
        "description": "Updates only; ignored on create. `append` (the default) adds these photos to the ones already on the task. `replace` removes every existing photo and stores only the ones in this request.\n"
      },
      "ImageRemoveIdList": {
        "type": "array",
        "maxItems": 30,
        "description": "Updates only; ignored on create. Photos to remove from the gallery. Each id comes from the task's `image_ids` (or `images[].id`). Used with the default `append`, or on its own.\n",
        "items": {
          "type": "string"
        }
      },
      "TaskImage": {
        "type": "object",
        "required": [
          "id",
          "position"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Gallery attachment id. The same value is in `image_ids`, and it is what `remove_image_ids` and `DELETE /tasks/{id}/images/{image_id}` take."
          },
          "url": {
            "type": "string",
            "nullable": true
          },
          "thumb_url": {
            "type": "string",
            "nullable": true
          },
          "content_type": {
            "type": "string",
            "nullable": true
          },
          "byte_size": {
            "type": "integer",
            "nullable": true
          },
          "width": {
            "type": "integer",
            "nullable": true
          },
          "height": {
            "type": "integer",
            "nullable": true
          },
          "position": {
            "type": "integer",
            "description": "Gallery order, starting at 0. The photo at position 0 is the cover (`image_url`)."
          },
          "source": {
            "type": "string",
            "nullable": true,
            "enum": [
              "upload",
              "url",
              "base64"
            ],
            "description": "How the photo was attached. Blank on photos stored before this field existed, and on a multipart upload."
          }
        }
      },
      "ImageUploadCreateModel": {
        "type": "object",
        "description": "Nested JSON:API request body (application/vnd.api+json) for POST /image-uploads. Equivalent flat shape: `ImageUploadCreateModelAttributes`.",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "type": "object",
            "required": [
              "attributes"
            ],
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "image-upload"
                ]
              },
              "attributes": {
                "$ref": "#/components/schemas/ImageUploadCreateFields"
              }
            }
          }
        }
      },
      "ImageUploadCreateModelAttributes": {
        "type": "object",
        "description": "Flat request body (application/json) for POST /image-uploads: the fields directly under `data`. Equivalent nested shape: `ImageUploadCreateModel`.",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "$ref": "#/components/schemas/ImageUploadCreateFields"
          }
        }
      },
      "ImageUploadCreateFields": {
        "type": "object",
        "required": [
          "filename",
          "content_type",
          "byte_size"
        ],
        "properties": {
          "filename": {
            "type": "string",
            "description": "File name, used in `curl_example` (`@IMG_0079.heic` when it is a plain basename).",
            "example": "IMG_0079.heic"
          },
          "content_type": {
            "type": "string",
            "description": "Declared type. Must match the file's magic bytes when the PUT arrives. One of image/jpeg, image/png, image/webp, image/heic, image/heif.",
            "example": "image/heic"
          },
          "byte_size": {
            "type": "integer",
            "minimum": 1,
            "maximum": 20971520,
            "description": "Exact size of the file in bytes. The PUT body must be this long, and it cannot exceed 20 MB."
          },
          "checksum": {
            "type": "string",
            "description": "Optional integrity check of the original file, standard base64 of the raw MD5 (16 bytes) or SHA-256 (32 bytes) digest. Checked against the bytes before HEIC conversion."
          }
        }
      },
      "ImageUploadResponse": {
        "type": "object",
        "description": "Nested JSON:API response (Accept application/vnd.api+json).",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "type": {
                "type": "string",
                "example": "image-upload"
              },
              "attributes": {
                "$ref": "#/components/schemas/ImageUploadFields"
              }
            }
          }
        }
      },
      "ImageUploadResponseAttributes": {
        "type": "object",
        "description": "Flat response (Accept application/json). The fields the agent needs are directly under `data`, including `upload_id`, `upload_url` and `curl_example`.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/ImageUploadFields"
          }
        }
      },
      "ImageUploadFields": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "example": "image-upload"
          },
          "filename": {
            "type": "string"
          },
          "content_type": {
            "type": "string"
          },
          "byte_size": {
            "type": "integer"
          },
          "upload_id": {
            "type": "string",
            "description": "Opaque signed id. Pass it in `image_upload_ids`. It is not the row's uuid and it is not the PUT token."
          },
          "upload_url": {
            "type": "string",
            "description": "One-time PUT URL on this API host. No bearer token; the credential is in the URL, scoped to this user, this upload and this byte size. Expires in about 30 minutes."
          },
          "method": {
            "type": "string",
            "enum": [
              "PUT"
            ]
          },
          "headers": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Headers the PUT must send. Includes Content-Type."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "max_bytes": {
            "type": "integer",
            "description": "Maximum accepted file size, 20 MB."
          },
          "curl_example": {
            "type": "string",
            "description": "Ready to run from the directory that contains the file. For example, curl -sS -X PUT --data-binary @IMG_0079.heic -H \"Content-Type: image/heic\" followed by the upload_url.\n"
          }
        }
      },
      "ImageUploadPutResponse": {
        "type": "object",
        "required": [
          "upload_id",
          "status",
          "content_type",
          "byte_size"
        ],
        "properties": {
          "upload_id": {
            "type": "string",
            "description": "The same signed id to pass as `image_upload_ids`."
          },
          "status": {
            "type": "string",
            "enum": [
              "uploaded"
            ]
          },
          "content_type": {
            "type": "string",
            "description": "The stored type. HEIC and HEIF are stored as image/jpeg."
          },
          "byte_size": {
            "type": "integer",
            "description": "Size of the stored image, after conversion, orientation and resizing."
          }
        }
      },
      "UserModel": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string"
          },
          "attributes": {
            "type": "object",
            "properties": {
              "email": {
                "type": "string",
                "format": "email"
              },
              "name": {
                "type": "string",
                "nullable": true
              },
              "about": {
                "type": "string",
                "nullable": true
              },
              "account_mode": {
                "type": "string",
                "enum": [
                  "grower",
                  "contractor"
                ],
                "nullable": true,
                "description": "Grower vs Contractor mode; only present for the current user."
              },
              "default_record_mode": {
                "type": "string",
                "enum": [
                  "ai",
                  "general"
                ],
                "description": "Which record type the composer opens on: `ai` (PurpleGrain AI, the default) or `general`. Only present for the current user. A null value is ignored on update."
              },
              "mobile": {
                "type": "string",
                "nullable": true,
                "description": "Canonical login mobile (E.164); only present for the current user."
              },
              "mobile_verified": {
                "type": "boolean",
                "nullable": true,
                "description": "Whether the mobile has been verified by SMS one-time code; only present for the current user."
              },
              "sms_opted_out": {
                "type": "boolean",
                "nullable": true,
                "description": "Whether SMS to this account is silenced (by replying STOP, or from the account screen); only present for the current user."
              },
              "handle": {
                "type": "string",
                "nullable": false
              },
              "free_trial_used": {
                "type": "boolean",
                "nullable": true,
                "description": "Whether the free trial has been used. Only populated when viewing your own profile; null otherwise."
              },
              "url": {
                "type": "string",
                "nullable": false
              },
              "avatar_url": {
                "type": "string",
                "nullable": true
              },
              "avatar_thumb_url": {
                "type": "string",
                "nullable": true
              },
              "updated_at": {
                "type": "string",
                "format": "date-time"
              },
              "created_at": {
                "type": "string",
                "format": "date-time"
              },
              "contact_info": {
                "type": "object",
                "properties": {
                  "mobile": {
                    "type": "string",
                    "nullable": true
                  },
                  "phone": {
                    "type": "string",
                    "nullable": true
                  },
                  "facebook": {
                    "type": "string",
                    "nullable": true
                  },
                  "url": {
                    "type": "string",
                    "nullable": true
                  },
                  "email": {
                    "type": "boolean",
                    "nullable": true
                  }
                }
              }
            }
          },
          "relationships": {
            "type": "object",
            "properties": {
              "city": {
                "type": "object",
                "properties": {
                  "data": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "type": {
                        "type": "string"
                      }
                    },
                    "nullable": true
                  }
                }
              },
              "place": {
                "type": "object",
                "properties": {
                  "data": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "type": {
                        "type": "string"
                      }
                    },
                    "nullable": true
                  }
                }
              },
              "services": {
                "type": "object",
                "properties": {
                  "data": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "type": {
                          "type": "string"
                        }
                      },
                      "nullable": true
                    },
                    "nullable": true
                  }
                }
              },
              "location": {
                "type": "object",
                "properties": {
                  "data": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "type": {
                        "type": "string"
                      }
                    },
                    "nullable": true
                  }
                }
              }
            }
          }
        }
      },
      "UserUpdateModel": {
        "type": "object",
        "description": "Writable fields for updating the current user (PUT /users/{id}). Only the\nself-editable attributes are accepted; read-only fields (email, slug, url,\navatar_*, timestamps) sent in the body are ignored.\n",
        "properties": {
          "data": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "string",
                "nullable": true
              },
              "type": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "user"
                ]
              },
              "attributes": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "nullable": true
                  },
                  "about": {
                    "type": "string",
                    "nullable": true
                  },
                  "handle": {
                    "type": "string"
                  },
                  "account_mode": {
                    "type": "string",
                    "enum": [
                      "grower",
                      "contractor"
                    ],
                    "nullable": true
                  },
                  "default_record_mode": {
                    "type": "string",
                    "enum": [
                      "ai",
                      "general"
                    ],
                    "description": "Which record type the composer opens on: `ai` (PurpleGrain AI, the default) or `general`. Only present for the current user. A null value is ignored on update."
                  },
                  "mobile": {
                    "type": "string",
                    "nullable": true
                  },
                  "contact_info": {
                    "type": "object",
                    "properties": {
                      "mobile": {
                        "type": "string",
                        "nullable": true
                      },
                      "phone": {
                        "type": "string",
                        "nullable": true
                      },
                      "facebook": {
                        "type": "string",
                        "nullable": true
                      },
                      "url": {
                        "type": "string",
                        "nullable": true
                      },
                      "email": {
                        "type": "boolean",
                        "nullable": true
                      }
                    }
                  }
                }
              },
              "relationships": {
                "type": "object",
                "properties": {
                  "place": {
                    "type": "object",
                    "properties": {
                      "data": {
                        "type": "object",
                        "nullable": true,
                        "properties": {
                          "id": {
                            "type": "string",
                            "nullable": true
                          },
                          "type": {
                            "type": "string",
                            "nullable": true,
                            "enum": [
                              "city",
                              "address"
                            ]
                          }
                        }
                      }
                    }
                  }
                }
              }
            },
            "required": [
              "id",
              "type",
              "attributes"
            ]
          }
        },
        "required": [
          "data"
        ]
      },
      "UsersIndexResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/UserModel"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta"
          }
        }
      },
      "UsersShowResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/UserModel"
          }
        }
      },
      "UserModelAttributes": {
        "type": "object",
        "description": "User in attributes-adapter shape (Accept application/json); fields at top level.",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "example": "user"
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "about": {
            "type": "string",
            "nullable": true
          },
          "account_mode": {
            "type": "string",
            "enum": [
              "grower",
              "contractor"
            ],
            "nullable": true,
            "description": "Grower vs Contractor mode; only present for the current user."
          },
          "default_record_mode": {
            "type": "string",
            "enum": [
              "ai",
              "general"
            ],
            "description": "Which record type the composer opens on: `ai` (PurpleGrain AI, the default) or `general`. Only present for the current user. A null value is ignored on update."
          },
          "free_trial_used": {
            "type": "boolean",
            "nullable": true,
            "description": "Whether the free trial has been used. Only populated when viewing your own profile; null otherwise."
          },
          "mobile": {
            "type": "string",
            "nullable": true,
            "description": "Canonical login mobile (E.164); only present for the current user."
          },
          "mobile_verified": {
            "type": "boolean",
            "nullable": true,
            "description": "Whether the mobile has been verified by SMS one-time code; only present for the current user."
          },
          "sms_opted_out": {
            "type": "boolean",
            "nullable": true,
            "description": "Whether SMS to this account is silenced (by replying STOP, or from the account screen); only present for the current user."
          },
          "handle": {
            "type": "string"
          },
          "url": {
            "type": "string"
          },
          "avatar_url": {
            "type": "string",
            "nullable": true
          },
          "avatar_thumb_url": {
            "type": "string",
            "nullable": true
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "contact_info": {
            "type": "object",
            "properties": {
              "mobile": {
                "type": "string",
                "nullable": true
              },
              "phone": {
                "type": "string",
                "nullable": true
              },
              "facebook": {
                "type": "string",
                "nullable": true
              },
              "url": {
                "type": "string",
                "nullable": true
              },
              "email": {
                "type": "boolean",
                "nullable": true
              }
            }
          },
          "city": {
            "type": "object",
            "description": "Inlined when include=city"
          },
          "place": {
            "type": "object",
            "description": "Inlined when include=place"
          },
          "services": {
            "type": "array",
            "nullable": true,
            "description": "Inlined when include=services"
          },
          "location": {
            "type": "object",
            "description": "Inlined when include=location"
          }
        }
      },
      "UsersIndexResponseAttributes": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/UserModelAttributes"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta"
          }
        }
      },
      "UsersShowResponseAttributes": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/UserModelAttributes"
          }
        }
      },
      "UserUpdateModelAttributes": {
        "type": "object",
        "description": "Flat attributes-adapter request body (Content-Type application/json). Attribute fields and relationships live directly under `data` (no nested `attributes` / `relationships` envelopes). Each relationship is a resource identifier object ({ id, type }). Equivalent JSON:API (nested) shape: `UserUpdateModel` sent as application/vnd.api+json.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string",
                "nullable": true
              },
              "about": {
                "type": "string",
                "nullable": true
              },
              "handle": {
                "type": "string"
              },
              "account_mode": {
                "type": "string",
                "enum": [
                  "grower",
                  "contractor"
                ],
                "nullable": true
              },
              "default_record_mode": {
                "type": "string",
                "enum": [
                  "ai",
                  "general"
                ],
                "description": "Which record type the composer opens on: `ai` (PurpleGrain AI, the default) or `general`. Only present for the current user. A null value is ignored on update."
              },
              "mobile": {
                "type": "string",
                "nullable": true
              },
              "contact_info": {
                "type": "object",
                "properties": {
                  "mobile": {
                    "type": "string",
                    "nullable": true
                  },
                  "phone": {
                    "type": "string",
                    "nullable": true
                  },
                  "facebook": {
                    "type": "string",
                    "nullable": true
                  },
                  "url": {
                    "type": "string",
                    "nullable": true
                  },
                  "email": {
                    "type": "boolean",
                    "nullable": true
                  }
                }
              },
              "place": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "string",
                    "nullable": true
                  },
                  "type": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "city",
                      "address"
                    ]
                  }
                }
              }
            }
          }
        },
        "required": [
          "data"
        ]
      },
      "OrganizationModel": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string"
          },
          "attributes": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "slug": {
                "type": "string",
                "nullable": true
              },
              "avatar_url": {
                "type": "string",
                "nullable": true
              },
              "avatar_thumb_url": {
                "type": "string",
                "nullable": true
              },
              "free_trial_used": {
                "type": "boolean",
                "description": "Whether the organization has used its free trial."
              },
              "ai_format_enabled": {
                "type": "boolean",
                "description": "Allow PurpleGrain AI to read this organisation's records (default true). Only present for members. When false, the formatter's request carries no farm context for this organisation. Writable on update by whoever can update the organization."
              },
              "updated_at": {
                "type": "string",
                "format": "date-time"
              },
              "created_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "relationships": {
            "type": "object",
            "properties": {
              "city": {
                "type": "object",
                "properties": {
                  "data": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "type": {
                        "type": "string"
                      }
                    },
                    "nullable": true
                  }
                }
              },
              "place": {
                "type": "object",
                "properties": {
                  "data": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "type": {
                        "type": "string"
                      }
                    },
                    "nullable": true
                  }
                }
              },
              "services": {
                "type": "object",
                "properties": {
                  "data": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "type": {
                          "type": "string"
                        }
                      },
                      "nullable": true
                    },
                    "nullable": true
                  }
                }
              },
              "organization_roles": {
                "type": "object",
                "properties": {
                  "data": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "type": {
                          "type": "string"
                        }
                      },
                      "nullable": true
                    },
                    "nullable": true
                  }
                }
              }
            }
          }
        }
      },
      "OrganizationCreateModel": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "organization"
                ]
              },
              "attributes": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "slug": {
                    "type": "string",
                    "nullable": true
                  },
                  "contact_info": {
                    "type": "object",
                    "description": "Free-form contact details (e.g. facebook, website, phone).",
                    "nullable": true
                  }
                },
                "required": [
                  "name"
                ]
              },
              "relationships": {
                "type": "object",
                "properties": {
                  "place": {
                    "type": "object",
                    "properties": {
                      "data": {
                        "type": "object",
                        "nullable": true,
                        "properties": {
                          "id": {
                            "type": "string",
                            "nullable": true
                          },
                          "type": {
                            "type": "string",
                            "nullable": true,
                            "enum": [
                              "city",
                              "address"
                            ]
                          }
                        }
                      }
                    }
                  }
                }
              }
            },
            "required": [
              "type",
              "attributes"
            ]
          }
        },
        "required": [
          "data"
        ]
      },
      "OrganizationUpdateModel": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "string",
                "nullable": true
              },
              "type": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "organization"
                ]
              },
              "attributes": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "slug": {
                    "type": "string",
                    "nullable": true
                  },
                  "contact_info": {
                    "type": "object",
                    "description": "Free-form contact details (e.g. facebook, website, phone).",
                    "nullable": true
                  },
                  "ai_format_enabled": {
                    "type": "boolean",
                    "description": "Allow PurpleGrain AI to read this organisation's records (default true). Off means the formatter's request carries no farm context for this organisation. Writable by whoever can update the organization; a null value is ignored."
                  }
                }
              },
              "relationships": {
                "type": "object",
                "properties": {
                  "place": {
                    "type": "object",
                    "properties": {
                      "data": {
                        "type": "object",
                        "nullable": true,
                        "properties": {
                          "id": {
                            "type": "string",
                            "nullable": true
                          },
                          "type": {
                            "type": "string",
                            "nullable": true,
                            "enum": [
                              "city",
                              "address"
                            ]
                          }
                        }
                      }
                    }
                  }
                }
              }
            },
            "required": [
              "id",
              "type",
              "attributes"
            ]
          }
        },
        "required": [
          "data"
        ]
      },
      "OrganizationsIndexResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OrganizationModel"
            }
          }
        }
      },
      "OrganizationsShowResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/OrganizationModel"
          }
        }
      },
      "OrganizationModelAttributes": {
        "type": "object",
        "description": "Organization in attributes-adapter shape (Accept application/json); fields at top level.",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "example": "organization"
          },
          "name": {
            "type": "string"
          },
          "slug": {
            "type": "string",
            "nullable": true
          },
          "avatar_url": {
            "type": "string",
            "nullable": true
          },
          "avatar_thumb_url": {
            "type": "string",
            "nullable": true
          },
          "free_trial_used": {
            "type": "boolean",
            "description": "Whether the organization has used its free trial."
          },
          "ai_format_enabled": {
            "type": "boolean",
            "description": "Allow PurpleGrain AI to read this organisation's records (default true). Only present for members. When false, the formatter's request carries no farm context for this organisation. Writable on update by whoever can update the organization."
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "city": {
            "type": "object",
            "description": "Inlined when include=city"
          },
          "place": {
            "type": "object",
            "description": "Inlined when include=place"
          },
          "services": {
            "type": "array",
            "nullable": true,
            "description": "Inlined when include=services"
          },
          "organization_roles": {
            "type": "array",
            "nullable": true,
            "description": "Inlined when include=organization_roles"
          }
        }
      },
      "OrganizationsIndexResponseAttributes": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OrganizationModelAttributes"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta"
          }
        }
      },
      "OrganizationsShowResponseAttributes": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/OrganizationModelAttributes"
          }
        }
      },
      "OrganizationCreateModelAttributes": {
        "type": "object",
        "description": "Flat attributes-adapter request body (Content-Type application/json). Attribute fields and relationships live directly under `data` (no nested `attributes` / `relationships` envelopes). Each relationship is a resource identifier object ({ id, type }). Equivalent JSON:API (nested) shape: `OrganizationCreateModel` sent as application/vnd.api+json.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "slug": {
                "type": "string",
                "nullable": true
              },
              "contact_info": {
                "type": "object",
                "description": "Free-form contact details (e.g. facebook, website, phone).",
                "nullable": true
              },
              "place": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "string",
                    "nullable": true
                  },
                  "type": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "city",
                      "address"
                    ]
                  }
                }
              }
            },
            "required": [
              "name"
            ]
          }
        },
        "required": [
          "data"
        ]
      },
      "OrganizationUpdateModelAttributes": {
        "type": "object",
        "description": "Flat attributes-adapter request body (Content-Type application/json). Attribute fields and relationships live directly under `data` (no nested `attributes` / `relationships` envelopes). Each relationship is a resource identifier object ({ id, type }). Equivalent JSON:API (nested) shape: `OrganizationUpdateModel` sent as application/vnd.api+json.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "slug": {
                "type": "string",
                "nullable": true
              },
              "contact_info": {
                "type": "object",
                "description": "Free-form contact details (e.g. facebook, website, phone).",
                "nullable": true
              },
              "ai_format_enabled": {
                "type": "boolean",
                "description": "Allow PurpleGrain AI to read this organisation's records (default true). Off means the formatter's request carries no farm context for this organisation. Writable by whoever can update the organization; a null value is ignored."
              },
              "place": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "string",
                    "nullable": true
                  },
                  "type": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "city",
                      "address"
                    ]
                  }
                }
              }
            }
          }
        },
        "required": [
          "data"
        ]
      },
      "OrganizationRoleModel": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string"
          },
          "attributes": {
            "type": "object",
            "properties": {
              "status": {
                "type": "string",
                "enum": [
                  "pending",
                  "accepted",
                  "declined"
                ]
              },
              "permission": {
                "type": "string",
                "enum": [
                  "owner",
                  "admin",
                  "manager",
                  "contractor",
                  "viewer"
                ]
              },
              "updated_at": {
                "type": "string",
                "format": "date-time"
              },
              "created_at": {
                "type": "string",
                "format": "date-time"
              },
              "invited_email": {
                "type": "string",
                "nullable": true,
                "description": "Pending invite identifier; cleared when claimed."
              },
              "invited_mobile": {
                "type": "string",
                "nullable": true,
                "description": "Pending invite identifier (E.164); cleared when claimed."
              },
              "invited": {
                "type": "boolean",
                "description": "True while the role has no attached user yet."
              },
              "inviter_name": {
                "type": "string",
                "description": "Display name of the inviter (the organization owner), for the invitee-side \"X invited you as ...\" byline. Name only, never a contact identifier."
              }
            }
          },
          "relationships": {
            "type": "object",
            "properties": {
              "organization": {
                "type": "object",
                "properties": {
                  "data": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "type": {
                        "type": "string"
                      }
                    },
                    "nullable": true
                  }
                }
              },
              "user": {
                "type": "object",
                "properties": {
                  "data": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "type": {
                        "type": "string"
                      }
                    },
                    "nullable": true
                  }
                }
              }
            }
          }
        }
      },
      "OrganizationRoleCreateModel": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "organization_role"
                ]
              },
              "attributes": {
                "type": "object",
                "properties": {
                  "permission": {
                    "type": "string",
                    "enum": [
                      "owner",
                      "admin",
                      "manager",
                      "contractor",
                      "viewer"
                    ]
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "pending",
                      "accepted",
                      "declined"
                    ],
                    "nullable": true
                  },
                  "invited_email": {
                    "type": "string",
                    "nullable": true,
                    "description": "Invite by email when no existing user is linked. One of user / invited_email / invited_mobile identifies the invitee."
                  },
                  "invited_mobile": {
                    "type": "string",
                    "nullable": true,
                    "description": "Invite by mobile when no existing user is linked."
                  }
                },
                "required": [
                  "permission"
                ]
              },
              "relationships": {
                "type": "object",
                "properties": {
                  "organization": {
                    "type": "object",
                    "properties": {
                      "data": {
                        "type": "object",
                        "nullable": true,
                        "properties": {
                          "id": {
                            "type": "string",
                            "nullable": true
                          },
                          "type": {
                            "type": "string",
                            "nullable": true,
                            "enum": [
                              "organization"
                            ]
                          }
                        }
                      }
                    },
                    "required": [
                      "data"
                    ]
                  },
                  "user": {
                    "type": "object",
                    "properties": {
                      "data": {
                        "type": "object",
                        "nullable": true,
                        "properties": {
                          "id": {
                            "type": "string",
                            "nullable": true
                          },
                          "type": {
                            "type": "string",
                            "nullable": true,
                            "enum": [
                              "user"
                            ]
                          }
                        }
                      }
                    },
                    "required": [
                      "data"
                    ]
                  }
                }
              }
            },
            "required": [
              "type",
              "attributes"
            ]
          }
        },
        "required": [
          "data"
        ]
      },
      "OrganizationRoleUpdateModel": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "string",
                "nullable": true
              },
              "type": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "organization_role"
                ]
              },
              "attributes": {
                "type": "object",
                "properties": {
                  "permission": {
                    "type": "string",
                    "enum": [
                      "owner",
                      "admin",
                      "manager",
                      "contractor",
                      "viewer"
                    ]
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "pending",
                      "accepted",
                      "declined"
                    ],
                    "nullable": true
                  }
                }
              },
              "relationships": {
                "type": "object",
                "properties": {
                  "organization": {
                    "type": "object",
                    "properties": {
                      "data": {
                        "type": "object",
                        "nullable": true,
                        "properties": {
                          "id": {
                            "type": "string",
                            "nullable": true
                          },
                          "type": {
                            "type": "string",
                            "nullable": true,
                            "enum": [
                              "organization"
                            ]
                          }
                        }
                      }
                    },
                    "required": [
                      "data"
                    ]
                  },
                  "user": {
                    "type": "object",
                    "properties": {
                      "data": {
                        "type": "object",
                        "nullable": true,
                        "properties": {
                          "id": {
                            "type": "string",
                            "nullable": true
                          },
                          "type": {
                            "type": "string",
                            "nullable": true,
                            "enum": [
                              "user"
                            ]
                          }
                        }
                      }
                    },
                    "required": [
                      "data"
                    ]
                  }
                }
              }
            },
            "required": [
              "id",
              "type",
              "attributes"
            ]
          }
        },
        "required": [
          "data"
        ]
      },
      "OrganizationRolesIndexResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OrganizationRoleModel"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta"
          }
        }
      },
      "OrganizationRolesShowResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/OrganizationRoleModel"
          }
        }
      },
      "OrganizationRoleModelAttributes": {
        "type": "object",
        "description": "Organization role in attributes-adapter shape (Accept application/json); fields at top level.",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "example": "organization_role"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "accepted",
              "declined"
            ]
          },
          "permission": {
            "type": "string",
            "enum": [
              "owner",
              "admin",
              "manager",
              "contractor",
              "viewer"
            ]
          },
          "invited_email": {
            "type": "string",
            "nullable": true
          },
          "invited_mobile": {
            "type": "string",
            "nullable": true
          },
          "invited": {
            "type": "boolean"
          },
          "inviter_name": {
            "type": "string",
            "description": "Display name of the inviter (the organization owner)."
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "organization": {
            "type": "object",
            "description": "Inlined when include=organization"
          },
          "user": {
            "type": "object",
            "description": "Inlined when include=user"
          }
        }
      },
      "OrganizationRolesIndexResponseAttributes": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OrganizationRoleModelAttributes"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta"
          }
        }
      },
      "OrganizationRolesShowResponseAttributes": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/OrganizationRoleModelAttributes"
          }
        }
      },
      "OrganizationRoleCreateModelAttributes": {
        "type": "object",
        "description": "Flat attributes-adapter request body (Content-Type application/json). Attribute fields and relationships live directly under `data` (no nested `attributes` / `relationships` envelopes). Each relationship is a resource identifier object ({ id, type }). Equivalent JSON:API (nested) shape: `OrganizationRoleCreateModel` sent as application/vnd.api+json.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "permission": {
                "type": "string",
                "enum": [
                  "owner",
                  "admin",
                  "manager",
                  "contractor",
                  "viewer"
                ]
              },
              "status": {
                "type": "string",
                "enum": [
                  "pending",
                  "accepted",
                  "declined"
                ],
                "nullable": true
              },
              "invited_email": {
                "type": "string",
                "nullable": true,
                "description": "Invite by email when no existing user is linked. One of user / invited_email / invited_mobile identifies the invitee."
              },
              "invited_mobile": {
                "type": "string",
                "nullable": true,
                "description": "Invite by mobile when no existing user is linked."
              },
              "organization": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "string",
                    "nullable": true
                  },
                  "type": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "organization"
                    ]
                  }
                }
              },
              "user": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "string",
                    "nullable": true
                  },
                  "type": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "user"
                    ]
                  }
                }
              }
            },
            "required": [
              "organization"
            ]
          }
        },
        "required": [
          "data"
        ]
      },
      "OrganizationRoleUpdateModelAttributes": {
        "type": "object",
        "description": "Flat attributes-adapter request body (Content-Type application/json). Attribute fields and relationships live directly under `data` (no nested `attributes` / `relationships` envelopes). Each relationship is a resource identifier object ({ id, type }). Equivalent JSON:API (nested) shape: `OrganizationRoleUpdateModel` sent as application/vnd.api+json.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "permission": {
                "type": "string",
                "enum": [
                  "owner",
                  "admin",
                  "manager",
                  "contractor",
                  "viewer"
                ]
              },
              "status": {
                "type": "string",
                "enum": [
                  "pending",
                  "accepted",
                  "declined"
                ],
                "nullable": true
              },
              "organization": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "string",
                    "nullable": true
                  },
                  "type": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "organization"
                    ]
                  }
                }
              },
              "user": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "string",
                    "nullable": true
                  },
                  "type": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "user"
                    ]
                  }
                }
              }
            },
            "required": [
              "organization",
              "user"
            ]
          }
        },
        "required": [
          "data"
        ]
      },
      "InviteResponse": {
        "type": "object",
        "description": "Public preview of a pending team invite (`GET /invites/{token}`). Plain\nJSON (not JSON:API) — the same shape is returned for both Accept types.\nThe invited identifier is always masked; the full mobile/email is never\nexposed.\n",
        "properties": {
          "invite": {
            "type": "object",
            "properties": {
              "status": {
                "type": "string",
                "enum": [
                  "valid",
                  "expired"
                ],
                "description": "Whether the invite can still be accepted (expires 7 days after sending)."
              },
              "organization_name": {
                "type": "string"
              },
              "organization_avatar_url": {
                "type": "string",
                "nullable": true
              },
              "inviter_name": {
                "type": "string",
                "description": "The organization owner's name; falls back to the organization name."
              },
              "masked_identifier": {
                "type": "string",
                "description": "Masked invited identifier, e.g. \"+61 427 xxx 210\" or \"j•••@farm.com.au\".",
                "example": "+61 427 xxx 210"
              },
              "channel": {
                "type": "string",
                "enum": [
                  "sms",
                  "email"
                ],
                "description": "How the invite was delivered."
              }
            },
            "required": [
              "status",
              "organization_name",
              "inviter_name",
              "masked_identifier",
              "channel"
            ]
          }
        },
        "required": [
          "invite"
        ]
      },
      "TaskModel": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string"
          },
          "attributes": {
            "type": "object",
            "properties": {
              "status": {
                "type": "string",
                "description": "`pending`, `cancelled` or `completed`. A treatment after the fact is `completed` with `created_as: record`. A planned treatment is `pending` with `created_as: task`.",
                "enum": [
                  "pending",
                  "cancelled",
                  "completed"
                ]
              },
              "completed_at": {
                "type": "string",
                "format": "date-time",
                "description": "When the task was completed. The server stamps it as status becomes completed and clears it when the task leaves completed, unless the client sent an explicit value — that wins, including a back-dated time. Stored and returned in UTC: `2025-12-23T09:00:00+11:00` and `2025-12-22T22:00:00Z` are the same moment. AEDT is +11:00 (first Sunday of October to first Sunday of April); AEST is +10:00. `createTreatmentRecord` stores midday in the application time zone from a `YYYY-MM-DD` date.",
                "nullable": true
              },
              "created_at": {
                "type": "string",
                "format": "date-time"
              },
              "updated_at": {
                "type": "string",
                "format": "date-time"
              },
              "description": {
                "type": "string",
                "nullable": true
              },
              "tags": {
                "type": "array",
                "description": "Hashtag-style labels (they work like Twitter hashtags) — free-form lowercase tokens stored without a leading hash, e.g. [\"slashing\", \"north-paddock\"]. Reusing a tag groups records together, and the /tasks/search and /services/search term parameter matches tags case-insensitively, so a tag aggregates everything labelled with it. A drench may carry `drench`, one lowercase-hyphenated tag per product, `whp-until-YYYY-MM-DD`, `esi-until-YYYY-MM-DD`, `needs-review` when something is unconfirmed, and `import-<source-id>` to group one imported sheet. Withholding dates come from the product days, not from tags. Always an array — the API never returns null.",
                "items": {
                  "type": "string"
                }
              },
              "image_url": {
                "type": "string",
                "nullable": true
              },
              "image_thumb_url": {
                "type": "string",
                "nullable": true
              },
              "image_ids": {
                "type": "array",
                "description": "Attachment ids for the photo gallery, aligned index-for-index with image_urls. Use one as the image_id in DELETE /tasks/{id}/images/{image_id}.",
                "items": {
                  "type": "string"
                },
                "nullable": true
              },
              "image_urls": {
                "type": "array",
                "description": "The photo gallery, oldest first — the first entry is the cover (also exposed as the singular image_url).",
                "items": {
                  "type": "string"
                },
                "nullable": true
              },
              "image_thumb_urls": {
                "type": "array",
                "description": "Thumbnail variants of image_urls, same order.",
                "items": {
                  "type": "string"
                },
                "nullable": true
              },
              "images": {
                "type": "array",
                "nullable": true,
                "description": "The gallery, oldest first. Position 0 is the cover (`image_url`). Same order as `image_urls` and `image_ids`.",
                "items": {
                  "$ref": "#/components/schemas/TaskImage"
                }
              },
              "url": {
                "type": "string",
                "nullable": true
              },
              "service_id": {
                "type": "string",
                "nullable": true
              },
              "share_scope": {
                "type": "string",
                "enum": [
                  "private",
                  "broadcast"
                ],
                "description": "Who can see the task — `private` (the owner's board only) or `broadcast` (shared with local contractors). Write it through `broadcast`. An API or MCP client that sends `broadcast` or `share_scope` is refused with 403 and nothing is written.",
                "nullable": true
              },
              "broadcast": {
                "type": "boolean",
                "description": "Whether the task is shared with local contractors (`share_scope` is `broadcast`). Writable on create and update — send `true` to share, `false` to un-share; sharing is free."
              },
              "broadcast_payment_status": {
                "type": "string",
                "description": "Legacy, read-only — sharing is free and no current operation writes this field.",
                "nullable": true
              },
              "broadcast_paid_at": {
                "type": "string",
                "format": "date-time",
                "description": "Legacy, read-only — see broadcast_payment_status.",
                "nullable": true
              },
              "fulfillment_status": {
                "type": "string",
                "nullable": true
              },
              "position": {
                "type": "integer",
                "nullable": true,
                "description": "Order within the sibling set (or within the top level). Only meaningful relative to siblings; reordering rewrites it and must never touch status or completed_at."
              },
              "sequenced": {
                "type": "boolean",
                "description": "On a parent, its children run in order. The UI numbers them and marks later steps \"after step N\"; completing out of sequence stays allowed, so this drives presentation only."
              },
              "progress": {
                "type": "object",
                "nullable": true,
                "description": "Derived roll-up over the children — never stored. Null when the task has no children, so a plain task is distinguishable from a campaign with nothing done yet.",
                "properties": {
                  "done": {
                    "type": "integer"
                  },
                  "total": {
                    "type": "integer"
                  }
                }
              },
              "operation_type": {
                "type": "string",
                "description": "What kind of work this is — one of `ai`, `spray`, `treatment`, `husbandry`, `fertiliser`, `drench`, `spread`, `sow`, `fieldwork`, `harvest`, `move_stock`, `shear`, `crutch`, `feed`, `fence`, `slash`, `observation`. `ai` is a PurpleGrain AI record: the farmer's own words (and photos) queued for the formatter, with no typed child and possibly no asset (see `ai_format_done_at`). Create one with `POST /tasks` and `operation_type: ai` from the PurpleGrain app or an API key (an MCP client is refused with 403: an assistant knows the type and writes the typed record itself); when the formatter finishes it replaces `ai` with the real type and writes the typed child, or clears the type and leaves a plain task if it gives up. `treatment` is an animal treatment on one mob. Save it with `createTreatmentRecord` and the appropriate `category` (`drench` for a drench, `vaccine` for a vaccination), which stores the products and derives the withholding dates. `drench` remains a deprecated operation type. `spray` is a treatment of one paddock (herbicide, pesticide, fungicide): save it with `createSprayRecord`, which stores the products and derives the graze and harvest dates. Deliberately a free string with no enum: the vocabulary is still moving, and a schema stricter than the model rejects valid traffic the day request validation is enforced — before authentication runs.",
                "nullable": true
              },
              "capture_source": {
                "type": "string",
                "description": "How the entry was captured — `typed`, `voice`, `agent` or `import`. `form` is accepted and stored as `typed`. A PurpleGrain AI record sends `voice` or `typed`. Agents send `agent`. A photographed or uploaded sheet is `import`. `createTreatmentRecord` sets `agent` itself. Free string with no enum, for the same reason as operation_type.",
                "nullable": true
              },
              "operator": {
                "type": "string",
                "description": "Who treated or did the work, as a display name or initials (for example \"AH\"). Free text: a name on the diary entry, not a link to a user. `createTreatmentRecord` sets this from the signed-in user and does not take it in the body; send it here when someone else treated.",
                "nullable": true
              },
              "record_summary": {
                "type": "string",
                "description": "The composed line a completed row shows on the timeline, for example \"Drenched Rams (11 hd): Startect 20 mL/hd. WHP until 6 Jan 2026, ESI until 20 Jan 2026\". Distinct from `description`, which stays the authored task text. `createTreatmentRecord` leaves this empty; the withholding dates are `treatment.safe_for_slaughter_on` and `treatment.export_eligible_on`.",
                "nullable": true
              },
              "record_notes": {
                "type": "string",
                "description": "The farmer's own words from the capture, unedited, rendered under the summary when the two differ. Product, batch, dose, WHP and ESI belong on `treatment.products` — that is what the withholding dates are calculated from. Expiry and route are fields of the treatment (`treatment.route`, each product's `expiry`); leave them unset rather than inventing either. Never invent a batch, expiry, WHP or ESI.",
                "nullable": true
              },
              "drench": {
                "description": "The typed child of a record: head, products and the two derived dates. Null for a plain task. Never stored dates: they stay null while the task is open and until a named product has a withholding period (DRENCH_RECORD.md).",
                "nullable": true,
                "allOf": [
                  {
                    "$ref": "#/components/schemas/DrenchRead"
                  }
                ]
              },
              "spray": {
                "description": "The second typed child of a record: a spray on a paddock — area, products and the two derived dates. Null for a plain task or a task whose typed child is a drench; a task carries at most one of `drench` and `spray`. Never stored dates: they stay null while the task is open and until a named product has a withholding period (SPRAY_RECORD.md).",
                "nullable": true,
                "allOf": [
                  {
                    "$ref": "#/components/schemas/SprayRead"
                  }
                ]
              },
              "treatment": {
                "description": "The typed treatment of a record: a product given to a mob (any category; a drench is category drench). Null unless the task is a treatment record; a task carries at most one typed record. Derived dates are never stored.",
                "nullable": true,
                "allOf": [
                  {
                    "$ref": "#/components/schemas/TreatmentRead"
                  }
                ]
              },
              "husbandry": {
                "description": "The typed husbandry of a record: something done to a mob: marking, shearing, weaning, scanning and so on. Null unless the task is a husbandry record; a task carries at most one typed record. Derived dates are never stored.",
                "nullable": true,
                "allOf": [
                  {
                    "$ref": "#/components/schemas/HusbandryRead"
                  }
                ]
              },
              "fertiliser": {
                "description": "The typed fertiliser of a record: fertiliser spread on a paddock (no withholding, never changes the paddock's status). Null unless the task is a fertiliser record; a task carries at most one typed record. Derived dates are never stored.",
                "nullable": true,
                "allOf": [
                  {
                    "$ref": "#/components/schemas/FertiliserRead"
                  }
                ]
              },
              "sowing": {
                "description": "The typed sowing of a record: a crop sown on a paddock (a seed treatment's grazing interval sets the paddock's grazing status; completing it sets the paddock's current crop). Null unless the task is a sowing record; a task carries at most one typed record. Derived dates are never stored.",
                "nullable": true,
                "allOf": [
                  {
                    "$ref": "#/components/schemas/SowingRead"
                  }
                ]
              },
              "fieldwork": {
                "description": "The typed fieldwork of a record: work done on a paddock: cultivation, harvest, hay or silage, slashing, rolling, burning, irrigation. Null unless the task is a fieldwork record; a task carries at most one typed record. Derived dates are never stored.",
                "nullable": true,
                "allOf": [
                  {
                    "$ref": "#/components/schemas/FieldworkRead"
                  }
                ]
              },
              "warnings": {
                "type": "array",
                "nullable": true,
                "description": "Field work only: what the paddock was held back from on the day of the work (a harvest before a harvest withholding period ends, hay or silage before a grazing one does). Null for any other task, an empty list when there is nothing to say. A warning never blocks the write.",
                "items": {
                  "type": "object",
                  "properties": {
                    "code": {
                      "type": "string",
                      "enum": [
                        "harvest_before_withholding_ends",
                        "hay_before_grazing_withholding_ends"
                      ]
                    },
                    "message": {
                      "type": "string"
                    },
                    "ends_on": {
                      "type": "string",
                      "format": "date"
                    },
                    "products": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "created_as": {
                "type": "string",
                "enum": [
                  "task",
                  "record"
                ],
                "description": "`task` when the row was created open and completed later, `record` when it was captured already done. A treatment after the fact is `status: completed` and `created_as: record`. A planned treatment is `status: pending` and `created_as: task`, and is completed later. `createTreatmentRecord` sets both from the date: a future date is the open task. Set once at creation and ignored on update. `can_reopen` is true only when this is `task`.",
                "nullable": true
              },
              "can_reopen": {
                "type": "boolean",
                "description": "Read-only. Whether the Reopen affordance applies — true exactly when `created_as` is `task`. A record that was born done has no open task to reopen to."
              },
              "ai_format_done_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "Read-only. When PurpleGrain AI finished with this task. Null while it is queued (and for every task that never was an AI record). Set once: a task with this set is never formatted again, and editing it does not re-queue it."
              },
              "ai_written_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "Read-only. When PurpleGrain AI first wrote this task's typed details. Null until it has, and for every task it never formatted. Set once: a re-run rewrites the details but does not move it. The app shows \"Details written by PurpleGrain AI 3 Sep\" and offers Re-run from it."
              },
              "ai_queued": {
                "type": "boolean",
                "description": "Read-only. Whether PurpleGrain AI holds this task right now: a record saved as `ai` that has no typed child yet, or any task sent (back) with `rerun_ai` and not finished. Read this, not `operation_type`, for the queued state: after a re-run the type and the typed details stay while the task waits."
              },
              "ai_format_meta": {
                "type": "object",
                "additionalProperties": true,
                "description": "Read-only. PurpleGrain AI's audit handle for this task, `{}` until the formatter has run: `outcome` (`clean`, `ambiguous`, `unplaced`, `needs_input`, `not_a_record`, `skipped`, `retrying` or `gave_up`), `model`, `input_tokens`, `output_tokens`, `latency_ms`, `prompt_hash`, `questions` (what the farmer must supply), `candidates` (the mobs or paddocks the words could mean, `{kind, id, name}`, when the asset was left unassigned), and `unconfirmed` (the product fields, as `products.0.batch`, that the farmer's words and photos do not support: stored, never confirmed). The model's raw response is not served."
              },
              "from_paddock_id": {
                "type": "string",
                "description": "Where a mob was before this record moved it, when it was anywhere. Null on a first placement.",
                "nullable": true
              },
              "to_paddock_id": {
                "type": "string",
                "description": "Where a mob went. Set by POST /mobs/{id}/move, and by completing a task about a mob whose text carries a `#Paddock` hashtag. A mob's current paddock is derived from this column on its most recent completed record — never from `operation_type`.",
                "nullable": true
              }
            }
          },
          "relationships": {
            "type": "object",
            "properties": {
              "asset": {
                "type": "object",
                "description": "The paddock or mob this work is about. One asset per row: a record never spans two. To record a drench on three mobs, create three records. Linkage is `{ id, type }` with type `paddock` or `mob`; the row itself is embedded only with `include=asset`.",
                "properties": {
                  "data": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "type": {
                        "type": "string"
                      }
                    },
                    "nullable": true
                  }
                }
              },
              "parent": {
                "type": "object",
                "description": "The task this one is a step of. One level only — a parent never has a parent of its own; the API rejects a third level. A multi-product drench is one record with several products, not a child task per product."
              },
              "children": {
                "type": "object",
                "description": "Its sub-tasks in board order. Linkage is always present; the rows are embedded only with `include=children`.",
                "properties": {
                  "data": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "type": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              },
              "farm": {
                "type": "object",
                "description": "The property this task belongs to, when set. For a drench this is the mob's property. `createTreatmentRecord` sets it from the mob.",
                "properties": {
                  "data": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "type": {
                        "type": "string"
                      }
                    },
                    "nullable": true
                  }
                }
              },
              "producer": {
                "type": "object",
                "description": "Who the record is for — a user or an organization. For a property owned by a business, createTask must set this to that organization (`type: organization`). Omitting it returns 403 Forbidden, detail \"Unauthorized\", with source.pointer `/request/headers/authorization`. That is a missing producer, not a bad token. A 403 whose code is `insufficient_access` is a real access refusal; its detail names the business and the access that is missing. createTreatmentRecord sets the producer from the farm owner.",
                "properties": {
                  "data": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "type": {
                        "type": "string"
                      }
                    },
                    "nullable": true
                  }
                }
              },
              "provider": {
                "type": "object",
                "properties": {
                  "data": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "type": {
                        "type": "string"
                      }
                    },
                    "nullable": true
                  }
                }
              },
              "place": {
                "type": "object",
                "properties": {
                  "data": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "type": {
                        "type": "string"
                      }
                    },
                    "nullable": true
                  }
                }
              },
              "service": {
                "type": "object",
                "properties": {
                  "data": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "type": {
                        "type": "string"
                      }
                    },
                    "nullable": true
                  }
                }
              },
              "category": {
                "type": "object",
                "properties": {
                  "data": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "type": {
                        "type": "string"
                      }
                    },
                    "nullable": true
                  }
                }
              }
            }
          }
        }
      },
      "TaskCreateModel": {
        "type": "object",
        "description": "Nested JSON:API request body (Content-Type application/vnd.api+json). The root has a single `data` object with `type: \"task\"`, attribute fields under `data.attributes`, and associations under `data.relationships` (each a `{ data: { id, type } }` linkage). Equivalent flat shape: `TaskCreateModelAttributes` sent as application/json. The request Content-Type selects which shape is validated and deserialized — the two must match.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "task"
                ]
              },
              "attributes": {
                "type": "object",
                "properties": {
                  "status": {
                    "type": "string",
                    "description": "`pending`, `cancelled` or `completed`. A treatment after the fact is `completed` with `created_as: record`. A planned treatment is `pending` with `created_as: task`. `createTreatmentRecord` sets this from the date.",
                    "enum": [
                      "pending",
                      "cancelled",
                      "completed"
                    ],
                    "nullable": true
                  },
                  "completed_at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Can be back-dated. Send an Australian offset, for example `2025-12-23T09:00:00+11:00`. It is stored and returned in UTC, so `2025-12-22T22:00:00Z` is the same moment. Use 09:00 local when only a date is known. AEDT is +11:00 (first Sunday of October to first Sunday of April); AEST is +10:00. Otherwise the server stamps it when status becomes completed and clears it when the task leaves completed; an explicit value wins. `createTreatmentRecord` takes `date` (`YYYY-MM-DD`) and stores midday in the application time zone.",
                    "nullable": true
                  },
                  "description": {
                    "type": "string",
                    "nullable": true
                  },
                  "tags": {
                    "type": "array",
                    "description": "Hashtag-style labels (they work like Twitter hashtags) — free-form lowercase tokens stored without a leading hash, e.g. [\"slashing\", \"north-paddock\"]. Reusing a tag groups records together, and the /tasks/search and /services/search term parameter matches tags case-insensitively, so a tag aggregates everything labelled with it. Send an empty array to clear every tag (null is accepted and stored as an empty array). For a drench saved with createTask, conventional tags are `drench`, one lowercase-hyphenated tag per product, `whp-until-YYYY-MM-DD`, `esi-until-YYYY-MM-DD`, `needs-review` when something is unconfirmed, and `import-<source-id>` to group one imported sheet. createTreatmentRecord does not accept tags. Withholding dates come from the product days, not from tags.",
                    "items": {
                      "type": "string"
                    },
                    "nullable": true
                  },
                  "service_id": {
                    "type": "string",
                    "nullable": true
                  },
                  "position": {
                    "type": "integer",
                    "nullable": true,
                    "description": "Order within the sibling set. Reordering rewrites it and must never touch status or completed_at."
                  },
                  "sequenced": {
                    "type": "boolean",
                    "description": "On a parent, its children run in order."
                  },
                  "broadcast": {
                    "type": "boolean",
                    "description": "Share with local contractors — `true` sets share_scope to `broadcast`, `false` makes the task private again. Free; the legacy payment fields are never written. Paused for agents (2026-09-17) — an API or MCP client that sends this field gets 403."
                  },
                  "operation_type": {
                    "type": "string",
                    "description": "What kind of work this is — one of `ai`, `spray`, `treatment`, `husbandry`, `fertiliser`, `drench`, `spread`, `sow`, `fieldwork`, `harvest`, `move_stock`, `shear`, `crutch`, `feed`, `fence`, `slash`, `observation`. `ai` is a PurpleGrain AI record: the farmer's own words (and photos) queued for the formatter, with no typed child and possibly no asset (see `ai_format_done_at`). Create one with `POST /tasks` and `operation_type: ai` from the PurpleGrain app or an API key (an MCP client is refused with 403: an assistant knows the type and writes the typed record itself); when the formatter finishes it replaces `ai` with the real type and writes the typed child, or clears the type and leaves a plain task if it gives up. `treatment` is an animal treatment on one mob. Save it with `createTreatmentRecord` and the appropriate `category` (`drench` for a drench, `vaccine` for a vaccination), which stores the products and derives the withholding dates. `drench` remains a deprecated operation type. `spray` is a treatment of one paddock (herbicide, pesticide, fungicide): save it with `createSprayRecord`, which stores the products and derives the graze and harvest dates. Deliberately a free string with no enum: the vocabulary is still moving, and a schema stricter than the model rejects valid traffic the day request validation is enforced — before authentication runs.",
                    "nullable": true
                  },
                  "capture_source": {
                    "type": "string",
                    "description": "How the entry was captured — `typed`, `voice`, `agent` or `import`. `form` is accepted and stored as `typed`. A PurpleGrain AI record sends `voice` or `typed`. Agents send `agent`. A photographed or uploaded sheet is `import`. `createTreatmentRecord` sets `agent` itself. Free string with no enum, for the same reason as operation_type.",
                    "nullable": true
                  },
                  "operator": {
                    "type": "string",
                    "description": "Who treated or did the work, as a display name or initials (for example \"AH\"). Free text: a name on the diary entry, not a link to a user. `createTreatmentRecord` sets this from the signed-in user and does not take it in the body; send it here when someone else treated.",
                    "nullable": true
                  },
                  "record_summary": {
                    "type": "string",
                    "description": "The composed line a completed row shows on the timeline, for example \"Drenched Rams (11 hd): Startect 20 mL/hd. WHP until 6 Jan 2026, ESI until 20 Jan 2026\". Distinct from `description`, which stays the authored task text. `createTreatmentRecord` leaves this empty; the withholding dates are `treatment.safe_for_slaughter_on` and `treatment.export_eligible_on`.",
                    "nullable": true
                  },
                  "record_notes": {
                    "type": "string",
                    "description": "The farmer's own words from the capture, unedited, rendered under the summary when the two differ. Product, batch, dose, WHP and ESI belong on `treatment.products` — that is what the withholding dates are calculated from. Expiry and route are fields of the treatment (`treatment.route`, each product's `expiry`); leave them unset rather than inventing either. Never invent a batch, expiry, WHP or ESI.",
                    "nullable": true
                  },
                  "drench": {
                    "description": "The typed treatment on this task: `head` and a `products` array (name, active, batch, dose, whp_days, esi_days). Prefer `createTreatmentRecord` for a treatment on a mob — it sets the producer from the farm owner. `products` replaces the set; position is the array index. `{}` creates a blank child; null is ignored and a drench is never removed. A completed drench with no product row is refused (422, \"A drench needs at least one product\").",
                    "nullable": true,
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/DrenchWrite"
                      }
                    ]
                  },
                  "spray": {
                    "description": "The typed spray on this task (a paddock's): `area_ha` and a `products` array (name, active, batch, rate, grazing_whp_days, harvest_whp_days). Prefer `createSprayRecord` for a spray on a paddock — it sets the producer from the farm owner. `area_ha` defaults to the paddock's area. `products` replaces the set; position is the array index. `{}` creates a blank child; null is ignored and a spray is never removed. A completed spray with no product row is refused (422, \"A spray needs at least one product\"). A spray on a mob is refused (422, \"A spray record belongs to a paddock — name the paddock, or change the record type\"), and so is a spray on a task that already holds a drench, and the reverse.",
                    "nullable": true,
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/SprayWrite"
                      }
                    ]
                  },
                  "treatment": {
                    "description": "The typed treatment on this task: a product given to a mob (any category; a drench is category drench). Prefer `createTreatmentRecord`, which sets the producer from the farm owner. `products` replaces the set (at most 10). A task carries at most one typed record.",
                    "nullable": true,
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/TreatmentWrite"
                      }
                    ]
                  },
                  "husbandry": {
                    "description": "The typed husbandry on this task: something done to a mob: marking, shearing, weaning, scanning and so on. Prefer `createHusbandryRecord`, which sets the producer from the farm owner. `products` replaces the set (at most 10). A task carries at most one typed record.",
                    "nullable": true,
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/HusbandryWrite"
                      }
                    ]
                  },
                  "fertiliser": {
                    "description": "The typed fertiliser on this task: fertiliser spread on a paddock (no withholding, never changes the paddock's status). Prefer `createFertiliserRecord`, which sets the producer from the farm owner. `products` replaces the set (at most 10). A task carries at most one typed record.",
                    "nullable": true,
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/FertiliserWrite"
                      }
                    ]
                  },
                  "sowing": {
                    "description": "The typed sowing on this task: a crop sown on a paddock (a seed treatment's grazing interval sets the paddock's grazing status; completing it sets the paddock's current crop). Prefer `createSowingRecord`, which sets the producer from the farm owner. `products` replaces the set (at most 10). A task carries at most one typed record.",
                    "nullable": true,
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/SowingWrite"
                      }
                    ]
                  },
                  "fieldwork": {
                    "description": "The typed fieldwork on this task: work done on a paddock: cultivation, harvest, hay or silage, slashing, rolling, burning, irrigation. Prefer `createFieldworkRecord`, which sets the producer from the farm owner. `products` replaces the set (at most 10). A task carries at most one typed record.",
                    "nullable": true,
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/FieldworkWrite"
                      }
                    ]
                  },
                  "created_as": {
                    "type": "string",
                    "enum": [
                      "task",
                      "record"
                    ],
                    "description": "`task` when the row was created open and completed later, `record` when it was captured already done. A treatment after the fact is `status: completed` and `created_as: record`. A planned treatment is `status: pending` and `created_as: task`, and is completed later. `createTreatmentRecord` sets both from the date: a future date is the open task. Set once at creation and ignored on update. `can_reopen` is true only when this is `task`.",
                    "nullable": true
                  },
                  "image_upload_ids": {
                    "$ref": "#/components/schemas/ImageUploadIdList"
                  },
                  "image_urls": {
                    "$ref": "#/components/schemas/ImageUrlList"
                  },
                  "image_base64s": {
                    "$ref": "#/components/schemas/ImageBase64List"
                  },
                  "image_mode": {
                    "$ref": "#/components/schemas/ImageMode"
                  },
                  "remove_image_ids": {
                    "$ref": "#/components/schemas/ImageRemoveIdList"
                  },
                  "from_paddock_id": {
                    "type": "string",
                    "description": "Where a mob was before this record moved it, when it was anywhere. Null on a first placement.",
                    "nullable": true
                  },
                  "to_paddock_id": {
                    "type": "string",
                    "description": "Where a mob went. Set by POST /mobs/{id}/move, and by completing a task about a mob whose text carries a `#Paddock` hashtag. A mob's current paddock is derived from this column on its most recent completed record — never from `operation_type`.",
                    "nullable": true
                  }
                }
              },
              "relationships": {
                "type": "object",
                "properties": {
                  "asset": {
                    "type": "object",
                    "description": "The paddock or mob this work is about. One asset per row: a record never spans two. To record a drench on three mobs, create three records. Linkage is `{ id, type }` with type `paddock` or `mob`; the row itself is embedded only with `include=asset`.",
                    "properties": {
                      "data": {
                        "type": "object",
                        "nullable": true,
                        "properties": {
                          "id": {
                            "type": "string",
                            "nullable": true
                          },
                          "type": {
                            "type": "string",
                            "nullable": true,
                            "enum": [
                              "paddock",
                              "mob"
                            ]
                          }
                        }
                      }
                    }
                  },
                  "producer": {
                    "type": "object",
                    "description": "Who the record is for — a user or an organization. For a property owned by a business, createTask must set this to that organization (`type: organization`). Omitting it returns 403 Forbidden, detail \"Unauthorized\", with source.pointer `/request/headers/authorization`. That is a missing producer, not a bad token. A 403 whose code is `insufficient_access` is a real access refusal; its detail names the business and the access that is missing. createTreatmentRecord sets the producer from the farm owner.",
                    "properties": {
                      "data": {
                        "type": "object",
                        "nullable": true,
                        "properties": {
                          "id": {
                            "type": "string",
                            "nullable": true
                          },
                          "type": {
                            "type": "string",
                            "nullable": true,
                            "enum": [
                              "user",
                              "organization"
                            ]
                          }
                        }
                      }
                    },
                    "required": [
                      "data"
                    ]
                  },
                  "provider": {
                    "type": "object",
                    "properties": {
                      "data": {
                        "type": "object",
                        "nullable": true,
                        "properties": {
                          "id": {
                            "type": "string",
                            "nullable": true
                          },
                          "type": {
                            "type": "string",
                            "nullable": true,
                            "enum": [
                              "user",
                              "organization"
                            ]
                          }
                        }
                      }
                    },
                    "required": [
                      "data"
                    ]
                  },
                  "place": {
                    "type": "object",
                    "properties": {
                      "data": {
                        "type": "object",
                        "nullable": true,
                        "properties": {
                          "id": {
                            "type": "string",
                            "nullable": true
                          },
                          "type": {
                            "type": "string",
                            "nullable": true,
                            "enum": [
                              "city",
                              "address"
                            ]
                          }
                        }
                      }
                    },
                    "required": [
                      "data"
                    ]
                  },
                  "service": {
                    "type": "object",
                    "properties": {
                      "data": {
                        "type": "object",
                        "nullable": true,
                        "properties": {
                          "id": {
                            "type": "string",
                            "nullable": true
                          },
                          "type": {
                            "type": "string",
                            "nullable": true,
                            "enum": [
                              "service"
                            ]
                          }
                        }
                      }
                    },
                    "required": [
                      "data"
                    ]
                  },
                  "category": {
                    "type": "object",
                    "properties": {
                      "data": {
                        "type": "object",
                        "nullable": true,
                        "properties": {
                          "id": {
                            "type": "string",
                            "nullable": true
                          },
                          "type": {
                            "type": "string",
                            "nullable": true,
                            "enum": [
                              "category"
                            ]
                          }
                        }
                      }
                    },
                    "required": [
                      "data"
                    ]
                  },
                  "farm": {
                    "type": "object",
                    "description": "The property this task belongs to. Writable — the composer resolves a `#hashtag` to one of the acting organisation's farms and sends its id here. For a drench this is the mob's property. `createTreatmentRecord` sets it from the mob.",
                    "properties": {
                      "data": {
                        "type": "object",
                        "nullable": true,
                        "properties": {
                          "id": {
                            "type": "string",
                            "nullable": true
                          },
                          "type": {
                            "type": "string",
                            "nullable": true,
                            "enum": [
                              "farm"
                            ]
                          }
                        }
                      }
                    },
                    "required": [
                      "data"
                    ]
                  },
                  "parent": {
                    "type": "object",
                    "description": "The task this one is a step of. One level only — the API rejects a parent that is itself a sub-task. A multi-product drench is one record with several products, not a child task per product.",
                    "properties": {
                      "data": {
                        "type": "object",
                        "nullable": true,
                        "properties": {
                          "id": {
                            "type": "string",
                            "nullable": true
                          },
                          "type": {
                            "type": "string",
                            "nullable": true,
                            "enum": [
                              "task"
                            ]
                          }
                        }
                      }
                    },
                    "required": [
                      "data"
                    ]
                  }
                }
              }
            },
            "required": [
              "type",
              "attributes"
            ]
          }
        },
        "required": [
          "data"
        ]
      },
      "TaskUpdateModel": {
        "type": "object",
        "description": "Nested JSON:API request body (Content-Type application/vnd.api+json). The root has a single `data` object with `id`, `type: \"task\"`, attribute fields under `data.attributes`, and associations under `data.relationships` (each a `{ data: { id, type } }` linkage). Equivalent flat shape: `TaskUpdateModelAttributes` sent as application/json. The request Content-Type selects which shape is validated and deserialized — the two must match.",
        "properties": {
          "data": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "string",
                "nullable": true
              },
              "type": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "task"
                ]
              },
              "attributes": {
                "type": "object",
                "properties": {
                  "status": {
                    "type": "string",
                    "description": "`pending`, `cancelled` or `completed`. A treatment after the fact is `completed` with `created_as: record`. A planned treatment is `pending` with `created_as: task`. `createTreatmentRecord` sets this from the date.",
                    "enum": [
                      "pending",
                      "cancelled",
                      "completed"
                    ],
                    "nullable": true
                  },
                  "completed_at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Can be back-dated. Send an Australian offset, for example `2025-12-23T09:00:00+11:00`. It is stored and returned in UTC, so `2025-12-22T22:00:00Z` is the same moment. Use 09:00 local when only a date is known. AEDT is +11:00 (first Sunday of October to first Sunday of April); AEST is +10:00. Otherwise the server stamps it when status becomes completed and clears it when the task leaves completed; an explicit value wins. `createTreatmentRecord` takes `date` (`YYYY-MM-DD`) and stores midday in the application time zone.",
                    "nullable": true
                  },
                  "description": {
                    "type": "string",
                    "nullable": true
                  },
                  "tags": {
                    "type": "array",
                    "description": "Hashtag-style labels (they work like Twitter hashtags) — free-form lowercase tokens stored without a leading hash, e.g. [\"slashing\", \"north-paddock\"]. Reusing a tag groups records together, and the /tasks/search and /services/search term parameter matches tags case-insensitively, so a tag aggregates everything labelled with it. Send an empty array to clear every tag (null is accepted and stored as an empty array). For a drench saved with createTask, conventional tags are `drench`, one lowercase-hyphenated tag per product, `whp-until-YYYY-MM-DD`, `esi-until-YYYY-MM-DD`, `needs-review` when something is unconfirmed, and `import-<source-id>` to group one imported sheet. createTreatmentRecord does not accept tags. Withholding dates come from the product days, not from tags.",
                    "items": {
                      "type": "string"
                    },
                    "nullable": true
                  },
                  "service_id": {
                    "type": "string",
                    "nullable": true
                  },
                  "position": {
                    "type": "integer",
                    "nullable": true,
                    "description": "Order within the sibling set. Reordering rewrites it and must never touch status or completed_at."
                  },
                  "sequenced": {
                    "type": "boolean",
                    "description": "On a parent, its children run in order."
                  },
                  "broadcast": {
                    "type": "boolean",
                    "description": "Share with local contractors — `true` sets share_scope to `broadcast`, `false` makes the task private again. Free; the legacy payment fields are never written. Paused for agents (2026-09-17) — an API or MCP client that sends this field gets 403."
                  },
                  "rerun_ai": {
                    "type": "boolean",
                    "description": "Write-only, PUT only. `true` sends the task (back) to PurpleGrain AI with the words, photos and typed details saved in this same write: the done stamp is cleared, `ai_queued` turns true, and the worker rewrites the details from the words and photos. A field the model cannot see keeps the value the record holds, so anything entered by hand survives; the mob or paddock is kept unless the words name another. The last run's handle moves under `ai_format_meta.previous`. From the PurpleGrain app or an API key; an MCP client is refused with 403 (an assistant writes the details itself). Never served."
                  },
                  "operation_type": {
                    "type": "string",
                    "description": "What kind of work this is — one of `ai`, `spray`, `treatment`, `husbandry`, `fertiliser`, `drench`, `spread`, `sow`, `fieldwork`, `harvest`, `move_stock`, `shear`, `crutch`, `feed`, `fence`, `slash`, `observation`. `ai` is a PurpleGrain AI record: the farmer's own words (and photos) queued for the formatter, with no typed child and possibly no asset (see `ai_format_done_at`). Create one with `POST /tasks` and `operation_type: ai` from the PurpleGrain app or an API key (an MCP client is refused with 403: an assistant knows the type and writes the typed record itself); when the formatter finishes it replaces `ai` with the real type and writes the typed child, or clears the type and leaves a plain task if it gives up. `treatment` is an animal treatment on one mob. Save it with `createTreatmentRecord` and the appropriate `category` (`drench` for a drench, `vaccine` for a vaccination), which stores the products and derives the withholding dates. `drench` remains a deprecated operation type. `spray` is a treatment of one paddock (herbicide, pesticide, fungicide): save it with `createSprayRecord`, which stores the products and derives the graze and harvest dates. Deliberately a free string with no enum: the vocabulary is still moving, and a schema stricter than the model rejects valid traffic the day request validation is enforced — before authentication runs.",
                    "nullable": true
                  },
                  "capture_source": {
                    "type": "string",
                    "description": "How the entry was captured — `typed`, `voice`, `agent` or `import`. `form` is accepted and stored as `typed`. A PurpleGrain AI record sends `voice` or `typed`. Agents send `agent`. A photographed or uploaded sheet is `import`. `createTreatmentRecord` sets `agent` itself. Free string with no enum, for the same reason as operation_type.",
                    "nullable": true
                  },
                  "operator": {
                    "type": "string",
                    "description": "Who treated or did the work, as a display name or initials (for example \"AH\"). Free text: a name on the diary entry, not a link to a user. `createTreatmentRecord` sets this from the signed-in user and does not take it in the body; send it here when someone else treated.",
                    "nullable": true
                  },
                  "record_summary": {
                    "type": "string",
                    "description": "The composed line a completed row shows on the timeline, for example \"Drenched Rams (11 hd): Startect 20 mL/hd. WHP until 6 Jan 2026, ESI until 20 Jan 2026\". Distinct from `description`, which stays the authored task text. `createTreatmentRecord` leaves this empty; the withholding dates are `treatment.safe_for_slaughter_on` and `treatment.export_eligible_on`.",
                    "nullable": true
                  },
                  "record_notes": {
                    "type": "string",
                    "description": "The farmer's own words from the capture, unedited, rendered under the summary when the two differ. Product, batch, dose, WHP and ESI belong on `treatment.products` — that is what the withholding dates are calculated from. Expiry and route are fields of the treatment (`treatment.route`, each product's `expiry`); leave them unset rather than inventing either. Never invent a batch, expiry, WHP or ESI.",
                    "nullable": true
                  },
                  "drench": {
                    "description": "The typed treatment on this task: `head` and a `products` array (name, active, batch, dose, whp_days, esi_days). Prefer `createTreatmentRecord` for a treatment on a mob — it sets the producer from the farm owner. `products` replaces the set; position is the array index. `{}` creates a blank child; null is ignored and a drench is never removed. A completed drench with no product row is refused (422, \"A drench needs at least one product\").",
                    "nullable": true,
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/DrenchWrite"
                      }
                    ]
                  },
                  "spray": {
                    "description": "The typed spray on this task (a paddock's): `area_ha` and a `products` array (name, active, batch, rate, grazing_whp_days, harvest_whp_days). Prefer `createSprayRecord` for a spray on a paddock — it sets the producer from the farm owner. `area_ha` defaults to the paddock's area. `products` replaces the set; position is the array index. `{}` creates a blank child; null is ignored and a spray is never removed. A completed spray with no product row is refused (422, \"A spray needs at least one product\"). A spray on a mob is refused (422, \"A spray record belongs to a paddock — name the paddock, or change the record type\"), and so is a spray on a task that already holds a drench, and the reverse.",
                    "nullable": true,
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/SprayWrite"
                      }
                    ]
                  },
                  "treatment": {
                    "description": "The typed treatment on this task: a product given to a mob (any category; a drench is category drench). Prefer `createTreatmentRecord`, which sets the producer from the farm owner. `products` replaces the set (at most 10). A task carries at most one typed record.",
                    "nullable": true,
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/TreatmentWrite"
                      }
                    ]
                  },
                  "husbandry": {
                    "description": "The typed husbandry on this task: something done to a mob: marking, shearing, weaning, scanning and so on. Prefer `createHusbandryRecord`, which sets the producer from the farm owner. `products` replaces the set (at most 10). A task carries at most one typed record.",
                    "nullable": true,
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/HusbandryWrite"
                      }
                    ]
                  },
                  "fertiliser": {
                    "description": "The typed fertiliser on this task: fertiliser spread on a paddock (no withholding, never changes the paddock's status). Prefer `createFertiliserRecord`, which sets the producer from the farm owner. `products` replaces the set (at most 10). A task carries at most one typed record.",
                    "nullable": true,
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/FertiliserWrite"
                      }
                    ]
                  },
                  "sowing": {
                    "description": "The typed sowing on this task: a crop sown on a paddock (a seed treatment's grazing interval sets the paddock's grazing status; completing it sets the paddock's current crop). Prefer `createSowingRecord`, which sets the producer from the farm owner. `products` replaces the set (at most 10). A task carries at most one typed record.",
                    "nullable": true,
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/SowingWrite"
                      }
                    ]
                  },
                  "fieldwork": {
                    "description": "The typed fieldwork on this task: work done on a paddock: cultivation, harvest, hay or silage, slashing, rolling, burning, irrigation. Prefer `createFieldworkRecord`, which sets the producer from the farm owner. `products` replaces the set (at most 10). A task carries at most one typed record.",
                    "nullable": true,
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/FieldworkWrite"
                      }
                    ]
                  },
                  "image_upload_ids": {
                    "$ref": "#/components/schemas/ImageUploadIdList"
                  },
                  "image_urls": {
                    "$ref": "#/components/schemas/ImageUrlList"
                  },
                  "image_base64s": {
                    "$ref": "#/components/schemas/ImageBase64List"
                  },
                  "image_mode": {
                    "$ref": "#/components/schemas/ImageMode"
                  },
                  "remove_image_ids": {
                    "$ref": "#/components/schemas/ImageRemoveIdList"
                  },
                  "from_paddock_id": {
                    "type": "string",
                    "description": "Where a mob was before this record moved it, when it was anywhere. Null on a first placement.",
                    "nullable": true
                  },
                  "to_paddock_id": {
                    "type": "string",
                    "description": "Where a mob went. Set by POST /mobs/{id}/move, and by completing a task about a mob whose text carries a `#Paddock` hashtag. A mob's current paddock is derived from this column on its most recent completed record — never from `operation_type`.",
                    "nullable": true
                  }
                }
              },
              "relationships": {
                "type": "object",
                "properties": {
                  "asset": {
                    "type": "object",
                    "description": "The paddock or mob this work is about. One asset per row: a record never spans two. To record a drench on three mobs, create three records. Linkage is `{ id, type }` with type `paddock` or `mob`; the row itself is embedded only with `include=asset`.",
                    "properties": {
                      "data": {
                        "type": "object",
                        "nullable": true,
                        "properties": {
                          "id": {
                            "type": "string",
                            "nullable": true
                          },
                          "type": {
                            "type": "string",
                            "nullable": true,
                            "enum": [
                              "paddock",
                              "mob"
                            ]
                          }
                        }
                      }
                    }
                  },
                  "producer": {
                    "type": "object",
                    "description": "Who the record is for — a user or an organization. For a property owned by a business, createTask must set this to that organization (`type: organization`). Omitting it returns 403 Forbidden, detail \"Unauthorized\", with source.pointer `/request/headers/authorization`. That is a missing producer, not a bad token. A 403 whose code is `insufficient_access` is a real access refusal; its detail names the business and the access that is missing. createTreatmentRecord sets the producer from the farm owner.",
                    "properties": {
                      "data": {
                        "type": "object",
                        "nullable": true,
                        "properties": {
                          "id": {
                            "type": "string",
                            "nullable": true
                          },
                          "type": {
                            "type": "string",
                            "nullable": true,
                            "enum": [
                              "user",
                              "organization"
                            ]
                          }
                        }
                      }
                    },
                    "required": [
                      "data"
                    ]
                  },
                  "provider": {
                    "type": "object",
                    "properties": {
                      "data": {
                        "type": "object",
                        "nullable": true,
                        "properties": {
                          "id": {
                            "type": "string",
                            "nullable": true
                          },
                          "type": {
                            "type": "string",
                            "nullable": true,
                            "enum": [
                              "user",
                              "organization"
                            ]
                          }
                        }
                      }
                    },
                    "required": [
                      "data"
                    ]
                  },
                  "place": {
                    "type": "object",
                    "properties": {
                      "data": {
                        "type": "object",
                        "nullable": true,
                        "properties": {
                          "id": {
                            "type": "string",
                            "nullable": true
                          },
                          "type": {
                            "type": "string",
                            "nullable": true,
                            "enum": [
                              "city",
                              "address"
                            ]
                          }
                        }
                      }
                    },
                    "required": [
                      "data"
                    ]
                  },
                  "service": {
                    "type": "object",
                    "properties": {
                      "data": {
                        "type": "object",
                        "nullable": true,
                        "properties": {
                          "id": {
                            "type": "string",
                            "nullable": true
                          },
                          "type": {
                            "type": "string",
                            "nullable": true,
                            "enum": [
                              "service"
                            ]
                          }
                        }
                      }
                    },
                    "required": [
                      "data"
                    ]
                  },
                  "category": {
                    "type": "object",
                    "properties": {
                      "data": {
                        "type": "object",
                        "nullable": true,
                        "properties": {
                          "id": {
                            "type": "string",
                            "nullable": true
                          },
                          "type": {
                            "type": "string",
                            "nullable": true,
                            "enum": [
                              "category"
                            ]
                          }
                        }
                      }
                    },
                    "required": [
                      "data"
                    ]
                  },
                  "farm": {
                    "type": "object",
                    "description": "The property this task belongs to. Writable — the composer resolves a `#hashtag` to one of the acting organisation's farms and sends its id here. For a drench this is the mob's property. `createTreatmentRecord` sets it from the mob.",
                    "properties": {
                      "data": {
                        "type": "object",
                        "nullable": true,
                        "properties": {
                          "id": {
                            "type": "string",
                            "nullable": true
                          },
                          "type": {
                            "type": "string",
                            "nullable": true,
                            "enum": [
                              "farm"
                            ]
                          }
                        }
                      }
                    },
                    "required": [
                      "data"
                    ]
                  },
                  "parent": {
                    "type": "object",
                    "description": "The task this one is a step of. One level only — the API rejects a parent that is itself a sub-task. A multi-product drench is one record with several products, not a child task per product.",
                    "properties": {
                      "data": {
                        "type": "object",
                        "nullable": true,
                        "properties": {
                          "id": {
                            "type": "string",
                            "nullable": true
                          },
                          "type": {
                            "type": "string",
                            "nullable": true,
                            "enum": [
                              "task"
                            ]
                          }
                        }
                      }
                    },
                    "required": [
                      "data"
                    ]
                  }
                }
              }
            },
            "required": [
              "id",
              "type",
              "attributes"
            ]
          }
        },
        "required": [
          "data"
        ]
      },
      "TasksIndexResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TaskModel"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta"
          }
        }
      },
      "TasksShowResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/TaskModel"
          }
        }
      },
      "TaskModelAttributes": {
        "type": "object",
        "description": "Task in attributes-adapter shape (Accept application/json); fields at top level.",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "example": "task"
          },
          "status": {
            "type": "string",
            "description": "`pending`, `cancelled` or `completed`. A treatment after the fact is `completed` with `created_as: record`. A planned treatment is `pending` with `created_as: task`.",
            "enum": [
              "pending",
              "cancelled",
              "completed"
            ]
          },
          "completed_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the task was completed. The server stamps it as status becomes completed and clears it when the task leaves completed, unless the client sent an explicit value — that wins, including a back-dated time. Stored and returned in UTC: `2025-12-23T09:00:00+11:00` and `2025-12-22T22:00:00Z` are the same moment. AEDT is +11:00 (first Sunday of October to first Sunday of April); AEST is +10:00. `createTreatmentRecord` stores midday in the application time zone from a `YYYY-MM-DD` date.",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "tags": {
            "type": "array",
            "description": "Hashtag-style labels (they work like Twitter hashtags) — free-form lowercase tokens stored without a leading hash, e.g. [\"slashing\", \"north-paddock\"]. Reusing a tag groups records together, and the /tasks/search and /services/search term parameter matches tags case-insensitively, so a tag aggregates everything labelled with it. A drench may carry `drench`, one lowercase-hyphenated tag per product, `whp-until-YYYY-MM-DD`, `esi-until-YYYY-MM-DD`, `needs-review` when something is unconfirmed, and `import-<source-id>` to group one imported sheet. Withholding dates come from the product days, not from tags. Always an array — the API never returns null.",
            "items": {
              "type": "string"
            }
          },
          "image_url": {
            "type": "string",
            "nullable": true
          },
          "image_thumb_url": {
            "type": "string",
            "nullable": true
          },
          "image_ids": {
            "type": "array",
            "description": "Attachment ids for the photo gallery, aligned index-for-index with image_urls. Use one as the image_id in DELETE /tasks/{id}/images/{image_id}.",
            "items": {
              "type": "string"
            },
            "nullable": true
          },
          "image_urls": {
            "type": "array",
            "description": "The photo gallery, oldest first — the first entry is the cover (also exposed as the singular image_url).",
            "items": {
              "type": "string"
            },
            "nullable": true
          },
          "image_thumb_urls": {
            "type": "array",
            "description": "Thumbnail variants of image_urls, same order.",
            "items": {
              "type": "string"
            },
            "nullable": true
          },
          "images": {
            "type": "array",
            "nullable": true,
            "description": "The gallery, oldest first. Position 0 is the cover (`image_url`). Same order as `image_urls` and `image_ids`.",
            "items": {
              "$ref": "#/components/schemas/TaskImage"
            }
          },
          "url": {
            "type": "string",
            "nullable": true
          },
          "service_id": {
            "type": "string",
            "nullable": true
          },
          "share_scope": {
            "type": "string",
            "enum": [
              "private",
              "broadcast"
            ],
            "description": "Who can see the task — `private` (the owner's board only) or `broadcast` (shared with local contractors). Write it through `broadcast`. An API or MCP client that sends `broadcast` or `share_scope` is refused with 403 and nothing is written.",
            "nullable": true
          },
          "broadcast": {
            "type": "boolean",
            "description": "Whether the task is shared with local contractors (`share_scope` is `broadcast`). Writable on create and update — send `true` to share, `false` to un-share; sharing is free."
          },
          "broadcast_payment_status": {
            "type": "string",
            "description": "Legacy, read-only — sharing is free and no current operation writes this field.",
            "nullable": true
          },
          "broadcast_paid_at": {
            "type": "string",
            "format": "date-time",
            "description": "Legacy, read-only — see broadcast_payment_status.",
            "nullable": true
          },
          "fulfillment_status": {
            "type": "string",
            "nullable": true
          },
          "position": {
            "type": "integer",
            "nullable": true,
            "description": "Order within the sibling set (or within the top level). Only meaningful relative to siblings; reordering rewrites it and must never touch status or completed_at."
          },
          "sequenced": {
            "type": "boolean",
            "description": "On a parent, its children run in order. The UI numbers them and marks later steps \"after step N\"; completing out of sequence stays allowed, so this drives presentation only."
          },
          "progress": {
            "type": "object",
            "nullable": true,
            "description": "Derived roll-up over the children — never stored. Null when the task has no children, so a plain task is distinguishable from a campaign with nothing done yet.",
            "properties": {
              "done": {
                "type": "integer"
              },
              "total": {
                "type": "integer"
              }
            }
          },
          "operation_type": {
            "type": "string",
            "description": "What kind of work this is — one of `ai`, `spray`, `treatment`, `husbandry`, `fertiliser`, `drench`, `spread`, `sow`, `fieldwork`, `harvest`, `move_stock`, `shear`, `crutch`, `feed`, `fence`, `slash`, `observation`. `ai` is a PurpleGrain AI record: the farmer's own words (and photos) queued for the formatter, with no typed child and possibly no asset (see `ai_format_done_at`). Create one with `POST /tasks` and `operation_type: ai` from the PurpleGrain app or an API key (an MCP client is refused with 403: an assistant knows the type and writes the typed record itself); when the formatter finishes it replaces `ai` with the real type and writes the typed child, or clears the type and leaves a plain task if it gives up. `treatment` is an animal treatment on one mob. Save it with `createTreatmentRecord` and the appropriate `category` (`drench` for a drench, `vaccine` for a vaccination), which stores the products and derives the withholding dates. `drench` remains a deprecated operation type. `spray` is a treatment of one paddock (herbicide, pesticide, fungicide): save it with `createSprayRecord`, which stores the products and derives the graze and harvest dates. Deliberately a free string with no enum: the vocabulary is still moving, and a schema stricter than the model rejects valid traffic the day request validation is enforced — before authentication runs.",
            "nullable": true
          },
          "capture_source": {
            "type": "string",
            "description": "How the entry was captured — `typed`, `voice`, `agent` or `import`. `form` is accepted and stored as `typed`. A PurpleGrain AI record sends `voice` or `typed`. Agents send `agent`. A photographed or uploaded sheet is `import`. `createTreatmentRecord` sets `agent` itself. Free string with no enum, for the same reason as operation_type.",
            "nullable": true
          },
          "operator": {
            "type": "string",
            "description": "Who treated or did the work, as a display name or initials (for example \"AH\"). Free text: a name on the diary entry, not a link to a user. `createTreatmentRecord` sets this from the signed-in user and does not take it in the body; send it here when someone else treated.",
            "nullable": true
          },
          "record_summary": {
            "type": "string",
            "description": "The composed line a completed row shows on the timeline, for example \"Drenched Rams (11 hd): Startect 20 mL/hd. WHP until 6 Jan 2026, ESI until 20 Jan 2026\". Distinct from `description`, which stays the authored task text. `createTreatmentRecord` leaves this empty; the withholding dates are `treatment.safe_for_slaughter_on` and `treatment.export_eligible_on`.",
            "nullable": true
          },
          "record_notes": {
            "type": "string",
            "description": "The farmer's own words from the capture, unedited, rendered under the summary when the two differ. Product, batch, dose, WHP and ESI belong on `treatment.products` — that is what the withholding dates are calculated from. Expiry and route are fields of the treatment (`treatment.route`, each product's `expiry`); leave them unset rather than inventing either. Never invent a batch, expiry, WHP or ESI.",
            "nullable": true
          },
          "drench": {
            "description": "The typed child of a record: head, products and the two derived dates. Null for a plain task. Never stored dates: they stay null while the task is open and until a named product has a withholding period (DRENCH_RECORD.md).",
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/DrenchRead"
              }
            ]
          },
          "spray": {
            "description": "The second typed child of a record: a spray on a paddock — area, products and the two derived dates. Null for a plain task or a task whose typed child is a drench; a task carries at most one of `drench` and `spray`. Never stored dates: they stay null while the task is open and until a named product has a withholding period (SPRAY_RECORD.md).",
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/SprayRead"
              }
            ]
          },
          "treatment": {
            "description": "The typed treatment of a record: a product given to a mob (any category; a drench is category drench). Null unless the task is a treatment record; a task carries at most one typed record. Derived dates are never stored.",
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/TreatmentRead"
              }
            ]
          },
          "husbandry": {
            "description": "The typed husbandry of a record: something done to a mob: marking, shearing, weaning, scanning and so on. Null unless the task is a husbandry record; a task carries at most one typed record. Derived dates are never stored.",
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/HusbandryRead"
              }
            ]
          },
          "fertiliser": {
            "description": "The typed fertiliser of a record: fertiliser spread on a paddock (no withholding, never changes the paddock's status). Null unless the task is a fertiliser record; a task carries at most one typed record. Derived dates are never stored.",
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/FertiliserRead"
              }
            ]
          },
          "sowing": {
            "description": "The typed sowing of a record: a crop sown on a paddock (a seed treatment's grazing interval sets the paddock's grazing status; completing it sets the paddock's current crop). Null unless the task is a sowing record; a task carries at most one typed record. Derived dates are never stored.",
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/SowingRead"
              }
            ]
          },
          "fieldwork": {
            "description": "The typed fieldwork of a record: work done on a paddock: cultivation, harvest, hay or silage, slashing, rolling, burning, irrigation. Null unless the task is a fieldwork record; a task carries at most one typed record. Derived dates are never stored.",
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/FieldworkRead"
              }
            ]
          },
          "warnings": {
            "type": "array",
            "nullable": true,
            "description": "Field work only: what the paddock was held back from on the day of the work (a harvest before a harvest withholding period ends, hay or silage before a grazing one does). Null for any other task, an empty list when there is nothing to say. A warning never blocks the write.",
            "items": {
              "type": "object",
              "properties": {
                "code": {
                  "type": "string",
                  "enum": [
                    "harvest_before_withholding_ends",
                    "hay_before_grazing_withholding_ends"
                  ]
                },
                "message": {
                  "type": "string"
                },
                "ends_on": {
                  "type": "string",
                  "format": "date"
                },
                "products": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              }
            }
          },
          "created_as": {
            "type": "string",
            "enum": [
              "task",
              "record"
            ],
            "description": "`task` when the row was created open and completed later, `record` when it was captured already done. A treatment after the fact is `status: completed` and `created_as: record`. A planned treatment is `status: pending` and `created_as: task`, and is completed later. `createTreatmentRecord` sets both from the date: a future date is the open task. Set once at creation and ignored on update. `can_reopen` is true only when this is `task`.",
            "nullable": true
          },
          "can_reopen": {
            "type": "boolean",
            "description": "Read-only. Whether the Reopen affordance applies — true exactly when `created_as` is `task`. A record that was born done has no open task to reopen to."
          },
          "ai_format_done_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Read-only. When PurpleGrain AI finished with this task. Null while it is queued (and for every task that never was an AI record). Set once: a task with this set is never formatted again, and editing it does not re-queue it."
          },
          "ai_written_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Read-only. When PurpleGrain AI first wrote this task's typed details. Null until it has, and for every task it never formatted. Set once: a re-run rewrites the details but does not move it. The app shows \"Details written by PurpleGrain AI 3 Sep\" and offers Re-run from it."
          },
          "ai_queued": {
            "type": "boolean",
            "description": "Read-only. Whether PurpleGrain AI holds this task right now: a record saved as `ai` that has no typed child yet, or any task sent (back) with `rerun_ai` and not finished. Read this, not `operation_type`, for the queued state: after a re-run the type and the typed details stay while the task waits."
          },
          "ai_format_meta": {
            "type": "object",
            "additionalProperties": true,
            "description": "Read-only. PurpleGrain AI's audit handle for this task, `{}` until the formatter has run: `outcome` (`clean`, `ambiguous`, `unplaced`, `needs_input`, `not_a_record`, `skipped`, `retrying` or `gave_up`), `model`, `input_tokens`, `output_tokens`, `latency_ms`, `prompt_hash`, `questions` (what the farmer must supply), `candidates` (the mobs or paddocks the words could mean, `{kind, id, name}`, when the asset was left unassigned), and `unconfirmed` (the product fields, as `products.0.batch`, that the farmer's words and photos do not support: stored, never confirmed). The model's raw response is not served."
          },
          "from_paddock_id": {
            "type": "string",
            "description": "Where a mob was before this record moved it, when it was anywhere. Null on a first placement.",
            "nullable": true
          },
          "to_paddock_id": {
            "type": "string",
            "description": "Where a mob went. Set by POST /mobs/{id}/move, and by completing a task about a mob whose text carries a `#Paddock` hashtag. A mob's current paddock is derived from this column on its most recent completed record — never from `operation_type`.",
            "nullable": true
          },
          "asset": {
            "type": "object",
            "nullable": true,
            "description": "Inlined when include=asset — the paddock or mob this work is about, as a PaddockModelAttributes or MobModelAttributes"
          },
          "producer": {
            "type": "object",
            "description": "Inlined when include=producer"
          },
          "provider": {
            "type": "object",
            "description": "Inlined when include=provider"
          },
          "place": {
            "type": "object",
            "description": "Inlined when include=place"
          },
          "service": {
            "type": "object",
            "description": "Inlined when include=service"
          },
          "category": {
            "type": "object",
            "description": "Inlined when include=category"
          },
          "farm": {
            "type": "object",
            "description": "Inlined when include=farm",
            "nullable": true
          },
          "parent": {
            "type": "object",
            "description": "Inlined when include=parent",
            "nullable": true
          },
          "children": {
            "type": "array",
            "description": "Inlined when include=children — the sub-tasks in board order, each a TaskModelAttributes",
            "items": {
              "type": "object"
            }
          }
        }
      },
      "TasksIndexResponseAttributes": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TaskModelAttributes"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta"
          }
        }
      },
      "TasksShowResponseAttributes": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/TaskModelAttributes"
          }
        }
      },
      "TaskCreateModelAttributes": {
        "type": "object",
        "description": "Flat attributes-adapter request body (Content-Type application/json). Attribute fields and relationships live directly under `data` (no nested `attributes` / `relationships` envelopes). Each relationship is a resource identifier object ({ id, type }). Equivalent JSON:API (nested) shape: `TaskCreateModel` sent as application/vnd.api+json.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "status": {
                "type": "string",
                "description": "`pending`, `cancelled` or `completed`. A treatment after the fact is `completed` with `created_as: record`. A planned treatment is `pending` with `created_as: task`. `createTreatmentRecord` sets this from the date.",
                "enum": [
                  "pending",
                  "cancelled",
                  "completed"
                ],
                "nullable": true
              },
              "completed_at": {
                "type": "string",
                "format": "date-time",
                "description": "Can be back-dated. Send an Australian offset, for example `2025-12-23T09:00:00+11:00`. It is stored and returned in UTC, so `2025-12-22T22:00:00Z` is the same moment. Use 09:00 local when only a date is known. AEDT is +11:00 (first Sunday of October to first Sunday of April); AEST is +10:00. Otherwise the server stamps it when status becomes completed and clears it when the task leaves completed; an explicit value wins. `createTreatmentRecord` takes `date` (`YYYY-MM-DD`) and stores midday in the application time zone.",
                "nullable": true
              },
              "description": {
                "type": "string",
                "nullable": true
              },
              "tags": {
                "type": "array",
                "description": "Hashtag-style labels (they work like Twitter hashtags) — free-form lowercase tokens stored without a leading hash, e.g. [\"slashing\", \"north-paddock\"]. Reusing a tag groups records together, and the /tasks/search and /services/search term parameter matches tags case-insensitively, so a tag aggregates everything labelled with it. Send an empty array to clear every tag (null is accepted and stored as an empty array). For a drench saved with createTask, conventional tags are `drench`, one lowercase-hyphenated tag per product, `whp-until-YYYY-MM-DD`, `esi-until-YYYY-MM-DD`, `needs-review` when something is unconfirmed, and `import-<source-id>` to group one imported sheet. createTreatmentRecord does not accept tags. Withholding dates come from the product days, not from tags.",
                "items": {
                  "type": "string"
                },
                "nullable": true
              },
              "service_id": {
                "type": "string",
                "nullable": true
              },
              "position": {
                "type": "integer",
                "nullable": true,
                "description": "Order within the sibling set. Reordering rewrites it and must never touch status or completed_at."
              },
              "sequenced": {
                "type": "boolean",
                "description": "On a parent, its children run in order."
              },
              "broadcast": {
                "type": "boolean",
                "description": "Share with local contractors — `true` sets share_scope to `broadcast`, `false` makes the task private again. Free; the legacy payment fields are never written. Paused for agents (2026-09-17) — an API or MCP client that sends this field gets 403."
              },
              "operation_type": {
                "type": "string",
                "description": "What kind of work this is — one of `ai`, `spray`, `treatment`, `husbandry`, `fertiliser`, `drench`, `spread`, `sow`, `fieldwork`, `harvest`, `move_stock`, `shear`, `crutch`, `feed`, `fence`, `slash`, `observation`. `ai` is a PurpleGrain AI record: the farmer's own words (and photos) queued for the formatter, with no typed child and possibly no asset (see `ai_format_done_at`). Create one with `POST /tasks` and `operation_type: ai` from the PurpleGrain app or an API key (an MCP client is refused with 403: an assistant knows the type and writes the typed record itself); when the formatter finishes it replaces `ai` with the real type and writes the typed child, or clears the type and leaves a plain task if it gives up. `treatment` is an animal treatment on one mob. Save it with `createTreatmentRecord` and the appropriate `category` (`drench` for a drench, `vaccine` for a vaccination), which stores the products and derives the withholding dates. `drench` remains a deprecated operation type. `spray` is a treatment of one paddock (herbicide, pesticide, fungicide): save it with `createSprayRecord`, which stores the products and derives the graze and harvest dates. Deliberately a free string with no enum: the vocabulary is still moving, and a schema stricter than the model rejects valid traffic the day request validation is enforced — before authentication runs.",
                "nullable": true
              },
              "capture_source": {
                "type": "string",
                "description": "How the entry was captured — `typed`, `voice`, `agent` or `import`. `form` is accepted and stored as `typed`. A PurpleGrain AI record sends `voice` or `typed`. Agents send `agent`. A photographed or uploaded sheet is `import`. `createTreatmentRecord` sets `agent` itself. Free string with no enum, for the same reason as operation_type.",
                "nullable": true
              },
              "operator": {
                "type": "string",
                "description": "Who treated or did the work, as a display name or initials (for example \"AH\"). Free text: a name on the diary entry, not a link to a user. `createTreatmentRecord` sets this from the signed-in user and does not take it in the body; send it here when someone else treated.",
                "nullable": true
              },
              "record_summary": {
                "type": "string",
                "description": "The composed line a completed row shows on the timeline, for example \"Drenched Rams (11 hd): Startect 20 mL/hd. WHP until 6 Jan 2026, ESI until 20 Jan 2026\". Distinct from `description`, which stays the authored task text. `createTreatmentRecord` leaves this empty; the withholding dates are `treatment.safe_for_slaughter_on` and `treatment.export_eligible_on`.",
                "nullable": true
              },
              "record_notes": {
                "type": "string",
                "description": "The farmer's own words from the capture, unedited, rendered under the summary when the two differ. Product, batch, dose, WHP and ESI belong on `treatment.products` — that is what the withholding dates are calculated from. Expiry and route are fields of the treatment (`treatment.route`, each product's `expiry`); leave them unset rather than inventing either. Never invent a batch, expiry, WHP or ESI.",
                "nullable": true
              },
              "drench": {
                "description": "The typed treatment on this task: `head` and a `products` array (name, active, batch, dose, whp_days, esi_days). Prefer `createTreatmentRecord` for a treatment on a mob — it sets the producer from the farm owner. `products` replaces the set; position is the array index. `{}` creates a blank child; null is ignored and a drench is never removed. A completed drench with no product row is refused (422, \"A drench needs at least one product\").",
                "nullable": true,
                "allOf": [
                  {
                    "$ref": "#/components/schemas/DrenchWrite"
                  }
                ]
              },
              "spray": {
                "description": "The typed spray on this task (a paddock's): `area_ha` and a `products` array (name, active, batch, rate, grazing_whp_days, harvest_whp_days). Prefer `createSprayRecord` for a spray on a paddock — it sets the producer from the farm owner. `area_ha` defaults to the paddock's area. `products` replaces the set; position is the array index. `{}` creates a blank child; null is ignored and a spray is never removed. A completed spray with no product row is refused (422, \"A spray needs at least one product\"). A spray on a mob is refused (422, \"A spray record belongs to a paddock — name the paddock, or change the record type\"), and so is a spray on a task that already holds a drench, and the reverse.",
                "nullable": true,
                "allOf": [
                  {
                    "$ref": "#/components/schemas/SprayWrite"
                  }
                ]
              },
              "treatment": {
                "description": "The typed treatment on this task: a product given to a mob (any category; a drench is category drench). Prefer `createTreatmentRecord`, which sets the producer from the farm owner. `products` replaces the set (at most 10). A task carries at most one typed record.",
                "nullable": true,
                "allOf": [
                  {
                    "$ref": "#/components/schemas/TreatmentWrite"
                  }
                ]
              },
              "husbandry": {
                "description": "The typed husbandry on this task: something done to a mob: marking, shearing, weaning, scanning and so on. Prefer `createHusbandryRecord`, which sets the producer from the farm owner. `products` replaces the set (at most 10). A task carries at most one typed record.",
                "nullable": true,
                "allOf": [
                  {
                    "$ref": "#/components/schemas/HusbandryWrite"
                  }
                ]
              },
              "fertiliser": {
                "description": "The typed fertiliser on this task: fertiliser spread on a paddock (no withholding, never changes the paddock's status). Prefer `createFertiliserRecord`, which sets the producer from the farm owner. `products` replaces the set (at most 10). A task carries at most one typed record.",
                "nullable": true,
                "allOf": [
                  {
                    "$ref": "#/components/schemas/FertiliserWrite"
                  }
                ]
              },
              "sowing": {
                "description": "The typed sowing on this task: a crop sown on a paddock (a seed treatment's grazing interval sets the paddock's grazing status; completing it sets the paddock's current crop). Prefer `createSowingRecord`, which sets the producer from the farm owner. `products` replaces the set (at most 10). A task carries at most one typed record.",
                "nullable": true,
                "allOf": [
                  {
                    "$ref": "#/components/schemas/SowingWrite"
                  }
                ]
              },
              "fieldwork": {
                "description": "The typed fieldwork on this task: work done on a paddock: cultivation, harvest, hay or silage, slashing, rolling, burning, irrigation. Prefer `createFieldworkRecord`, which sets the producer from the farm owner. `products` replaces the set (at most 10). A task carries at most one typed record.",
                "nullable": true,
                "allOf": [
                  {
                    "$ref": "#/components/schemas/FieldworkWrite"
                  }
                ]
              },
              "created_as": {
                "type": "string",
                "enum": [
                  "task",
                  "record"
                ],
                "description": "`task` when the row was created open and completed later, `record` when it was captured already done. A treatment after the fact is `status: completed` and `created_as: record`. A planned treatment is `status: pending` and `created_as: task`, and is completed later. `createTreatmentRecord` sets both from the date: a future date is the open task. Set once at creation and ignored on update. `can_reopen` is true only when this is `task`.",
                "nullable": true
              },
              "image_upload_ids": {
                "$ref": "#/components/schemas/ImageUploadIdList"
              },
              "image_urls": {
                "$ref": "#/components/schemas/ImageUrlList"
              },
              "image_base64s": {
                "$ref": "#/components/schemas/ImageBase64List"
              },
              "image_mode": {
                "$ref": "#/components/schemas/ImageMode"
              },
              "remove_image_ids": {
                "$ref": "#/components/schemas/ImageRemoveIdList"
              },
              "from_paddock_id": {
                "type": "string",
                "description": "Where a mob was before this record moved it, when it was anywhere. Null on a first placement.",
                "nullable": true
              },
              "to_paddock_id": {
                "type": "string",
                "description": "Where a mob went. Set by POST /mobs/{id}/move, and by completing a task about a mob whose text carries a `#Paddock` hashtag. A mob's current paddock is derived from this column on its most recent completed record — never from `operation_type`.",
                "nullable": true
              },
              "asset": {
                "type": "object",
                "nullable": true,
                "description": "The paddock or mob this work is about. One asset per row: a record never spans two. To record a drench on three mobs, create three records. Linkage is `{ id, type }` with type `paddock` or `mob`; the row itself is embedded only with `include=asset`.",
                "properties": {
                  "id": {
                    "type": "string",
                    "nullable": true
                  },
                  "type": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "paddock",
                      "mob"
                    ]
                  }
                }
              },
              "producer": {
                "type": "object",
                "nullable": true,
                "description": "Who the record is for — a user or an organization (`{ id, type }`). For a property owned by a business, createTask must set this to that organization (`type: organization`). Omitting it returns 403 Forbidden, detail \"Unauthorized\", with source.pointer `/request/headers/authorization`. That is a missing producer, not a bad token. A 403 whose code is `insufficient_access` is a real access refusal; its detail names the business and the access that is missing. createTreatmentRecord sets the producer from the farm owner.",
                "properties": {
                  "id": {
                    "type": "string",
                    "nullable": true
                  },
                  "type": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "user",
                      "organization"
                    ]
                  }
                }
              },
              "provider": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "string",
                    "nullable": true
                  },
                  "type": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "user",
                      "organization"
                    ]
                  }
                }
              },
              "place": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "string",
                    "nullable": true
                  },
                  "type": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "city",
                      "address"
                    ]
                  }
                }
              },
              "service": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "string",
                    "nullable": true
                  },
                  "type": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "service"
                    ]
                  }
                }
              },
              "category": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "string",
                    "nullable": true
                  },
                  "type": {
                    "type": "string",
                    "nullable": true
                  }
                }
              },
              "farm": {
                "type": "object",
                "nullable": true,
                "description": "The property this task belongs to. Writable — the composer resolves a `#hashtag` to one of the acting organisation's farms and sends its id here. For a drench this is the mob's property. `createTreatmentRecord` sets it from the mob.",
                "properties": {
                  "id": {
                    "type": "string",
                    "nullable": true
                  },
                  "type": {
                    "type": "string",
                    "nullable": true
                  }
                }
              },
              "parent": {
                "type": "object",
                "nullable": true,
                "description": "The task this one is a step of. One level only — the API rejects a parent that is itself a sub-task. A multi-product drench is one record with several products, not a child task per product.",
                "properties": {
                  "id": {
                    "type": "string",
                    "nullable": true
                  },
                  "type": {
                    "type": "string",
                    "nullable": true
                  }
                }
              }
            },
            "required": [
              "producer",
              "description"
            ]
          }
        },
        "required": [
          "data"
        ]
      },
      "TaskUpdateModelAttributes": {
        "type": "object",
        "description": "Flat attributes-adapter request body (Content-Type application/json). Attribute fields and relationships live directly under `data` (no nested `attributes` / `relationships` envelopes). Each relationship is a resource identifier object ({ id, type }). Equivalent JSON:API (nested) shape: `TaskUpdateModel` sent as application/vnd.api+json.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "status": {
                "type": "string",
                "description": "`pending`, `cancelled` or `completed`. A treatment after the fact is `completed` with `created_as: record`. A planned treatment is `pending` with `created_as: task`. `createTreatmentRecord` sets this from the date.",
                "enum": [
                  "pending",
                  "cancelled",
                  "completed"
                ],
                "nullable": true
              },
              "completed_at": {
                "type": "string",
                "format": "date-time",
                "description": "Can be back-dated. Send an Australian offset, for example `2025-12-23T09:00:00+11:00`. It is stored and returned in UTC, so `2025-12-22T22:00:00Z` is the same moment. Use 09:00 local when only a date is known. AEDT is +11:00 (first Sunday of October to first Sunday of April); AEST is +10:00. Otherwise the server stamps it when status becomes completed and clears it when the task leaves completed; an explicit value wins. `createTreatmentRecord` takes `date` (`YYYY-MM-DD`) and stores midday in the application time zone.",
                "nullable": true
              },
              "description": {
                "type": "string",
                "nullable": true
              },
              "tags": {
                "type": "array",
                "description": "Hashtag-style labels (they work like Twitter hashtags) — free-form lowercase tokens stored without a leading hash, e.g. [\"slashing\", \"north-paddock\"]. Reusing a tag groups records together, and the /tasks/search and /services/search term parameter matches tags case-insensitively, so a tag aggregates everything labelled with it. Send an empty array to clear every tag (null is accepted and stored as an empty array). For a drench saved with createTask, conventional tags are `drench`, one lowercase-hyphenated tag per product, `whp-until-YYYY-MM-DD`, `esi-until-YYYY-MM-DD`, `needs-review` when something is unconfirmed, and `import-<source-id>` to group one imported sheet. createTreatmentRecord does not accept tags. Withholding dates come from the product days, not from tags.",
                "items": {
                  "type": "string"
                },
                "nullable": true
              },
              "service_id": {
                "type": "string",
                "nullable": true
              },
              "position": {
                "type": "integer",
                "nullable": true,
                "description": "Order within the sibling set. Reordering rewrites it and must never touch status or completed_at."
              },
              "sequenced": {
                "type": "boolean",
                "description": "On a parent, its children run in order."
              },
              "broadcast": {
                "type": "boolean",
                "description": "Share with local contractors — `true` sets share_scope to `broadcast`, `false` makes the task private again. Free; the legacy payment fields are never written. Paused for agents (2026-09-17) — an API or MCP client that sends this field gets 403."
              },
              "rerun_ai": {
                "type": "boolean",
                "description": "Write-only, PUT only. `true` sends the task (back) to PurpleGrain AI with the words, photos and typed details saved in this same write: the done stamp is cleared, `ai_queued` turns true, and the worker rewrites the details from the words and photos. A field the model cannot see keeps the value the record holds, so anything entered by hand survives; the mob or paddock is kept unless the words name another. The last run's handle moves under `ai_format_meta.previous`. From the PurpleGrain app or an API key; an MCP client is refused with 403 (an assistant writes the details itself). Never served."
              },
              "operation_type": {
                "type": "string",
                "description": "What kind of work this is — one of `ai`, `spray`, `treatment`, `husbandry`, `fertiliser`, `drench`, `spread`, `sow`, `fieldwork`, `harvest`, `move_stock`, `shear`, `crutch`, `feed`, `fence`, `slash`, `observation`. `ai` is a PurpleGrain AI record: the farmer's own words (and photos) queued for the formatter, with no typed child and possibly no asset (see `ai_format_done_at`). Create one with `POST /tasks` and `operation_type: ai` from the PurpleGrain app or an API key (an MCP client is refused with 403: an assistant knows the type and writes the typed record itself); when the formatter finishes it replaces `ai` with the real type and writes the typed child, or clears the type and leaves a plain task if it gives up. `treatment` is an animal treatment on one mob. Save it with `createTreatmentRecord` and the appropriate `category` (`drench` for a drench, `vaccine` for a vaccination), which stores the products and derives the withholding dates. `drench` remains a deprecated operation type. `spray` is a treatment of one paddock (herbicide, pesticide, fungicide): save it with `createSprayRecord`, which stores the products and derives the graze and harvest dates. Deliberately a free string with no enum: the vocabulary is still moving, and a schema stricter than the model rejects valid traffic the day request validation is enforced — before authentication runs.",
                "nullable": true
              },
              "capture_source": {
                "type": "string",
                "description": "How the entry was captured — `typed`, `voice`, `agent` or `import`. `form` is accepted and stored as `typed`. A PurpleGrain AI record sends `voice` or `typed`. Agents send `agent`. A photographed or uploaded sheet is `import`. `createTreatmentRecord` sets `agent` itself. Free string with no enum, for the same reason as operation_type.",
                "nullable": true
              },
              "operator": {
                "type": "string",
                "description": "Who treated or did the work, as a display name or initials (for example \"AH\"). Free text: a name on the diary entry, not a link to a user. `createTreatmentRecord` sets this from the signed-in user and does not take it in the body; send it here when someone else treated.",
                "nullable": true
              },
              "record_summary": {
                "type": "string",
                "description": "The composed line a completed row shows on the timeline, for example \"Drenched Rams (11 hd): Startect 20 mL/hd. WHP until 6 Jan 2026, ESI until 20 Jan 2026\". Distinct from `description`, which stays the authored task text. `createTreatmentRecord` leaves this empty; the withholding dates are `treatment.safe_for_slaughter_on` and `treatment.export_eligible_on`.",
                "nullable": true
              },
              "record_notes": {
                "type": "string",
                "description": "The farmer's own words from the capture, unedited, rendered under the summary when the two differ. Product, batch, dose, WHP and ESI belong on `treatment.products` — that is what the withholding dates are calculated from. Expiry and route are fields of the treatment (`treatment.route`, each product's `expiry`); leave them unset rather than inventing either. Never invent a batch, expiry, WHP or ESI.",
                "nullable": true
              },
              "drench": {
                "description": "The typed treatment on this task: `head` and a `products` array (name, active, batch, dose, whp_days, esi_days). Prefer `createTreatmentRecord` for a treatment on a mob — it sets the producer from the farm owner. `products` replaces the set; position is the array index. `{}` creates a blank child; null is ignored and a drench is never removed. A completed drench with no product row is refused (422, \"A drench needs at least one product\").",
                "nullable": true,
                "allOf": [
                  {
                    "$ref": "#/components/schemas/DrenchWrite"
                  }
                ]
              },
              "spray": {
                "description": "The typed spray on this task (a paddock's): `area_ha` and a `products` array (name, active, batch, rate, grazing_whp_days, harvest_whp_days). Prefer `createSprayRecord` for a spray on a paddock — it sets the producer from the farm owner. `area_ha` defaults to the paddock's area. `products` replaces the set; position is the array index. `{}` creates a blank child; null is ignored and a spray is never removed. A completed spray with no product row is refused (422, \"A spray needs at least one product\"). A spray on a mob is refused (422, \"A spray record belongs to a paddock — name the paddock, or change the record type\"), and so is a spray on a task that already holds a drench, and the reverse.",
                "nullable": true,
                "allOf": [
                  {
                    "$ref": "#/components/schemas/SprayWrite"
                  }
                ]
              },
              "treatment": {
                "description": "The typed treatment on this task: a product given to a mob (any category; a drench is category drench). Prefer `createTreatmentRecord`, which sets the producer from the farm owner. `products` replaces the set (at most 10). A task carries at most one typed record.",
                "nullable": true,
                "allOf": [
                  {
                    "$ref": "#/components/schemas/TreatmentWrite"
                  }
                ]
              },
              "husbandry": {
                "description": "The typed husbandry on this task: something done to a mob: marking, shearing, weaning, scanning and so on. Prefer `createHusbandryRecord`, which sets the producer from the farm owner. `products` replaces the set (at most 10). A task carries at most one typed record.",
                "nullable": true,
                "allOf": [
                  {
                    "$ref": "#/components/schemas/HusbandryWrite"
                  }
                ]
              },
              "fertiliser": {
                "description": "The typed fertiliser on this task: fertiliser spread on a paddock (no withholding, never changes the paddock's status). Prefer `createFertiliserRecord`, which sets the producer from the farm owner. `products` replaces the set (at most 10). A task carries at most one typed record.",
                "nullable": true,
                "allOf": [
                  {
                    "$ref": "#/components/schemas/FertiliserWrite"
                  }
                ]
              },
              "sowing": {
                "description": "The typed sowing on this task: a crop sown on a paddock (a seed treatment's grazing interval sets the paddock's grazing status; completing it sets the paddock's current crop). Prefer `createSowingRecord`, which sets the producer from the farm owner. `products` replaces the set (at most 10). A task carries at most one typed record.",
                "nullable": true,
                "allOf": [
                  {
                    "$ref": "#/components/schemas/SowingWrite"
                  }
                ]
              },
              "fieldwork": {
                "description": "The typed fieldwork on this task: work done on a paddock: cultivation, harvest, hay or silage, slashing, rolling, burning, irrigation. Prefer `createFieldworkRecord`, which sets the producer from the farm owner. `products` replaces the set (at most 10). A task carries at most one typed record.",
                "nullable": true,
                "allOf": [
                  {
                    "$ref": "#/components/schemas/FieldworkWrite"
                  }
                ]
              },
              "image_upload_ids": {
                "$ref": "#/components/schemas/ImageUploadIdList"
              },
              "image_urls": {
                "$ref": "#/components/schemas/ImageUrlList"
              },
              "image_base64s": {
                "$ref": "#/components/schemas/ImageBase64List"
              },
              "image_mode": {
                "$ref": "#/components/schemas/ImageMode"
              },
              "remove_image_ids": {
                "$ref": "#/components/schemas/ImageRemoveIdList"
              },
              "from_paddock_id": {
                "type": "string",
                "description": "Where a mob was before this record moved it, when it was anywhere. Null on a first placement.",
                "nullable": true
              },
              "to_paddock_id": {
                "type": "string",
                "description": "Where a mob went. Set by POST /mobs/{id}/move, and by completing a task about a mob whose text carries a `#Paddock` hashtag. A mob's current paddock is derived from this column on its most recent completed record — never from `operation_type`.",
                "nullable": true
              },
              "asset": {
                "type": "object",
                "nullable": true,
                "description": "The paddock or mob this work is about. One asset per row: a record never spans two. To record a drench on three mobs, create three records. Linkage is `{ id, type }` with type `paddock` or `mob`; the row itself is embedded only with `include=asset`.",
                "properties": {
                  "id": {
                    "type": "string",
                    "nullable": true
                  },
                  "type": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "paddock",
                      "mob"
                    ]
                  }
                }
              },
              "producer": {
                "type": "object",
                "nullable": true,
                "description": "Who the record is for — a user or an organization (`{ id, type }`). For a property owned by a business, createTask must set this to that organization (`type: organization`). Omitting it returns 403 Forbidden, detail \"Unauthorized\", with source.pointer `/request/headers/authorization`. That is a missing producer, not a bad token. A 403 whose code is `insufficient_access` is a real access refusal; its detail names the business and the access that is missing. createTreatmentRecord sets the producer from the farm owner.",
                "properties": {
                  "id": {
                    "type": "string",
                    "nullable": true
                  },
                  "type": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "user",
                      "organization"
                    ]
                  }
                }
              },
              "provider": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "string",
                    "nullable": true
                  },
                  "type": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "user",
                      "organization"
                    ]
                  }
                }
              },
              "place": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "string",
                    "nullable": true
                  },
                  "type": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "city",
                      "address"
                    ]
                  }
                }
              },
              "service": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "string",
                    "nullable": true
                  },
                  "type": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "service"
                    ]
                  }
                }
              },
              "category": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "string",
                    "nullable": true
                  },
                  "type": {
                    "type": "string",
                    "nullable": true
                  }
                }
              },
              "farm": {
                "type": "object",
                "nullable": true,
                "description": "The property this task belongs to. Writable — the composer resolves a `#hashtag` to one of the acting organisation's farms and sends its id here. For a drench this is the mob's property. `createTreatmentRecord` sets it from the mob.",
                "properties": {
                  "id": {
                    "type": "string",
                    "nullable": true
                  },
                  "type": {
                    "type": "string",
                    "nullable": true
                  }
                }
              },
              "parent": {
                "type": "object",
                "nullable": true,
                "description": "The task this one is a step of. One level only — the API rejects a parent that is itself a sub-task. A multi-product drench is one record with several products, not a child task per product.",
                "properties": {
                  "id": {
                    "type": "string",
                    "nullable": true
                  },
                  "type": {
                    "type": "string",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "required": [
          "data"
        ]
      },
      "TaskReorderModel": {
        "type": "object",
        "description": "Nested JSON:API request body for POST /tasks/reorder (Content-Type application/vnd.api+json). `data.attributes.ids` is the sibling set in its new order; `data.relationships.parent` names the parent those tasks sit under, or `{ data: null }` (or omitted) for an owner's top-level tasks. Equivalent flat shape: `TaskReorderModelAttributes` sent as application/json. The request Content-Type selects which shape is validated and deserialized — the two must match.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "task"
                ]
              },
              "attributes": {
                "type": "object",
                "properties": {
                  "ids": {
                    "type": "array",
                    "description": "Task ids in their new order. The first becomes position 1.",
                    "minItems": 1,
                    "items": {
                      "type": "string"
                    }
                  }
                },
                "required": [
                  "ids"
                ]
              },
              "relationships": {
                "type": "object",
                "properties": {
                  "parent": {
                    "type": "object",
                    "description": "The parent whose sub-tasks are being reordered. Send `{ data: null }` (or omit the relationship) to reorder an owner's top-level tasks.",
                    "properties": {
                      "data": {
                        "type": "object",
                        "nullable": true,
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "type": {
                            "type": "string",
                            "enum": [
                              "task"
                            ]
                          }
                        }
                      }
                    },
                    "required": [
                      "data"
                    ]
                  }
                }
              }
            },
            "required": [
              "type",
              "attributes"
            ]
          }
        },
        "required": [
          "data"
        ]
      },
      "TaskReorderModelAttributes": {
        "type": "object",
        "description": "Flat attributes-adapter request body for POST /tasks/reorder (Content-Type application/json). `data.ids` is the sibling set in its new order; `data.parent` is a `{ id, type }` resource identifier for the parent those tasks sit under, or null (or omitted) for an owner's top-level tasks. Equivalent JSON:API (nested) shape: `TaskReorderModel` sent as application/vnd.api+json.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "ids": {
                "type": "array",
                "description": "Task ids in their new order. The first becomes position 1.",
                "minItems": 1,
                "items": {
                  "type": "string"
                }
              },
              "parent": {
                "type": "object",
                "nullable": true,
                "description": "The parent whose sub-tasks are being reordered; null or omitted for an owner's top-level tasks.",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "type": {
                    "type": "string",
                    "enum": [
                      "task"
                    ]
                  }
                }
              }
            },
            "required": [
              "ids"
            ]
          }
        },
        "required": [
          "data"
        ]
      },
      "DrenchProductRead": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "nullable": true,
            "description": "What went in the gun. Blank is valid — a drench recorded before anyone said what it was."
          },
          "active": {
            "type": "string",
            "nullable": true,
            "description": "Label active constituent, e.g. `Abamectin 1 g/L`."
          },
          "batch": {
            "type": "string",
            "nullable": true,
            "description": "Batch number from the pack. Null when it was not recorded. Never invented."
          },
          "expiry": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "description": "Expiry date on the pack, `YYYY-MM-DD`. Null when it was not recorded. Never invented."
          },
          "dose": {
            "type": "string",
            "nullable": true,
            "description": "As entered, e.g. `5 mL / 20 kg`."
          },
          "dose_value": {
            "type": "number",
            "minimum": 0,
            "nullable": true,
            "description": "The number in the dose, e.g. 5. Only what the caller sent; never parsed from `dose`."
          },
          "dose_unit": {
            "type": "string",
            "nullable": true,
            "enum": [
              "mL",
              "L",
              "g",
              "kg"
            ],
            "description": "Unit of `dose_value`."
          },
          "dose_basis": {
            "type": "string",
            "nullable": true,
            "enum": [
              "per_head",
              "per_kg",
              "per_mob"
            ],
            "description": "What `dose_value` is per."
          },
          "whp_days": {
            "type": "integer",
            "minimum": 0,
            "description": "Withholding period, days."
          },
          "esi_days": {
            "type": "integer",
            "minimum": 0,
            "description": "Export slaughter interval, days."
          },
          "position": {
            "type": "integer",
            "description": "Order the products were entered in."
          }
        }
      },
      "DrenchRead": {
        "type": "object",
        "description": "The drench of a task or record. `safe_for_slaughter_on` and `export_eligible_on` are DERIVED (never stored) from the completion date plus the longest WHP / ESI among named products, and null while the task is open or until a named product carries a withholding period.",
        "properties": {
          "id": {
            "type": "string",
            "description": "The DrenchRecord id — the same before and after the task is completed."
          },
          "head": {
            "type": "integer",
            "minimum": 0
          },
          "products": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DrenchProductRead"
            }
          },
          "safe_for_slaughter_on": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "export_eligible_on": {
            "type": "string",
            "format": "date",
            "nullable": true
          }
        }
      },
      "DrenchProductWrite": {
        "type": "object",
        "description": "One product in the gun. A combination treatment is several of these on the one record. `expiry` and the dose as `dose_value`, `dose_unit` and `dose_basis` are fields when the farmer stated them; the route is on the treatment (`createTreatmentRecord`).",
        "properties": {
          "name": {
            "type": "string",
            "nullable": true,
            "description": "Product trade name, for example \"Startect\". Blank is valid, but a blank name does not contribute to the derived withholding dates."
          },
          "active": {
            "type": "string",
            "nullable": true,
            "description": "Label active constituent, for example \"Derquantel + abamectin\"."
          },
          "batch": {
            "type": "string",
            "nullable": true,
            "description": "Batch number from the pack. Required by LPA, including when a vet treated. Never invent one; leave it unset until the farmer confirms it."
          },
          "expiry": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "description": "Expiry date on the pack, `YYYY-MM-DD`. Never invent one; unset prints `not recorded`."
          },
          "dose": {
            "type": "string",
            "nullable": true,
            "description": "Value, unit and basis, as entered, for example \"20 mL per head\" or \"1 mL / 5 kg\". Ask if the unit or the basis (per head or per kg) is missing."
          },
          "dose_value": {
            "description": "The number in the dose, e.g. 5. Only what the caller sends; it is never parsed from `dose`.",
            "oneOf": [
              {
                "type": "number",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "dose_unit": {
            "type": "string",
            "nullable": true,
            "enum": [
              "mL",
              "L",
              "g",
              "kg"
            ],
            "description": "Unit of `dose_value`."
          },
          "dose_basis": {
            "type": "string",
            "nullable": true,
            "enum": [
              "per_head",
              "per_kg",
              "per_mob"
            ],
            "description": "What `dose_value` is per."
          },
          "whp_days": {
            "description": "Meat withholding period from the label, in whole days. \"14 d\" is read as 14. Never invent it.",
            "oneOf": [
              {
                "type": "integer",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "esi_days": {
            "description": "Export slaughter interval from the label, in whole days. \"28 d\" is read as 28. If the label has no ESI, send the WHP number — the server does not substitute. Never invent it.",
            "oneOf": [
              {
                "type": "integer",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          }
        }
      },
      "DrenchWrite": {
        "type": "object",
        "description": "Writes the drench child. `head` strips separators (\"1,240\" is 1240) and defaults to the mob's current head count when a task is about a mob and none is sent. It may be less than the mob. `products` REPLACES the set (at most 10). A completed task needs at least one product row (a blank name is fine); an open task may have none. If the label has no ESI, send the WHP number as `esi_days`. Never invent a batch, WHP or ESI.",
        "properties": {
          "head": {
            "description": "A whole number of head; separators are stripped (\"1,240\" is 1240).",
            "oneOf": [
              {
                "type": "integer",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "products": {
            "type": "array",
            "maxItems": 10,
            "items": {
              "$ref": "#/components/schemas/DrenchProductWrite"
            }
          }
        }
      },
      "ServiceModel": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string"
          },
          "attributes": {
            "type": "object",
            "properties": {
              "slug": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "status": {
                "type": "string",
                "enum": [
                  "active",
                  "inactive"
                ]
              },
              "cost": {
                "type": "string",
                "nullable": true
              },
              "currency": {
                "type": "string",
                "nullable": true
              },
              "description": {
                "type": "string",
                "nullable": true
              },
              "tags": {
                "type": "array",
                "description": "Hashtag-style labels (they work like Twitter hashtags) — free-form lowercase tokens stored without a leading hash, e.g. [\"slashing\", \"north-paddock\"]. Reusing a tag groups records together, and the /tasks/search and /services/search term parameter matches tags case-insensitively, so a tag aggregates everything labelled with it. Always an array — the API never returns null.",
                "items": {
                  "type": "string",
                  "nullable": true
                }
              },
              "radius": {
                "type": "integer",
                "nullable": true
              },
              "url": {
                "type": "string",
                "nullable": false
              },
              "image_url": {
                "type": "string",
                "nullable": true
              },
              "image_thumb_url": {
                "type": "string",
                "nullable": true
              },
              "updated_at": {
                "type": "string",
                "format": "date-time"
              },
              "created_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "relationships": {
            "type": "object",
            "properties": {
              "city": {
                "type": "object",
                "properties": {
                  "data": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "type": {
                        "type": "string"
                      }
                    },
                    "nullable": true
                  }
                }
              },
              "place": {
                "type": "object",
                "properties": {
                  "data": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "type": {
                        "type": "string"
                      }
                    },
                    "nullable": true
                  }
                }
              },
              "owner": {
                "type": "object",
                "properties": {
                  "data": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "type": {
                        "type": "string"
                      }
                    },
                    "nullable": true
                  }
                }
              },
              "category": {
                "type": "object",
                "properties": {
                  "data": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "type": {
                        "type": "string"
                      }
                    },
                    "nullable": true
                  }
                }
              },
              "location": {
                "type": "object",
                "properties": {
                  "data": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "type": {
                        "type": "string"
                      }
                    },
                    "nullable": true
                  }
                }
              }
            }
          }
        }
      },
      "ServiceCreateModel": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "service"
                ]
              },
              "attributes": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "slug": {
                    "type": "string",
                    "nullable": true
                  },
                  "description": {
                    "type": "string",
                    "nullable": true
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "active",
                      "inactive"
                    ],
                    "nullable": true
                  },
                  "cost": {
                    "type": "string",
                    "nullable": true
                  },
                  "currency": {
                    "type": "string",
                    "nullable": true
                  },
                  "radius": {
                    "type": "integer",
                    "nullable": true
                  },
                  "tags": {
                    "type": "array",
                    "description": "Hashtag-style labels (they work like Twitter hashtags) — free-form lowercase tokens stored without a leading hash, e.g. [\"slashing\", \"north-paddock\"]. Reusing a tag groups records together, and the /tasks/search and /services/search term parameter matches tags case-insensitively, so a tag aggregates everything labelled with it. Send an empty array to clear every tag (null is accepted and stored as an empty array).",
                    "items": {
                      "type": "string"
                    },
                    "nullable": true
                  }
                },
                "required": [
                  "name",
                  "currency"
                ]
              },
              "relationships": {
                "type": "object",
                "properties": {
                  "owner": {
                    "type": "object",
                    "properties": {
                      "data": {
                        "type": "object",
                        "nullable": true,
                        "properties": {
                          "id": {
                            "type": "string",
                            "nullable": true
                          },
                          "type": {
                            "type": "string",
                            "nullable": true,
                            "enum": [
                              "user",
                              "organization"
                            ]
                          }
                        }
                      }
                    }
                  },
                  "place": {
                    "type": "object",
                    "properties": {
                      "data": {
                        "type": "object",
                        "nullable": true,
                        "properties": {
                          "id": {
                            "type": "string",
                            "nullable": true
                          },
                          "type": {
                            "type": "string",
                            "nullable": true,
                            "enum": [
                              "city",
                              "address"
                            ]
                          }
                        }
                      }
                    }
                  },
                  "category": {
                    "type": "object",
                    "properties": {
                      "data": {
                        "type": "object",
                        "nullable": true,
                        "properties": {
                          "id": {
                            "type": "string",
                            "nullable": true
                          },
                          "type": {
                            "type": "string",
                            "nullable": true,
                            "enum": [
                              "category"
                            ]
                          }
                        }
                      }
                    }
                  }
                }
              }
            },
            "required": [
              "type",
              "attributes"
            ]
          }
        },
        "required": [
          "data"
        ]
      },
      "ServiceUpdateModel": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "string",
                "nullable": true
              },
              "type": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "service"
                ]
              },
              "attributes": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "slug": {
                    "type": "string",
                    "nullable": true
                  },
                  "description": {
                    "type": "string",
                    "nullable": true
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "active",
                      "inactive"
                    ],
                    "nullable": true
                  },
                  "cost": {
                    "type": "string",
                    "nullable": true
                  },
                  "currency": {
                    "type": "string",
                    "nullable": true
                  },
                  "radius": {
                    "type": "integer",
                    "nullable": true
                  },
                  "tags": {
                    "type": "array",
                    "description": "Hashtag-style labels (they work like Twitter hashtags) — free-form lowercase tokens stored without a leading hash, e.g. [\"slashing\", \"north-paddock\"]. Reusing a tag groups records together, and the /tasks/search and /services/search term parameter matches tags case-insensitively, so a tag aggregates everything labelled with it. Send an empty array to clear every tag (null is accepted and stored as an empty array).",
                    "items": {
                      "type": "string"
                    },
                    "nullable": true
                  }
                }
              },
              "relationships": {
                "type": "object",
                "properties": {
                  "owner": {
                    "type": "object",
                    "properties": {
                      "data": {
                        "type": "object",
                        "nullable": true,
                        "properties": {
                          "id": {
                            "type": "string",
                            "nullable": true
                          },
                          "type": {
                            "type": "string",
                            "nullable": true,
                            "enum": [
                              "user",
                              "organization"
                            ]
                          }
                        }
                      }
                    }
                  },
                  "place": {
                    "type": "object",
                    "properties": {
                      "data": {
                        "type": "object",
                        "nullable": true,
                        "properties": {
                          "id": {
                            "type": "string",
                            "nullable": true
                          },
                          "type": {
                            "type": "string",
                            "nullable": true,
                            "enum": [
                              "city",
                              "address"
                            ]
                          }
                        }
                      }
                    }
                  },
                  "category": {
                    "type": "object",
                    "properties": {
                      "data": {
                        "type": "object",
                        "nullable": true,
                        "properties": {
                          "id": {
                            "type": "string",
                            "nullable": true
                          },
                          "type": {
                            "type": "string",
                            "nullable": true,
                            "enum": [
                              "category"
                            ]
                          }
                        }
                      }
                    }
                  }
                }
              }
            },
            "required": [
              "id",
              "type",
              "attributes"
            ]
          }
        },
        "required": [
          "data"
        ]
      },
      "ServicesIndexResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ServiceModel"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta"
          }
        }
      },
      "ServicesShowResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/ServiceModel"
          }
        }
      },
      "ServiceModelAttributes": {
        "type": "object",
        "description": "Service in attributes-adapter shape (Accept application/json); fields at top level.",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "example": "service"
          },
          "slug": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "inactive"
            ]
          },
          "cost": {
            "type": "string",
            "nullable": true
          },
          "currency": {
            "type": "string",
            "nullable": true
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "tags": {
            "type": "array",
            "description": "Hashtag-style labels (they work like Twitter hashtags) — free-form lowercase tokens stored without a leading hash, e.g. [\"slashing\", \"north-paddock\"]. Reusing a tag groups records together, and the /tasks/search and /services/search term parameter matches tags case-insensitively, so a tag aggregates everything labelled with it. Always an array — the API never returns null.",
            "items": {
              "type": "string",
              "nullable": true
            }
          },
          "radius": {
            "type": "integer",
            "nullable": true
          },
          "url": {
            "type": "string"
          },
          "image_url": {
            "type": "string",
            "nullable": true
          },
          "image_thumb_url": {
            "type": "string",
            "nullable": true
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "city": {
            "type": "object",
            "description": "Inlined when include=city"
          },
          "place": {
            "type": "object",
            "description": "Inlined when include=place"
          },
          "owner": {
            "type": "object",
            "description": "Inlined when include=owner"
          },
          "category": {
            "type": "object",
            "description": "Inlined when include=category"
          },
          "location": {
            "type": "object",
            "description": "Inlined when include=location"
          }
        }
      },
      "ServicesIndexResponseAttributes": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ServiceModelAttributes"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta"
          }
        }
      },
      "ServicesShowResponseAttributes": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/ServiceModelAttributes"
          }
        }
      },
      "ServiceCreateModelAttributes": {
        "type": "object",
        "description": "Flat attributes-adapter request body (Content-Type application/json). Attribute fields and relationships live directly under `data` (no nested `attributes` / `relationships` envelopes). Each relationship is a resource identifier object ({ id, type }). Equivalent JSON:API (nested) shape: `ServiceCreateModel` sent as application/vnd.api+json.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "slug": {
                "type": "string",
                "nullable": true
              },
              "description": {
                "type": "string",
                "nullable": true
              },
              "status": {
                "type": "string",
                "enum": [
                  "active",
                  "inactive"
                ],
                "nullable": true
              },
              "cost": {
                "type": "string",
                "nullable": true
              },
              "currency": {
                "type": "string",
                "nullable": true
              },
              "radius": {
                "type": "integer",
                "nullable": true
              },
              "tags": {
                "type": "array",
                "description": "Hashtag-style labels (they work like Twitter hashtags) — free-form lowercase tokens stored without a leading hash, e.g. [\"slashing\", \"north-paddock\"]. Reusing a tag groups records together, and the /tasks/search and /services/search term parameter matches tags case-insensitively, so a tag aggregates everything labelled with it. Send an empty array to clear every tag (null is accepted and stored as an empty array).",
                "items": {
                  "type": "string"
                },
                "nullable": true
              },
              "owner": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "string",
                    "nullable": true
                  },
                  "type": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "user",
                      "organization"
                    ]
                  }
                }
              },
              "place": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "string",
                    "nullable": true
                  },
                  "type": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "city",
                      "address"
                    ]
                  }
                }
              },
              "category": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "string",
                    "nullable": true
                  },
                  "type": {
                    "type": "string",
                    "nullable": true
                  }
                }
              }
            },
            "required": [
              "name",
              "currency"
            ]
          }
        },
        "required": [
          "data"
        ]
      },
      "ServiceUpdateModelAttributes": {
        "type": "object",
        "description": "Flat attributes-adapter request body (Content-Type application/json). Attribute fields and relationships live directly under `data` (no nested `attributes` / `relationships` envelopes). Each relationship is a resource identifier object ({ id, type }). Equivalent JSON:API (nested) shape: `ServiceUpdateModel` sent as application/vnd.api+json.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "slug": {
                "type": "string",
                "nullable": true
              },
              "description": {
                "type": "string",
                "nullable": true
              },
              "status": {
                "type": "string",
                "enum": [
                  "active",
                  "inactive"
                ],
                "nullable": true
              },
              "cost": {
                "type": "string",
                "nullable": true
              },
              "currency": {
                "type": "string",
                "nullable": true
              },
              "radius": {
                "type": "integer",
                "nullable": true
              },
              "tags": {
                "type": "array",
                "description": "Hashtag-style labels (they work like Twitter hashtags) — free-form lowercase tokens stored without a leading hash, e.g. [\"slashing\", \"north-paddock\"]. Reusing a tag groups records together, and the /tasks/search and /services/search term parameter matches tags case-insensitively, so a tag aggregates everything labelled with it. Send an empty array to clear every tag (null is accepted and stored as an empty array).",
                "items": {
                  "type": "string"
                },
                "nullable": true
              },
              "owner": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "string",
                    "nullable": true
                  },
                  "type": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "user",
                      "organization"
                    ]
                  }
                }
              },
              "place": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "string",
                    "nullable": true
                  },
                  "type": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "city",
                      "address"
                    ]
                  }
                }
              },
              "category": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "string",
                    "nullable": true
                  },
                  "type": {
                    "type": "string",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "required": [
          "data"
        ]
      },
      "SubscriptionModel": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "example": "subscription"
          },
          "attributes": {
            "type": "object",
            "properties": {
              "plan": {
                "type": "string",
                "enum": [
                  "provider"
                ]
              },
              "status": {
                "type": "string",
                "enum": [
                  "trialing",
                  "active",
                  "cancelled"
                ]
              },
              "services_limit": {
                "type": "integer",
                "description": "Maximum number of services the owner may list under this subscription."
              },
              "price_cents": {
                "type": "integer",
                "description": "Recurring price in the smallest currency unit (cents)."
              },
              "interval": {
                "type": "string",
                "example": "month"
              },
              "payment_provider": {
                "type": "string",
                "enum": [
                  "mock",
                  "stripe"
                ]
              },
              "current_period_start": {
                "type": "string",
                "format": "date-time"
              },
              "current_period_end": {
                "type": "string",
                "format": "date-time"
              },
              "trial_ends_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "started_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "cancelled_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "created_at": {
                "type": "string",
                "format": "date-time"
              },
              "updated_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "relationships": {
            "type": "object",
            "properties": {
              "subscribable": {
                "type": "object",
                "description": "Polymorphic owner of the subscription (a user or an organization).",
                "properties": {
                  "data": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "type": {
                        "type": "string",
                        "enum": [
                          "user",
                          "organization"
                        ]
                      }
                    },
                    "nullable": true
                  }
                }
              }
            }
          }
        }
      },
      "SubscriptionCreateModel": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "subscription"
                ]
              },
              "attributes": {
                "type": "object",
                "properties": {
                  "plan": {
                    "type": "string",
                    "enum": [
                      "provider"
                    ],
                    "nullable": true
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "trialing",
                      "active",
                      "cancelled"
                    ],
                    "nullable": true
                  }
                }
              },
              "relationships": {
                "type": "object",
                "properties": {
                  "subscribable": {
                    "type": "object",
                    "properties": {
                      "data": {
                        "type": "object",
                        "nullable": true,
                        "properties": {
                          "id": {
                            "type": "string",
                            "nullable": true
                          },
                          "type": {
                            "type": "string",
                            "nullable": true,
                            "enum": [
                              "user",
                              "organization"
                            ]
                          }
                        }
                      }
                    },
                    "required": [
                      "data"
                    ]
                  }
                },
                "required": [
                  "subscribable"
                ]
              }
            },
            "required": [
              "type",
              "relationships"
            ]
          }
        },
        "required": [
          "data"
        ]
      },
      "SubscriptionUpdateModel": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "string",
                "nullable": true
              },
              "type": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "subscription"
                ]
              },
              "attributes": {
                "type": "object",
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "trialing",
                      "active",
                      "cancelled"
                    ]
                  }
                }
              }
            },
            "required": [
              "id",
              "type",
              "attributes"
            ]
          }
        },
        "required": [
          "data"
        ]
      },
      "SubscriptionsIndexResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SubscriptionModel"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta"
          }
        }
      },
      "SubscriptionsShowResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/SubscriptionModel"
          }
        }
      },
      "SubscriptionModelAttributes": {
        "type": "object",
        "description": "Single subscription in attributes-adapter shape (Accept application/json).\nid, type, and attribute fields at top level; no nested attributes/relationships.\nThe subscribable relationship key may appear when include is used.\n",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "example": "subscription"
          },
          "plan": {
            "type": "string",
            "enum": [
              "provider"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "trialing",
              "active",
              "cancelled"
            ]
          },
          "services_limit": {
            "type": "integer"
          },
          "price_cents": {
            "type": "integer"
          },
          "interval": {
            "type": "string",
            "example": "month"
          },
          "payment_provider": {
            "type": "string",
            "enum": [
              "mock",
              "stripe"
            ]
          },
          "current_period_start": {
            "type": "string",
            "format": "date-time"
          },
          "current_period_end": {
            "type": "string",
            "format": "date-time"
          },
          "trial_ends_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "started_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "cancelled_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "subscribable": {
            "type": "object",
            "description": "Present when include=subscribable"
          }
        }
      },
      "SubscriptionsIndexResponseAttributes": {
        "type": "object",
        "description": "List subscriptions response for Accept application/json (attributes adapter).",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SubscriptionModelAttributes"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta"
          }
        }
      },
      "SubscriptionsShowResponseAttributes": {
        "type": "object",
        "description": "Single subscription response for Accept application/json (attributes adapter).",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/SubscriptionModelAttributes"
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta",
            "description": "Optional; typically omitted for show"
          }
        }
      },
      "SubscriptionCreateModelAttributes": {
        "type": "object",
        "description": "Flat attributes-adapter request body (Content-Type application/json). Attribute fields and relationships live directly under `data` (no nested `attributes` / `relationships` envelopes). Each relationship is a resource identifier object ({ id, type }). Equivalent JSON:API (nested) shape: `SubscriptionCreateModel` sent as application/vnd.api+json.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "plan": {
                "type": "string",
                "enum": [
                  "provider"
                ],
                "nullable": true
              },
              "status": {
                "type": "string",
                "enum": [
                  "trialing",
                  "active",
                  "cancelled"
                ],
                "nullable": true
              },
              "subscribable": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "string",
                    "nullable": true
                  },
                  "type": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "user",
                      "organization"
                    ]
                  }
                }
              }
            },
            "required": [
              "subscribable"
            ]
          }
        },
        "required": [
          "data"
        ]
      },
      "SubscriptionUpdateModelAttributes": {
        "type": "object",
        "description": "Flat attributes-adapter request body (Content-Type application/json). Attribute fields and relationships live directly under `data` (no nested `attributes` / `relationships` envelopes). Each relationship is a resource identifier object ({ id, type }). Equivalent JSON:API (nested) shape: `SubscriptionUpdateModel` sent as application/vnd.api+json.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "status": {
                "type": "string",
                "enum": [
                  "trialing",
                  "active",
                  "cancelled"
                ]
              }
            }
          }
        },
        "required": [
          "data"
        ]
      },
      "LikedContractorModel": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "example": "liked_contractor"
          },
          "attributes": {
            "type": "object",
            "properties": {
              "created_at": {
                "type": "string",
                "format": "date-time"
              },
              "updated_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "relationships": {
            "type": "object",
            "properties": {
              "owner": {
                "type": "object",
                "description": "Polymorphic owner of the like (a user or an organization).",
                "properties": {
                  "data": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "type": {
                        "type": "string",
                        "enum": [
                          "user",
                          "organization"
                        ]
                      }
                    },
                    "nullable": true
                  }
                }
              },
              "contractor": {
                "type": "object",
                "description": "The liked contractor (a user or an organization).",
                "properties": {
                  "data": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "type": {
                        "type": "string",
                        "enum": [
                          "user",
                          "organization"
                        ]
                      }
                    },
                    "nullable": true
                  }
                }
              }
            }
          }
        }
      },
      "LikedContractorCreateModel": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "liked_contractor"
                ]
              },
              "relationships": {
                "type": "object",
                "properties": {
                  "contractor": {
                    "type": "object",
                    "properties": {
                      "data": {
                        "type": "object",
                        "nullable": true,
                        "properties": {
                          "id": {
                            "type": "string",
                            "nullable": true
                          },
                          "type": {
                            "type": "string",
                            "nullable": true,
                            "enum": [
                              "user",
                              "organization"
                            ]
                          }
                        }
                      }
                    },
                    "required": [
                      "data"
                    ]
                  },
                  "owner": {
                    "type": "object",
                    "description": "The owner (contractor) who liked this contractor — a User or an Organization the caller can edit. Defaults to the current user when omitted.",
                    "properties": {
                      "data": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "type": {
                            "type": "string",
                            "enum": [
                              "user",
                              "organization"
                            ]
                          }
                        }
                      }
                    }
                  }
                },
                "required": [
                  "contractor"
                ]
              }
            },
            "required": [
              "type",
              "relationships"
            ]
          }
        },
        "required": [
          "data"
        ]
      },
      "LikedContractorsIndexResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LikedContractorModel"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta"
          }
        }
      },
      "LikedContractorsShowResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/LikedContractorModel"
          }
        }
      },
      "LikedContractorModelAttributes": {
        "type": "object",
        "description": "Single liked contractor in attributes-adapter shape (Accept application/json).\nid, type, and attribute fields at top level; no nested attributes/relationships.\nThe owner/contractor relationship keys may appear when include is used.\n",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "example": "liked_contractor"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "owner": {
            "type": "object",
            "description": "Present when include=owner"
          },
          "contractor": {
            "type": "object",
            "description": "Present when include=contractor"
          }
        }
      },
      "LikedContractorsIndexResponseAttributes": {
        "type": "object",
        "description": "List liked contractors response for Accept application/json (attributes adapter).",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LikedContractorModelAttributes"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta"
          }
        }
      },
      "LikedContractorsShowResponseAttributes": {
        "type": "object",
        "description": "Single liked contractor response for Accept application/json (attributes adapter).",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/LikedContractorModelAttributes"
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta",
            "description": "Optional; typically omitted for show"
          }
        }
      },
      "LikedContractorCreateModelAttributes": {
        "type": "object",
        "description": "Flat attributes-adapter request body (Content-Type application/json). Attribute fields and relationships live directly under `data` (no nested `attributes` / `relationships` envelopes). Each relationship is a resource identifier object ({ id, type }). Equivalent JSON:API (nested) shape: `LikedContractorCreateModel` sent as application/vnd.api+json.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "contractor": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "string",
                    "nullable": true
                  },
                  "type": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "user",
                      "organization"
                    ]
                  }
                }
              },
              "owner": {
                "type": "object",
                "description": "The owner (contractor) who liked this contractor — a User or an Organization the caller can edit. Defaults to the current user when omitted.",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "type": {
                    "type": "string",
                    "enum": [
                      "user",
                      "organization"
                    ]
                  }
                }
              }
            },
            "required": [
              "contractor"
            ]
          }
        },
        "required": [
          "data"
        ]
      },
      "ClientModel": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "example": "client"
          },
          "attributes": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "mobile": {
                "type": "string",
                "description": "E.164 Australian mobile (+614XXXXXXXX).",
                "nullable": true
              },
              "email": {
                "type": "string",
                "nullable": true
              },
              "location": {
                "type": "string",
                "description": "Free-text town label, e.g. \"Wagga Wagga, NSW\".",
                "nullable": true
              },
              "tasks_count": {
                "type": "integer",
                "description": "Derived — jobs where the owner is the provider and the linked user (or a user with the client's mobile) is the producer."
              },
              "last_task_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "Derived — created_at of the most recent matching job."
              },
              "created_at": {
                "type": "string",
                "format": "date-time"
              },
              "updated_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "relationships": {
            "type": "object",
            "properties": {
              "owner": {
                "type": "object",
                "description": "Polymorphic owner of the client record (a user or an organization).",
                "properties": {
                  "data": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "type": {
                        "type": "string",
                        "enum": [
                          "user",
                          "organization"
                        ]
                      }
                    },
                    "nullable": true
                  }
                }
              },
              "user": {
                "type": "object",
                "description": "Linked platform account, when the client is a real user.",
                "properties": {
                  "data": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "type": {
                        "type": "string",
                        "enum": [
                          "user"
                        ]
                      }
                    },
                    "nullable": true
                  }
                }
              }
            }
          }
        }
      },
      "ClientCreateModel": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "client"
                ]
              },
              "attributes": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "mobile": {
                    "type": "string",
                    "description": "Australian mobile in any common format; normalized to E.164."
                  },
                  "email": {
                    "type": "string",
                    "nullable": true
                  },
                  "location": {
                    "type": "string",
                    "nullable": true
                  }
                },
                "required": [
                  "name",
                  "mobile"
                ]
              },
              "relationships": {
                "type": "object",
                "properties": {
                  "user": {
                    "type": "object",
                    "description": "Optional link to a platform account.",
                    "properties": {
                      "data": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "type": {
                            "type": "string",
                            "enum": [
                              "user"
                            ]
                          }
                        },
                        "nullable": true
                      }
                    }
                  },
                  "owner": {
                    "type": "object",
                    "description": "The owner (contractor) of this client — a User or an Organization the caller can edit. Defaults to the current user when omitted.",
                    "properties": {
                      "data": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "type": {
                            "type": "string",
                            "enum": [
                              "user",
                              "organization"
                            ]
                          }
                        }
                      }
                    }
                  }
                }
              }
            },
            "required": [
              "type",
              "attributes"
            ]
          }
        },
        "required": [
          "data"
        ]
      },
      "ClientUpdateModel": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "type": {
                "type": "string",
                "enum": [
                  "client"
                ]
              },
              "attributes": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "mobile": {
                    "type": "string"
                  },
                  "email": {
                    "type": "string",
                    "nullable": true
                  },
                  "location": {
                    "type": "string",
                    "nullable": true
                  }
                }
              }
            }
          }
        }
      },
      "ClientsIndexResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ClientModel"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta"
          }
        }
      },
      "ClientsShowResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/ClientModel"
          }
        }
      },
      "ClientModelAttributes": {
        "type": "object",
        "description": "Single client in attributes-adapter shape (Accept application/json).\nid, type, and attribute fields at top level; no nested attributes/relationships.\nThe owner/user relationship keys may appear when include is used.\n",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "example": "client"
          },
          "name": {
            "type": "string"
          },
          "mobile": {
            "type": "string",
            "nullable": true
          },
          "email": {
            "type": "string",
            "nullable": true
          },
          "location": {
            "type": "string",
            "nullable": true
          },
          "tasks_count": {
            "type": "integer"
          },
          "last_task_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "owner": {
            "type": "object",
            "description": "Present when include=owner"
          },
          "user": {
            "type": "object",
            "description": "Present when include=user"
          }
        }
      },
      "ClientsIndexResponseAttributes": {
        "type": "object",
        "description": "List clients response for Accept application/json (attributes adapter).",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ClientModelAttributes"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta"
          }
        }
      },
      "ClientsShowResponseAttributes": {
        "type": "object",
        "description": "Single client response for Accept application/json (attributes adapter).",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/ClientModelAttributes"
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta",
            "description": "Optional; typically omitted for show"
          }
        }
      },
      "ClientCreateModelAttributes": {
        "type": "object",
        "description": "Flat attributes-adapter request body (Content-Type application/json). Attribute fields and relationships live directly under `data` (no nested `attributes` / `relationships` envelopes). Each relationship is a resource identifier object ({ id, type }). Equivalent JSON:API (nested) shape: `ClientCreateModel` sent as application/vnd.api+json.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "mobile": {
                "type": "string",
                "description": "Australian mobile in any common format; normalized to E.164."
              },
              "email": {
                "type": "string",
                "nullable": true
              },
              "location": {
                "type": "string",
                "nullable": true
              },
              "user": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "type": {
                    "type": "string",
                    "enum": [
                      "user"
                    ]
                  }
                },
                "nullable": true
              },
              "owner": {
                "type": "object",
                "description": "The owner (contractor) of this client — a User or an Organization the caller can edit. Defaults to the current user when omitted.",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "type": {
                    "type": "string",
                    "enum": [
                      "user",
                      "organization"
                    ]
                  }
                }
              }
            }
          }
        },
        "required": [
          "data"
        ]
      },
      "ClientUpdateModelAttributes": {
        "type": "object",
        "description": "Flat attributes-adapter request body (Content-Type application/json). Attribute fields and relationships live directly under `data` (no nested `attributes` / `relationships` envelopes). Each relationship is a resource identifier object ({ id, type }). Equivalent JSON:API (nested) shape: `ClientUpdateModel` sent as application/vnd.api+json.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "mobile": {
                "type": "string"
              },
              "email": {
                "type": "string",
                "nullable": true
              },
              "location": {
                "type": "string",
                "nullable": true
              }
            }
          }
        },
        "required": [
          "data"
        ]
      },
      "ContractorModel": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The provider's id (User or Organization)."
          },
          "type": {
            "type": "string",
            "example": "contractor"
          },
          "attributes": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "slug": {
                "type": "string"
              },
              "owner_type": {
                "type": "string",
                "enum": [
                  "User",
                  "Organization"
                ]
              },
              "place_label": {
                "type": "string",
                "nullable": true
              },
              "distance_km": {
                "type": "number",
                "nullable": true,
                "description": "From the search point to the provider's nearest matching service; null without lat/lon."
              },
              "categories": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "services_count": {
                "type": "integer"
              },
              "liked": {
                "type": "boolean",
                "description": "In the caller's Liked-contractors set; false when signed out."
              },
              "avatar_url": {
                "type": "string",
                "nullable": true
              },
              "avatar_thumb_url": {
                "type": "string",
                "nullable": true
              },
              "url": {
                "type": "string",
                "nullable": true
              }
            }
          }
        }
      },
      "ContractorsIndexResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ContractorModel"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta"
          }
        }
      },
      "ContractorModelAttributes": {
        "type": "object",
        "description": "Single contractor card in attributes-adapter shape (Accept application/json).",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "example": "contractor"
          },
          "name": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "owner_type": {
            "type": "string",
            "enum": [
              "User",
              "Organization"
            ]
          },
          "place_label": {
            "type": "string",
            "nullable": true
          },
          "distance_km": {
            "type": "number",
            "nullable": true
          },
          "categories": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "services_count": {
            "type": "integer"
          },
          "liked": {
            "type": "boolean"
          },
          "avatar_url": {
            "type": "string",
            "nullable": true
          },
          "avatar_thumb_url": {
            "type": "string",
            "nullable": true
          },
          "url": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "ContractorsIndexResponseAttributes": {
        "type": "object",
        "description": "Contractors directory response for Accept application/json (attributes adapter).",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ContractorModelAttributes"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta"
          }
        }
      },
      "FarmModel": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "example": "farm"
          },
          "attributes": {
            "type": "object",
            "properties": {
              "slug": {
                "type": "string",
                "description": "Unique within its parent (a property per owner; a paddock or mob per property); generated from the name and renamed with it. The old slug keeps resolving. See `resolveLink`."
              },
              "visibility": {
                "type": "string",
                "enum": [
                  "private",
                  "link",
                  "public"
                ],
                "description": "Who can see the property and, through it, its paddocks, mobs and scoped task lists. `private` (default) is members only; `link` is anyone with the share token (`?share=`); `public` is anyone and search engines."
              },
              "web_path": {
                "type": "string",
                "description": "The property's address on the web, e.g. `/organizations/hillcrest/properties/yarrabee`. Present on a property read directly."
              },
              "url": {
                "type": "string",
                "description": "The whole link to hand someone; for a property shared by link it carries `?share=<token>`. Present on a property read directly."
              },
              "can_edit": {
                "type": "boolean",
                "description": "Whether the caller may change the property. From the session's role, never the address. Present on a property read directly."
              },
              "share_token": {
                "type": "string",
                "nullable": true,
                "description": "The token behind `visibility=link`. Returned only to callers who can change who sees the property."
              },
              "name": {
                "type": "string"
              },
              "location": {
                "type": "string",
                "nullable": true,
                "description": "Free-text label, e.g. \"Wagga Wagga, NSW\"."
              },
              "size": {
                "type": "string",
                "nullable": true,
                "description": "Free-text label, e.g. \"1,200 ha\"."
              },
              "place_label": {
                "type": "string",
                "nullable": true,
                "description": "The geocoded label shown under the property name on the map, e.g. \"1180 Kywong Road, Currawarna NSW 2650\". Distinct from `location` (the settings \"Town, State\" caption) and from `size` (a free-text label)."
              },
              "centroid_lat": {
                "type": "number",
                "nullable": true,
                "description": "Latitude of the point the map opens on. Present exactly when `centroid_lon` is."
              },
              "centroid_lon": {
                "type": "number",
                "nullable": true,
                "description": "Longitude of the point the map opens on. Present exactly when `centroid_lat` is."
              },
              "boundary": {
                "type": "array",
                "nullable": true,
                "description": "The property outline: an array of rings, each ring an array of\n`[longitude, latitude]` pairs in RFC 7946 order — **longitude\nfirst** — with at least 4 positions and the first position repeated\nlast. This is NOT a GeoJSON object: a value carrying a `type` key\nis read as a relationship on the flat `application/json` path and\nits coordinates are silently discarded, so the wire format is a\nplain array on both content types. Holes are not modelled; each\npolygon part contributes its exterior ring. Stored as PostGIS\n`geography(MultiPolygon,4326)`.\n",
                "items": {
                  "type": "array",
                  "description": "One closed exterior ring.",
                  "minItems": 4,
                  "items": {
                    "type": "array",
                    "description": "[longitude, latitude]",
                    "minItems": 2,
                    "maxItems": 2,
                    "items": {
                      "type": "number"
                    }
                  }
                }
              },
              "boundary_captured_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "When the boundary was last saved. Null while the property has no ring."
              },
              "boundary_source": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "traced",
                  "grid",
                  "gps",
                  "imported"
                ],
                "description": "How the ring was captured — traced over imagery, placed on the grid with no imagery, walked with the phone's GPS, or read from a KML/shapefile. Null for a ring saved before provenance was recorded or by a caller that named no source. Written with the ring and cleared with it."
              },
              "boundary_captured_by": {
                "type": "string",
                "nullable": true,
                "description": "Id of the user who saved the ring, stamped server-side from the request's actor and never accepted from a client. Null when a machine token saved it, and null while the property has no ring."
              },
              "area_ha": {
                "type": "number",
                "nullable": true,
                "description": "Property area in hectares. Measured from the boundary when `area_source` is `drawn`; the farmer's own number when it is `typed`."
              },
              "area_source": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "typed",
                  "drawn"
                ],
                "description": "Where `area_ha` came from. A `typed` area is never overwritten by a drawn one."
              },
              "paddock_count": {
                "type": "integer",
                "description": "How many LIVE paddocks the property has — the farm tabs' \"12 paddocks · 2 mobs\", and the only way to tell that a page of `listPaddocks` truncated. Archived paddocks are not counted."
              },
              "mob_count": {
                "type": "integer",
                "description": "How many LIVE mobs the property has. Archived mobs are not counted."
              },
              "blocks": {
                "type": "array",
                "description": "The property's live blocks: labels for where paddocks sit inside it, with the member paddocks and the area and outline derived from them. Empty when every paddock is in the home block. Present on reads of a property (`getFarm`, `listFarms`, `getFarmPicture`), visitors included.",
                "items": {
                  "$ref": "#/components/schemas/PropertyBlock"
                }
              },
              "created_at": {
                "type": "string",
                "format": "date-time"
              },
              "updated_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "relationships": {
            "type": "object",
            "properties": {
              "organization": {
                "type": "object",
                "description": "The owning business; `data` is null on a personal property.",
                "properties": {
                  "data": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "type": {
                        "type": "string"
                      }
                    },
                    "nullable": true
                  }
                }
              },
              "user": {
                "type": "object",
                "description": "The person who owns a personal property; `data` is null on a business's farm.",
                "properties": {
                  "data": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "type": {
                        "type": "string"
                      }
                    },
                    "nullable": true
                  }
                }
              }
            }
          }
        }
      },
      "FarmCreateModel": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "farm"
                ]
              },
              "attributes": {
                "type": "object",
                "properties": {
                  "visibility": {
                    "type": "string",
                    "enum": [
                      "private",
                      "link",
                      "public"
                    ],
                    "description": "Who can see the property and, through it, its paddocks, mobs and scoped task lists. `private` (default) is members only; `link` is anyone with the share token (`?share=`); `public` is anyone and search engines."
                  },
                  "name": {
                    "type": "string"
                  },
                  "location": {
                    "type": "string",
                    "nullable": true
                  },
                  "size": {
                    "type": "string",
                    "nullable": true
                  },
                  "place_label": {
                    "type": "string",
                    "nullable": true,
                    "description": "The geocoded label shown under the property name on the map. Distinct from `location` (the settings \"Town, State\" caption)."
                  },
                  "centroid_lat": {
                    "type": "number",
                    "nullable": true,
                    "description": "Latitude of the point the map opens on. Send both `centroid_lat` and `centroid_lon`, or neither — a half pair is rejected. A property with no boundary still needs a point (\"Nothing on the map yet\")."
                  },
                  "centroid_lon": {
                    "type": "number",
                    "nullable": true,
                    "description": "Longitude of the point the map opens on. Send both `centroid_lat` and `centroid_lon`, or neither."
                  }
                },
                "required": [
                  "name"
                ]
              },
              "relationships": {
                "type": "object",
                "description": "Omit both, or send `organization` with null `data`, for the signed-in user's personal account.",
                "properties": {
                  "organization": {
                    "type": "object",
                    "description": "The business the property belongs to.",
                    "properties": {
                      "data": {
                        "type": "object",
                        "nullable": true,
                        "properties": {
                          "id": {
                            "type": "string",
                            "nullable": true
                          },
                          "type": {
                            "type": "string",
                            "nullable": true,
                            "enum": [
                              "organization"
                            ]
                          }
                        }
                      }
                    },
                    "required": [
                      "data"
                    ]
                  },
                  "user": {
                    "type": "object",
                    "description": "The person the property belongs to — only the signed-in user themselves.",
                    "properties": {
                      "data": {
                        "type": "object",
                        "nullable": true,
                        "properties": {
                          "id": {
                            "type": "string",
                            "nullable": true
                          },
                          "type": {
                            "type": "string",
                            "nullable": true,
                            "enum": [
                              "user"
                            ]
                          }
                        }
                      }
                    },
                    "required": [
                      "data"
                    ]
                  }
                }
              }
            },
            "required": [
              "type",
              "attributes"
            ]
          }
        },
        "required": [
          "data"
        ]
      },
      "FarmUpdateModel": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "type": {
                "type": "string",
                "enum": [
                  "farm"
                ]
              },
              "attributes": {
                "type": "object",
                "properties": {
                  "visibility": {
                    "type": "string",
                    "enum": [
                      "private",
                      "link",
                      "public"
                    ],
                    "description": "Who can see the property and, through it, its paddocks, mobs and scoped task lists. `private` (default) is members only; `link` is anyone with the share token (`?share=`); `public` is anyone and search engines."
                  },
                  "name": {
                    "type": "string"
                  },
                  "location": {
                    "type": "string",
                    "nullable": true
                  },
                  "size": {
                    "type": "string",
                    "nullable": true
                  },
                  "place_label": {
                    "type": "string",
                    "nullable": true,
                    "description": "The geocoded label shown under the property name on the map. Distinct from `location` (the settings \"Town, State\" caption)."
                  },
                  "centroid_lat": {
                    "type": "number",
                    "nullable": true,
                    "description": "Latitude of the point the map opens on. Send both `centroid_lat` and `centroid_lon`, or neither — a half pair is rejected. A property with no boundary still needs a point (\"Nothing on the map yet\")."
                  },
                  "centroid_lon": {
                    "type": "number",
                    "nullable": true,
                    "description": "Longitude of the point the map opens on. Send both `centroid_lat` and `centroid_lon`, or neither."
                  }
                }
              }
            }
          }
        }
      },
      "FarmsIndexResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FarmModel"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta"
          }
        }
      },
      "FarmsShowResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/FarmModel"
          }
        }
      },
      "FarmModelAttributes": {
        "type": "object",
        "description": "Single farm in attributes-adapter shape (Accept application/json).",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "example": "farm"
          },
          "slug": {
            "type": "string",
            "description": "Unique within its parent (a property per owner; a paddock or mob per property); generated from the name and renamed with it. The old slug keeps resolving. See `resolveLink`."
          },
          "visibility": {
            "type": "string",
            "enum": [
              "private",
              "link",
              "public"
            ],
            "description": "Who can see the property and, through it, its paddocks, mobs and scoped task lists. `private` (default) is members only; `link` is anyone with the share token (`?share=`); `public` is anyone and search engines."
          },
          "web_path": {
            "type": "string",
            "description": "The property's address on the web, e.g. `/organizations/hillcrest/properties/yarrabee`. Present on a property read directly."
          },
          "url": {
            "type": "string",
            "description": "The whole link to hand someone; for a property shared by link it carries `?share=<token>`. Present on a property read directly."
          },
          "can_edit": {
            "type": "boolean",
            "description": "Whether the caller may change the property. From the session's role, never the address. Present on a property read directly."
          },
          "share_token": {
            "type": "string",
            "nullable": true,
            "description": "The token behind `visibility=link`. Returned only to callers who can change who sees the property."
          },
          "name": {
            "type": "string"
          },
          "location": {
            "type": "string",
            "nullable": true
          },
          "size": {
            "type": "string",
            "nullable": true
          },
          "place_label": {
            "type": "string",
            "nullable": true,
            "description": "The geocoded label shown under the property name on the map, e.g. \"1180 Kywong Road, Currawarna NSW 2650\"."
          },
          "centroid_lat": {
            "type": "number",
            "nullable": true,
            "description": "Latitude of the point the map opens on. Present exactly when `centroid_lon` is."
          },
          "centroid_lon": {
            "type": "number",
            "nullable": true,
            "description": "Longitude of the point the map opens on. Present exactly when `centroid_lat` is."
          },
          "boundary": {
            "type": "array",
            "nullable": true,
            "description": "The property outline: an array of rings, each ring an array of\n`[longitude, latitude]` pairs in RFC 7946 order — **longitude first** —\nwith at least 4 positions and the first repeated last. Not a GeoJSON\nobject (see `FarmBoundaryModel`).\n",
            "items": {
              "type": "array",
              "description": "One closed exterior ring.",
              "minItems": 4,
              "items": {
                "type": "array",
                "description": "[longitude, latitude]",
                "minItems": 2,
                "maxItems": 2,
                "items": {
                  "type": "number"
                }
              }
            }
          },
          "boundary_captured_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the boundary was last saved. Null while the property has no ring."
          },
          "boundary_source": {
            "type": "string",
            "nullable": true,
            "enum": [
              "traced",
              "grid",
              "gps",
              "imported"
            ],
            "description": "How the ring was captured — traced over imagery, placed on the grid with no imagery, walked with the phone's GPS, or read from a KML/shapefile. Null for a ring saved before provenance was recorded or by a caller that named no source. Written with the ring and cleared with it."
          },
          "boundary_captured_by": {
            "type": "string",
            "nullable": true,
            "description": "Id of the user who saved the ring, stamped server-side from the request's actor and never accepted from a client. Null when a machine token saved it, and null while the property has no ring."
          },
          "area_ha": {
            "type": "number",
            "nullable": true,
            "description": "Property area in hectares. Measured from the boundary when `area_source` is `drawn`; the farmer's own number when it is `typed`."
          },
          "area_source": {
            "type": "string",
            "nullable": true,
            "enum": [
              "typed",
              "drawn"
            ],
            "description": "Where `area_ha` came from. A `typed` area is never overwritten by a drawn one."
          },
          "paddock_count": {
            "type": "integer",
            "description": "How many LIVE paddocks the property has. Archived paddocks are not counted."
          },
          "mob_count": {
            "type": "integer",
            "description": "How many LIVE mobs the property has. Archived mobs are not counted."
          },
          "blocks": {
            "type": "array",
            "description": "The property's live blocks. Empty when every paddock is in the home block.",
            "items": {
              "$ref": "#/components/schemas/PropertyBlock"
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "organization": {
            "type": "object",
            "description": "Present when include=organization"
          },
          "user": {
            "type": "object",
            "nullable": true,
            "description": "The person who owns a personal property (linkage only); null on a business's farm."
          }
        }
      },
      "FarmsIndexResponseAttributes": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FarmModelAttributes"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta"
          }
        }
      },
      "FarmsShowResponseAttributes": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/FarmModelAttributes"
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta",
            "description": "Optional; typically omitted for show"
          }
        }
      },
      "FarmCreateModelAttributes": {
        "type": "object",
        "description": "Flat attributes-adapter request body (Content-Type application/json). Attribute fields and relationships live directly under `data` (no nested `attributes` / `relationships` envelopes). Each relationship is a resource identifier object ({ id, type }). Equivalent JSON:API (nested) shape: `FarmCreateModel` sent as application/vnd.api+json.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "visibility": {
                "type": "string",
                "enum": [
                  "private",
                  "link",
                  "public"
                ],
                "description": "Who can see the property and, through it, its paddocks, mobs and scoped task lists. `private` (default) is members only; `link` is anyone with the share token (`?share=`); `public` is anyone and search engines."
              },
              "name": {
                "type": "string"
              },
              "location": {
                "type": "string",
                "nullable": true
              },
              "size": {
                "type": "string",
                "nullable": true
              },
              "place_label": {
                "type": "string",
                "nullable": true,
                "description": "The geocoded label shown under the property name on the map. Distinct from `location` (the settings \"Town, State\" caption)."
              },
              "centroid_lat": {
                "type": "number",
                "nullable": true,
                "description": "Latitude of the point the map opens on. Send both `centroid_lat` and `centroid_lon`, or neither — a half pair is rejected. A property with no boundary still needs a point (\"Nothing on the map yet\")."
              },
              "centroid_lon": {
                "type": "number",
                "nullable": true,
                "description": "Longitude of the point the map opens on. Send both `centroid_lat` and `centroid_lon`, or neither."
              },
              "organization": {
                "type": "object",
                "nullable": true,
                "description": "The business the property belongs to. Omit it, or send null, for the signed-in user's personal account.",
                "properties": {
                  "id": {
                    "type": "string",
                    "nullable": true
                  },
                  "type": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "organization"
                    ]
                  }
                }
              },
              "user": {
                "type": "object",
                "nullable": true,
                "description": "The person the property belongs to — only the signed-in user themselves.",
                "properties": {
                  "id": {
                    "type": "string",
                    "nullable": true
                  },
                  "type": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "user"
                    ]
                  }
                }
              }
            },
            "required": [
              "name"
            ]
          }
        },
        "required": [
          "data"
        ]
      },
      "FarmUpdateModelAttributes": {
        "type": "object",
        "description": "Flat attributes-adapter request body (Content-Type application/json). Attribute fields and relationships live directly under `data` (no nested `attributes` / `relationships` envelopes). Each relationship is a resource identifier object ({ id, type }). Equivalent JSON:API (nested) shape: `FarmUpdateModel` sent as application/vnd.api+json.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "visibility": {
                "type": "string",
                "enum": [
                  "private",
                  "link",
                  "public"
                ],
                "description": "Who can see the property and, through it, its paddocks, mobs and scoped task lists. `private` (default) is members only; `link` is anyone with the share token (`?share=`); `public` is anyone and search engines."
              },
              "name": {
                "type": "string"
              },
              "location": {
                "type": "string",
                "nullable": true
              },
              "size": {
                "type": "string",
                "nullable": true
              },
              "place_label": {
                "type": "string",
                "nullable": true,
                "description": "The geocoded label shown under the property name on the map. Distinct from `location` (the settings \"Town, State\" caption)."
              },
              "centroid_lat": {
                "type": "number",
                "nullable": true,
                "description": "Latitude of the point the map opens on. Send both `centroid_lat` and `centroid_lon`, or neither — a half pair is rejected. A property with no boundary still needs a point (\"Nothing on the map yet\")."
              },
              "centroid_lon": {
                "type": "number",
                "nullable": true,
                "description": "Longitude of the point the map opens on. Send both `centroid_lat` and `centroid_lon`, or neither."
              }
            }
          }
        },
        "required": [
          "data"
        ]
      },
      "FarmBoundaryModel": {
        "type": "object",
        "description": "Nested JSON:API request body (Content-Type application/vnd.api+json) for the property boundary. Equivalent flat (attributes-adapter) shape: `FarmBoundaryModelAttributes` sent as application/json.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "type": {
                "type": "string",
                "enum": [
                  "farm"
                ]
              },
              "attributes": {
                "type": "object",
                "properties": {
                  "boundary": {
                    "type": "array",
                    "nullable": true,
                    "description": "The property outline: an array of rings, each ring an array of\n`[longitude, latitude]` pairs in RFC 7946 order — **longitude\nfirst** — with at least 4 positions and the first position\nrepeated last. This is NOT a GeoJSON object: a value carrying a\n`type` key is read as a relationship on the flat\n`application/json` path and its coordinates are silently\ndiscarded, so the wire format is a plain array on both content\ntypes. Holes are not modelled; each ring becomes one hole-less\npolygon and the parts are stored as a MultiPolygon. Rings are\nrepaired (`ST_MakeValid`) and wound counter-clockwise before\nthe area is measured, so the geometry read back may be\nreordered or split into parts. Omit the key to leave the stored\ngeometry untouched; send `null` to clear it.\n",
                    "items": {
                      "type": "array",
                      "description": "One closed exterior ring.",
                      "minItems": 4,
                      "items": {
                        "type": "array",
                        "description": "[longitude, latitude]",
                        "minItems": 2,
                        "maxItems": 2,
                        "items": {
                          "type": "number"
                        }
                      }
                    }
                  },
                  "area_source": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "typed",
                      "drawn"
                    ],
                    "description": "`drawn` recomputes `area_ha` from the ring. `typed` saves the\nring and leaves `area_ha` exactly as it is — the boundary\nflow's \"keep the stated area\" choice. Omitted, the property's\nown source stands, and a property that has never had one gets\n`drawn`.\n"
                  },
                  "boundary_source": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "traced",
                      "grid",
                      "gps",
                      "imported"
                    ],
                    "description": "How the ring being saved was captured. Provenance describes THIS\nring: omitted alongside a new `boundary`, the stored source is\nreset to null rather than carried over from the ring it replaced;\nsent without a `boundary`, it corrects the stored ring's source in\nplace. Cleared with the ring. `boundary_captured_by` is not\naccepted — the server stamps it from the acting user.\n"
                  },
                  "place_label": {
                    "type": "string",
                    "nullable": true,
                    "description": "The geocoded label shown under the property name on the map, e.g. \"1180 Kywong Road, Currawarna NSW 2650\"."
                  },
                  "centroid_lat": {
                    "type": "number",
                    "nullable": true,
                    "description": "Latitude of the point the map opens on. Send both `centroid_lat` and `centroid_lon`, or neither. When neither is sent and the property has no point yet, one is derived from the ring; an existing point is never moved."
                  },
                  "centroid_lon": {
                    "type": "number",
                    "nullable": true,
                    "description": "Longitude of the point the map opens on. Send both `centroid_lat` and `centroid_lon`, or neither."
                  }
                }
              }
            }
          }
        },
        "required": [
          "data"
        ]
      },
      "FarmBoundaryModelAttributes": {
        "type": "object",
        "description": "Flat attributes-adapter request body (Content-Type application/json) for the property boundary. The same fields hoisted directly under `data` (no nested `attributes` envelope). Equivalent JSON:API (nested) shape: `FarmBoundaryModel` sent as application/vnd.api+json.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "boundary": {
                "type": "array",
                "nullable": true,
                "description": "The property outline: an array of rings, each ring an array of\n`[longitude, latitude]` pairs in RFC 7946 order — **longitude\nfirst** — with at least 4 positions and the first position repeated\nlast. Not a GeoJSON object. Omit the key to leave the stored\ngeometry untouched; send `null` to clear it.\n",
                "items": {
                  "type": "array",
                  "description": "One closed exterior ring.",
                  "minItems": 4,
                  "items": {
                    "type": "array",
                    "description": "[longitude, latitude]",
                    "minItems": 2,
                    "maxItems": 2,
                    "items": {
                      "type": "number"
                    }
                  }
                }
              },
              "area_source": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "typed",
                  "drawn"
                ],
                "description": "`drawn` recomputes `area_ha` from the ring; `typed` leaves `area_ha` untouched. Omitted, the property's own source stands."
              },
              "boundary_source": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "traced",
                  "grid",
                  "gps",
                  "imported"
                ],
                "description": "How the ring being saved was captured. Provenance describes THIS\nring: omitted alongside a new `boundary`, the stored source is\nreset to null rather than carried over from the ring it replaced;\nsent without a `boundary`, it corrects the stored ring's source in\nplace. Cleared with the ring. `boundary_captured_by` is not\naccepted — the server stamps it from the acting user.\n"
              },
              "place_label": {
                "type": "string",
                "nullable": true,
                "description": "The geocoded label shown under the property name on the map."
              },
              "centroid_lat": {
                "type": "number",
                "nullable": true,
                "description": "Latitude of the point the map opens on. Send both `centroid_lat` and `centroid_lon`, or neither."
              },
              "centroid_lon": {
                "type": "number",
                "nullable": true,
                "description": "Longitude of the point the map opens on. Send both `centroid_lat` and `centroid_lon`, or neither."
              }
            }
          }
        },
        "required": [
          "data"
        ]
      },
      "MobWithholding": {
        "type": "object",
        "nullable": true,
        "description": "What is holding a mob back, or null when it is clear: the latest end of each interval across\nthe completed treatment and husbandry records still under one. Dates are ISO 8601. `task_id` and\n`record_id` name the record that ends last; `products` is every named product still holding\nthe mob back.\n",
        "properties": {
          "task_id": {
            "type": "string"
          },
          "record_id": {
            "type": "string"
          },
          "safe_for_slaughter_on": {
            "type": "string",
            "nullable": true,
            "description": "The latest end of any meat withholding period, or null when none is stated."
          },
          "export_eligible_on": {
            "type": "string",
            "nullable": true,
            "description": "The latest end of any export slaughter interval, or null when none is stated."
          },
          "products": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "PaddockWithholding": {
        "type": "object",
        "nullable": true,
        "description": "What is holding a paddock back, or null when it is clear: the latest end of each interval across\nthe completed sprays and seed treatments still under one. Dates are ISO 8601. `task_id` and `record_id` name the record that ends last; `products` is every\nnamed product still holding the paddock back. Fertiliser and field work never appear here.\n",
        "properties": {
          "task_id": {
            "type": "string"
          },
          "record_id": {
            "type": "string"
          },
          "safe_to_graze_on": {
            "type": "string",
            "nullable": true,
            "description": "The latest end of any grazing withholding period, or null when none is stated."
          },
          "safe_to_harvest_on": {
            "type": "string",
            "nullable": true,
            "description": "The latest end of any harvest withholding period, or null when none is stated."
          },
          "products": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "FarmStatusMob": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "state": {
            "type": "string",
            "enum": [
              "clear",
              "under_withholding",
              "under_export_withholding"
            ]
          },
          "withholding": {
            "$ref": "#/components/schemas/MobWithholding"
          }
        }
      },
      "FarmStatusPaddock": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "state": {
            "type": "string",
            "enum": [
              "clear",
              "graze_restricted",
              "harvest_restricted"
            ]
          },
          "withholding": {
            "$ref": "#/components/schemas/PaddockWithholding"
          }
        }
      },
      "FarmStatusResponse": {
        "type": "object",
        "description": "The status of every active mob and paddock on a property, as of today.",
        "properties": {
          "farm_id": {
            "type": "string"
          },
          "as_of": {
            "type": "string",
            "format": "date",
            "description": "The day the dates were compared against, in the server's time zone."
          },
          "mobs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FarmStatusMob"
            }
          },
          "paddocks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FarmStatusPaddock"
            }
          }
        }
      },
      "AssetResourceRef": {
        "type": "object",
        "required": [
          "type",
          "id"
        ],
        "properties": {
          "type": {
            "type": "string",
            "description": "`farm`, `paddock`, `mob` or `task`.",
            "enum": [
              "farm",
              "paddock",
              "mob",
              "task"
            ]
          },
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "nullable": true,
            "description": "Present on a farm, paddock or mob."
          },
          "head_count": {
            "type": "integer",
            "nullable": true,
            "description": "Present on a mob."
          },
          "status": {
            "type": "string",
            "nullable": true,
            "description": "Present on a task. `pending`, `completed` or `cancelled`."
          },
          "operation_type": {
            "type": "string",
            "nullable": true,
            "description": "Present on a task when one was recorded."
          },
          "description": {
            "type": "string",
            "nullable": true,
            "description": "The task text."
          },
          "record_summary": {
            "type": "string",
            "nullable": true,
            "description": "The composed diary line, when the task is a record."
          }
        }
      },
      "AssetResourcesModel": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The asset's id."
          },
          "type": {
            "type": "string",
            "example": "asset_resources"
          },
          "attributes": {
            "type": "object",
            "properties": {
              "asset_type": {
                "type": "string",
                "description": "`paddock` or `mob`."
              },
              "asset_id": {
                "type": "string"
              },
              "name": {
                "type": "string",
                "description": "The asset's name."
              },
              "farm_id": {
                "type": "string",
                "nullable": true
              },
              "resources": {
                "type": "array",
                "description": "The property, the related paddock or mobs, and this page of\ntasks and records. Full records are embedded with `include`.\n",
                "items": {
                  "$ref": "#/components/schemas/AssetResourceRef"
                }
              }
            }
          },
          "relationships": {
            "type": "object",
            "properties": {
              "farm": {
                "type": "object",
                "nullable": true
              },
              "paddock": {
                "type": "object",
                "nullable": true,
                "description": "The paddock a mob is in. Null on a paddock, and null while a mob has never been placed."
              },
              "mobs": {
                "type": "object",
                "description": "Live mobs whose current placement is this paddock. Empty when the asset is a mob."
              },
              "tasks": {
                "type": "object",
                "description": "This page of tasks and records, including moves that name the paddock."
              }
            }
          }
        }
      },
      "AssetResourcesShowResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/AssetResourcesModel"
          },
          "included": {
            "type": "array",
            "description": "Present when `include` names `farm`, `tasks`, `mobs` or `paddock`.",
            "items": {
              "type": "object"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta"
          }
        }
      },
      "AssetResourcesModelAttributes": {
        "type": "object",
        "description": "Single asset-resource list in attributes-adapter shape (Accept application/json).",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "example": "asset_resources"
          },
          "asset_type": {
            "type": "string"
          },
          "asset_id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "farm_id": {
            "type": "string",
            "nullable": true
          },
          "resources": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AssetResourceRef"
            }
          },
          "farm": {
            "type": "object",
            "nullable": true,
            "description": "Present when `include=farm`."
          },
          "paddock": {
            "type": "object",
            "nullable": true,
            "description": "Present when `include=paddock`."
          },
          "mobs": {
            "type": "array",
            "description": "Present when `include=mobs`.",
            "items": {
              "type": "object"
            }
          },
          "tasks": {
            "type": "array",
            "description": "Present when `include=tasks`.",
            "items": {
              "type": "object"
            }
          }
        }
      },
      "AssetResourcesShowResponseAttributes": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/AssetResourcesModelAttributes"
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta"
          }
        }
      },
      "PaddockModel": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "example": "paddock"
          },
          "attributes": {
            "type": "object",
            "properties": {
              "slug": {
                "type": "string",
                "description": "Unique within its parent (a property per owner; a paddock or mob per property); generated from the name and renamed with it. The old slug keeps resolving. See `resolveLink`."
              },
              "web_path": {
                "type": "string",
                "description": "The address on the web. Present when read directly."
              },
              "url": {
                "type": "string",
                "description": "The whole link; carries the property's `?share=` token when it is shared by link. Present when read directly."
              },
              "name": {
                "type": "string",
                "description": "Unique per property, case-insensitively, across live and archived paddocks."
              },
              "area_ha": {
                "type": "number",
                "nullable": true,
                "description": "Paddock area in hectares. Measured from the boundary when `area_source` is `drawn`; the farmer's own number when it is `typed`."
              },
              "area_source": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "typed",
                  "drawn"
                ],
                "description": "Where `area_ha` came from. A `typed` area is never overwritten by a drawn one."
              },
              "land_use": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "cropping",
                  "grazing",
                  "mixed",
                  "fallow"
                ],
                "description": "Null is a real state, not \"unanswered\" — tapping the active chip on the edit form clears it."
              },
              "current_crop": {
                "type": "string",
                "nullable": true,
                "description": "Free text, e.g. \"Wheat\"."
              },
              "boundary": {
                "type": "array",
                "nullable": true,
                "description": "The paddock outline: an array of rings, each ring an array of\n`[longitude, latitude]` pairs in RFC 7946 order — **longitude\nfirst** — with at least 4 positions and the first position repeated\nlast. This is NOT a GeoJSON object: a value carrying a `type` key\nis read as a relationship on the flat `application/json` path and\nits coordinates are silently discarded, so the wire format is a\nplain array on both content types. Holes are not modelled; each\npolygon part contributes its exterior ring. Stored as PostGIS\n`geography(MultiPolygon,4326)`.\n",
                "items": {
                  "type": "array",
                  "description": "One closed exterior ring.",
                  "minItems": 4,
                  "items": {
                    "type": "array",
                    "description": "[longitude, latitude]",
                    "minItems": 2,
                    "maxItems": 2,
                    "items": {
                      "type": "number"
                    }
                  }
                }
              },
              "has_boundary": {
                "type": "boolean",
                "description": "Whether the paddock has an outline at all — the cheap face of `boundary`, so a list can render \"7 mapped · 5 by name\" without reading a single ring."
              },
              "withholding": {
                "nullable": true,
                "description": "Is the paddock under a grazing or harvest withholding period right now? The latest spray record whose graze or harvest date is still ahead, or null. **Derived, not stored.** Read-only. Served on the paddock's own read and on lists (SPRAY_RECORD.md).",
                "type": "object",
                "properties": {
                  "task_id": {
                    "type": "string"
                  },
                  "record_id": {
                    "type": "string",
                    "description": "The SprayRecord id."
                  },
                  "safe_to_graze_on": {
                    "type": "string",
                    "format": "date",
                    "nullable": true
                  },
                  "safe_to_harvest_on": {
                    "type": "string",
                    "format": "date",
                    "nullable": true
                  },
                  "products": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              },
              "boundary_captured_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "When the boundary was last saved. Null while the paddock has no ring."
              },
              "boundary_source": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "traced",
                  "grid",
                  "gps",
                  "imported"
                ],
                "description": "How the ring was captured — traced over imagery, placed on the grid with no imagery, walked with the phone's GPS, or read from a KML/shapefile. Null for a ring saved before provenance was recorded or by a caller that named no source. Written with the ring and cleared with it."
              },
              "boundary_captured_by": {
                "type": "string",
                "nullable": true,
                "description": "Id of the user who saved the ring, stamped server-side from the request's actor and never accepted from a client. Null when a machine token saved it, and null while the paddock has no ring."
              },
              "archived": {
                "type": "boolean",
                "description": "An archived paddock leaves the map and the lists; its records stay on the property. Written as a plain boolean through `updatePaddock` — there is no archive route."
              },
              "archived_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "When the paddock was archived. Null while it is live."
              },
              "block_id": {
                "type": "string",
                "format": "uuid",
                "nullable": true,
                "description": "The block the paddock sits in, a label for where it is inside the property. Null when it is in the home block. The block's name, kind, area and outline are on the property's `blocks`."
              },
              "record_count": {
                "type": "integer",
                "description": "Completed rows on this paddock — its diary. A rename, an area change or a boundary save that changes the area adds one."
              },
              "open_task_count": {
                "type": "integer",
                "description": "Pending rows on this paddock. Cancelled rows count towards neither counter."
              },
              "last_record_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "When the most recent record was completed. Null until there is one."
              },
              "created_at": {
                "type": "string",
                "format": "date-time"
              },
              "updated_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "relationships": {
            "type": "object",
            "properties": {
              "farm": {
                "type": "object",
                "properties": {
                  "data": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "type": {
                        "type": "string"
                      }
                    },
                    "nullable": true
                  }
                }
              }
            }
          }
        }
      },
      "PaddockCreateModel": {
        "type": "object",
        "description": "Nested JSON:API request body (Content-Type application/vnd.api+json). Equivalent flat (attributes-adapter) shape: `PaddockCreateModelAttributes` sent as application/json.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "paddock"
                ]
              },
              "attributes": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "area_ha": {
                    "type": "number",
                    "nullable": true
                  },
                  "area_source": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "typed",
                      "drawn"
                    ]
                  },
                  "land_use": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "cropping",
                      "grazing",
                      "mixed",
                      "fallow"
                    ]
                  },
                  "current_crop": {
                    "type": "string",
                    "nullable": true
                  },
                  "archived": {
                    "type": "boolean",
                    "nullable": true
                  }
                },
                "required": [
                  "name"
                ]
              },
              "relationships": {
                "type": "object",
                "properties": {
                  "farm": {
                    "type": "object",
                    "properties": {
                      "data": {
                        "type": "object",
                        "nullable": true,
                        "properties": {
                          "id": {
                            "type": "string",
                            "nullable": true
                          },
                          "type": {
                            "type": "string",
                            "nullable": true,
                            "enum": [
                              "farm"
                            ]
                          }
                        }
                      }
                    },
                    "required": [
                      "data"
                    ]
                  }
                },
                "required": [
                  "farm"
                ]
              }
            },
            "required": [
              "type",
              "attributes",
              "relationships"
            ]
          }
        },
        "required": [
          "data"
        ]
      },
      "PaddockUpdateModel": {
        "type": "object",
        "description": "Nested JSON:API request body (Content-Type application/vnd.api+json). Every field is optional — a partial write. Equivalent flat (attributes-adapter) shape: `PaddockUpdateModelAttributes` sent as application/json.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "type": {
                "type": "string",
                "enum": [
                  "paddock"
                ]
              },
              "attributes": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "area_ha": {
                    "type": "number",
                    "nullable": true
                  },
                  "area_source": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "typed",
                      "drawn"
                    ]
                  },
                  "land_use": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "cropping",
                      "grazing",
                      "mixed",
                      "fallow"
                    ]
                  },
                  "current_crop": {
                    "type": "string",
                    "nullable": true
                  },
                  "archived": {
                    "type": "boolean",
                    "nullable": true,
                    "description": "`true` archives the paddock, `false` restores it. This is the whole archive surface — there is no `/archive` route."
                  },
                  "block_id": {
                    "type": "string",
                    "format": "uuid",
                    "nullable": true,
                    "description": "Puts the paddock in a live block of the same property, or `null` to take it out. Writes no record: grouping leaves every record unchanged."
                  }
                }
              }
            }
          }
        }
      },
      "PaddocksIndexResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PaddockModel"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta"
          }
        }
      },
      "PaddocksShowResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/PaddockModel"
          }
        }
      },
      "PaddockModelAttributes": {
        "type": "object",
        "description": "Single paddock in attributes-adapter shape (Accept application/json).",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "example": "paddock"
          },
          "slug": {
            "type": "string",
            "description": "Unique within its parent (a property per owner; a paddock or mob per property); generated from the name and renamed with it. The old slug keeps resolving. See `resolveLink`."
          },
          "web_path": {
            "type": "string",
            "description": "The address on the web. Present when read directly."
          },
          "url": {
            "type": "string",
            "description": "The whole link; carries the property's `?share=` token when it is shared by link. Present when read directly."
          },
          "name": {
            "type": "string"
          },
          "area_ha": {
            "type": "number",
            "nullable": true,
            "description": "Paddock area in hectares. Measured from the boundary when `area_source` is `drawn`; the farmer's own number when it is `typed`."
          },
          "area_source": {
            "type": "string",
            "nullable": true,
            "enum": [
              "typed",
              "drawn"
            ],
            "description": "Where `area_ha` came from. A `typed` area is never overwritten by a drawn one."
          },
          "land_use": {
            "type": "string",
            "nullable": true,
            "enum": [
              "cropping",
              "grazing",
              "mixed",
              "fallow"
            ]
          },
          "current_crop": {
            "type": "string",
            "nullable": true
          },
          "boundary": {
            "type": "array",
            "nullable": true,
            "description": "The paddock outline: an array of rings, each ring an array of\n`[longitude, latitude]` pairs in RFC 7946 order — **longitude first** —\nwith at least 4 positions and the first repeated last. Not a GeoJSON\nobject (see `PaddockBoundaryModel`).\n",
            "items": {
              "type": "array",
              "description": "One closed exterior ring.",
              "minItems": 4,
              "items": {
                "type": "array",
                "description": "[longitude, latitude]",
                "minItems": 2,
                "maxItems": 2,
                "items": {
                  "type": "number"
                }
              }
            }
          },
          "has_boundary": {
            "type": "boolean",
            "description": "Whether the paddock has an outline at all — the cheap face of `boundary`."
          },
          "withholding": {
            "nullable": true,
            "description": "Is the paddock under a grazing or harvest withholding period right now? The latest spray record whose graze or harvest date is still ahead, or null. **Derived, not stored.** Read-only. Served on the paddock's own read and on lists (SPRAY_RECORD.md).",
            "type": "object",
            "properties": {
              "task_id": {
                "type": "string"
              },
              "record_id": {
                "type": "string",
                "description": "The SprayRecord id."
              },
              "safe_to_graze_on": {
                "type": "string",
                "format": "date",
                "nullable": true
              },
              "safe_to_harvest_on": {
                "type": "string",
                "format": "date",
                "nullable": true
              },
              "products": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          },
          "boundary_captured_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "boundary_source": {
            "type": "string",
            "nullable": true,
            "enum": [
              "traced",
              "grid",
              "gps",
              "imported"
            ],
            "description": "How the ring was captured — traced over imagery, placed on the grid with no imagery, walked with the phone's GPS, or read from a KML/shapefile. Null for a ring saved before provenance was recorded or by a caller that named no source. Written with the ring and cleared with it."
          },
          "boundary_captured_by": {
            "type": "string",
            "nullable": true,
            "description": "Id of the user who saved the ring, stamped server-side from the request's actor and never accepted from a client. Null when a machine token saved it, and null while the paddock has no ring."
          },
          "archived": {
            "type": "boolean",
            "description": "An archived paddock leaves the map and the lists; its records stay on the property."
          },
          "archived_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "block_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "The block the paddock sits in. Null in the home block."
          },
          "record_count": {
            "type": "integer",
            "description": "Completed rows on this paddock — its diary."
          },
          "open_task_count": {
            "type": "integer",
            "description": "Pending rows on this paddock."
          },
          "last_record_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "farm": {
            "type": "object",
            "description": "Present when include=farm"
          }
        }
      },
      "PaddocksIndexResponseAttributes": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PaddockModelAttributes"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta"
          }
        }
      },
      "PaddocksShowResponseAttributes": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/PaddockModelAttributes"
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta",
            "description": "Optional; typically omitted for show"
          }
        }
      },
      "PaddockCreateModelAttributes": {
        "type": "object",
        "description": "Flat attributes-adapter request body (Content-Type application/json). Attribute fields and relationships live directly under `data` (no nested `attributes` / `relationships` envelopes). Each relationship is a resource identifier object ({ id, type }). Equivalent JSON:API (nested) shape: `PaddockCreateModel` sent as application/vnd.api+json.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "area_ha": {
                "type": "number",
                "nullable": true
              },
              "area_source": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "typed",
                  "drawn"
                ]
              },
              "land_use": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "cropping",
                  "grazing",
                  "mixed",
                  "fallow"
                ]
              },
              "current_crop": {
                "type": "string",
                "nullable": true
              },
              "archived": {
                "type": "boolean",
                "nullable": true
              },
              "farm": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "string",
                    "nullable": true
                  },
                  "type": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "farm"
                    ]
                  }
                }
              }
            },
            "required": [
              "name",
              "farm"
            ]
          }
        },
        "required": [
          "data"
        ]
      },
      "PaddockUpdateModelAttributes": {
        "type": "object",
        "description": "Flat attributes-adapter request body (Content-Type application/json). Attribute fields live directly under `data` (no nested `attributes` envelope). Every field is optional — a partial write. Equivalent JSON:API (nested) shape: `PaddockUpdateModel` sent as application/vnd.api+json.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "area_ha": {
                "type": "number",
                "nullable": true
              },
              "area_source": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "typed",
                  "drawn"
                ]
              },
              "land_use": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "cropping",
                  "grazing",
                  "mixed",
                  "fallow"
                ]
              },
              "current_crop": {
                "type": "string",
                "nullable": true
              },
              "archived": {
                "type": "boolean",
                "nullable": true,
                "description": "`true` archives the paddock, `false` restores it."
              },
              "block_id": {
                "type": "string",
                "format": "uuid",
                "nullable": true,
                "description": "Puts the paddock in a live block of the same property, or `null` to take it out. Writes no record."
              }
            }
          }
        },
        "required": [
          "data"
        ]
      },
      "PaddockBoundaryModel": {
        "type": "object",
        "description": "Nested JSON:API request body (Content-Type application/vnd.api+json) for the paddock boundary. Equivalent flat (attributes-adapter) shape: `PaddockBoundaryModelAttributes` sent as application/json.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "type": {
                "type": "string",
                "enum": [
                  "paddock"
                ]
              },
              "attributes": {
                "type": "object",
                "properties": {
                  "boundary": {
                    "type": "array",
                    "nullable": true,
                    "description": "The paddock outline: an array of rings, each ring an array of\n`[longitude, latitude]` pairs in RFC 7946 order — **longitude\nfirst** — with at least 4 positions and the first position\nrepeated last. This is NOT a GeoJSON object: a value carrying a\n`type` key is read as a relationship on the flat\n`application/json` path and its coordinates are silently\ndiscarded, so the wire format is a plain array on both content\ntypes. Holes are not modelled; each ring becomes one hole-less\npolygon and the parts are stored as a MultiPolygon. Rings are\nrepaired (`ST_MakeValid`) and wound counter-clockwise before\nthe area is measured, so the geometry read back may be\nreordered or split into parts. Omit the key to leave the stored\ngeometry untouched; send `null` to clear it.\n",
                    "items": {
                      "type": "array",
                      "description": "One closed exterior ring.",
                      "minItems": 4,
                      "items": {
                        "type": "array",
                        "description": "[longitude, latitude]",
                        "minItems": 2,
                        "maxItems": 2,
                        "items": {
                          "type": "number"
                        }
                      }
                    }
                  },
                  "area_source": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "typed",
                      "drawn"
                    ],
                    "description": "`drawn` recomputes `area_ha` from the ring. `typed` saves the\nring and leaves `area_ha` exactly as it is — the \"keep the\nstated area\" choice. Omitted, the paddock's own source stands,\nand a paddock that has never had one gets `drawn`.\n"
                  },
                  "boundary_source": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "traced",
                      "grid",
                      "gps",
                      "imported"
                    ],
                    "description": "How the ring being saved was captured. Provenance describes THIS\nring: omitted alongside a new `boundary`, the stored source is\nreset to null rather than carried over from the ring it replaced;\nsent without a `boundary`, it corrects the stored ring's source in\nplace. Cleared with the ring. `boundary_captured_by` is not\naccepted — the server stamps it from the acting user.\n"
                  }
                }
              }
            }
          }
        },
        "required": [
          "data"
        ]
      },
      "PaddockBoundaryModelAttributes": {
        "type": "object",
        "description": "Flat attributes-adapter request body (Content-Type application/json) for the paddock boundary. The same fields hoisted directly under `data` (no nested `attributes` envelope). Equivalent JSON:API (nested) shape: `PaddockBoundaryModel` sent as application/vnd.api+json.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "boundary": {
                "type": "array",
                "nullable": true,
                "description": "The paddock outline: an array of rings, each ring an array of\n`[longitude, latitude]` pairs in RFC 7946 order — **longitude\nfirst** — with at least 4 positions and the first position repeated\nlast. Not a GeoJSON object. Omit the key to leave the stored\ngeometry untouched; send `null` to clear it.\n",
                "items": {
                  "type": "array",
                  "description": "One closed exterior ring.",
                  "minItems": 4,
                  "items": {
                    "type": "array",
                    "description": "[longitude, latitude]",
                    "minItems": 2,
                    "maxItems": 2,
                    "items": {
                      "type": "number"
                    }
                  }
                }
              },
              "area_source": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "typed",
                  "drawn"
                ],
                "description": "`drawn` recomputes `area_ha` from the ring; `typed` leaves `area_ha` untouched. Omitted, the paddock's own source stands."
              },
              "boundary_source": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "traced",
                  "grid",
                  "gps",
                  "imported"
                ],
                "description": "How the ring being saved was captured. Provenance describes THIS\nring: omitted alongside a new `boundary`, the stored source is\nreset to null rather than carried over from the ring it replaced;\nsent without a `boundary`, it corrects the stored ring's source in\nplace. Cleared with the ring. `boundary_captured_by` is not\naccepted — the server stamps it from the acting user.\n"
              }
            }
          }
        },
        "required": [
          "data"
        ]
      },
      "MobModel": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "example": "mob"
          },
          "attributes": {
            "type": "object",
            "properties": {
              "slug": {
                "type": "string",
                "description": "Unique within its parent (a property per owner; a paddock or mob per property); generated from the name and renamed with it. The old slug keeps resolving. See `resolveLink`."
              },
              "web_path": {
                "type": "string",
                "description": "The address on the web. Present when read directly."
              },
              "url": {
                "type": "string",
                "description": "The whole link; carries the property's `?share=` token when it is shared by link. Present when read directly."
              },
              "name": {
                "type": "string",
                "description": "Unique per property, case-insensitively, across live and archived mobs."
              },
              "head_count": {
                "type": "integer",
                "nullable": true,
                "minimum": 0,
                "description": "How many head are in the mob, as a number — never a display string. Null is a real state: a mob nobody has counted yet. The unit and the thousands separator are the client's to render."
              },
              "withholding": {
                "nullable": true,
                "description": "Is the mob under a withholding period or export slaughter interval right now? The latest drench record whose safe-for-slaughter or export date is still ahead, or null. **Derived, not stored.** Read-only.",
                "type": "object",
                "properties": {
                  "task_id": {
                    "type": "string"
                  },
                  "record_id": {
                    "type": "string",
                    "description": "The DrenchRecord id."
                  },
                  "safe_for_slaughter_on": {
                    "type": "string",
                    "format": "date",
                    "nullable": true
                  },
                  "export_eligible_on": {
                    "type": "string",
                    "format": "date",
                    "nullable": true
                  },
                  "products": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              },
              "current_paddock_id": {
                "type": "string",
                "nullable": true,
                "description": "Where the mob is right now. **Derived, not stored**: it is the\n`to_paddock_id` of the most recent completed record on the mob that\ncarries one — which is deliberately keyed on the column and not on\n`operation_type`, because closing a task like\n\"Drench the merino wethers #Top40\" moves the mob and that row is a\ndrench record, not a `move_stock` one. Null is the \"Put in a\npaddock\" state: a mob that has never been moved. It changes on its\nown when a movement record is reopened or its destination paddock\nis removed, so it is read-only — move a mob with `moveMob`.\n"
              },
              "archived": {
                "type": "boolean",
                "description": "An archived mob leaves the map and the lists; its records stay on the property. Written as a plain boolean through `updateMob` — there is no archive route."
              },
              "archived_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "When the mob was archived. Null while it is live."
              },
              "record_count": {
                "type": "integer",
                "description": "Completed rows on this mob — its diary. A rename, a head-count change or a move each add one."
              },
              "open_task_count": {
                "type": "integer",
                "description": "Pending rows on this mob. Cancelled rows count towards neither counter."
              },
              "last_record_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "When the most recent record was completed. Null until there is one."
              },
              "created_at": {
                "type": "string",
                "format": "date-time"
              },
              "updated_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "relationships": {
            "type": "object",
            "properties": {
              "farm": {
                "type": "object",
                "properties": {
                  "data": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "type": {
                        "type": "string"
                      }
                    },
                    "nullable": true
                  }
                }
              }
            }
          }
        }
      },
      "MobCreateModel": {
        "type": "object",
        "description": "Nested JSON:API request body (Content-Type application/vnd.api+json). Equivalent flat (attributes-adapter) shape: `MobCreateModelAttributes` sent as application/json.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "mob"
                ]
              },
              "attributes": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "head_count": {
                    "type": "integer",
                    "nullable": true,
                    "minimum": 0
                  },
                  "archived": {
                    "type": "boolean",
                    "nullable": true
                  }
                },
                "required": [
                  "name"
                ]
              },
              "relationships": {
                "type": "object",
                "properties": {
                  "farm": {
                    "type": "object",
                    "properties": {
                      "data": {
                        "type": "object",
                        "nullable": true,
                        "properties": {
                          "id": {
                            "type": "string",
                            "nullable": true
                          },
                          "type": {
                            "type": "string",
                            "nullable": true,
                            "enum": [
                              "farm"
                            ]
                          }
                        }
                      }
                    },
                    "required": [
                      "data"
                    ]
                  }
                },
                "required": [
                  "farm"
                ]
              }
            },
            "required": [
              "type",
              "attributes",
              "relationships"
            ]
          }
        },
        "required": [
          "data"
        ]
      },
      "MobUpdateModel": {
        "type": "object",
        "description": "Nested JSON:API request body (Content-Type application/vnd.api+json). Every field is optional — a partial write. Equivalent flat (attributes-adapter) shape: `MobUpdateModelAttributes` sent as application/json.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "type": {
                "type": "string",
                "enum": [
                  "mob"
                ]
              },
              "attributes": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "head_count": {
                    "type": "integer",
                    "nullable": true,
                    "minimum": 0
                  },
                  "archived": {
                    "type": "boolean",
                    "nullable": true,
                    "description": "`true` archives the mob, `false` restores it. This is the whole archive surface — there is no `/archive` route."
                  }
                }
              }
            }
          }
        }
      },
      "MobsIndexResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MobModel"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta"
          }
        }
      },
      "MobsShowResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/MobModel"
          }
        }
      },
      "MobModelAttributes": {
        "type": "object",
        "description": "Single mob in attributes-adapter shape (Accept application/json).",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "example": "mob"
          },
          "slug": {
            "type": "string",
            "description": "Unique within its parent (a property per owner; a paddock or mob per property); generated from the name and renamed with it. The old slug keeps resolving. See `resolveLink`."
          },
          "web_path": {
            "type": "string",
            "description": "The address on the web. Present when read directly."
          },
          "url": {
            "type": "string",
            "description": "The whole link; carries the property's `?share=` token when it is shared by link. Present when read directly."
          },
          "name": {
            "type": "string"
          },
          "head_count": {
            "type": "integer",
            "nullable": true,
            "minimum": 0,
            "description": "How many head are in the mob, as a number — never a display string. Null is a real state."
          },
          "withholding": {
            "nullable": true,
            "description": "Is the mob under a withholding period or export slaughter interval right now? The latest drench record whose safe-for-slaughter or export date is still ahead, or null. **Derived, not stored.** Read-only.",
            "type": "object",
            "properties": {
              "task_id": {
                "type": "string"
              },
              "record_id": {
                "type": "string",
                "description": "The DrenchRecord id."
              },
              "safe_for_slaughter_on": {
                "type": "string",
                "format": "date",
                "nullable": true
              },
              "export_eligible_on": {
                "type": "string",
                "format": "date",
                "nullable": true
              },
              "products": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          },
          "current_paddock_id": {
            "type": "string",
            "nullable": true,
            "description": "Where the mob is right now, derived from the most recent completed record carrying a destination (see `MobModel`). Read-only — move a mob with `moveMob`."
          },
          "archived": {
            "type": "boolean",
            "description": "An archived mob leaves the map and the lists; its records stay on the property."
          },
          "archived_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "record_count": {
            "type": "integer",
            "description": "Completed rows on this mob — its diary."
          },
          "open_task_count": {
            "type": "integer",
            "description": "Pending rows on this mob."
          },
          "last_record_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "farm": {
            "type": "object",
            "description": "Present when include=farm"
          }
        }
      },
      "MobsIndexResponseAttributes": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MobModelAttributes"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta"
          }
        }
      },
      "MobsShowResponseAttributes": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/MobModelAttributes"
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta",
            "description": "Optional; typically omitted for show"
          }
        }
      },
      "MobCreateModelAttributes": {
        "type": "object",
        "description": "Flat attributes-adapter request body (Content-Type application/json). Attribute fields and relationships live directly under `data` (no nested `attributes` / `relationships` envelopes). Each relationship is a resource identifier object ({ id, type }). Equivalent JSON:API (nested) shape: `MobCreateModel` sent as application/vnd.api+json.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "head_count": {
                "type": "integer",
                "nullable": true,
                "minimum": 0
              },
              "archived": {
                "type": "boolean",
                "nullable": true
              },
              "farm": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "string",
                    "nullable": true
                  },
                  "type": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "farm"
                    ]
                  }
                }
              }
            },
            "required": [
              "name",
              "farm"
            ]
          }
        },
        "required": [
          "data"
        ]
      },
      "MobUpdateModelAttributes": {
        "type": "object",
        "description": "Flat attributes-adapter request body (Content-Type application/json). Attribute fields live directly under `data` (no nested `attributes` envelope). Every field is optional — a partial write. Equivalent JSON:API (nested) shape: `MobUpdateModel` sent as application/vnd.api+json.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "head_count": {
                "type": "integer",
                "nullable": true,
                "minimum": 0
              },
              "archived": {
                "type": "boolean",
                "nullable": true,
                "description": "`true` archives the mob, `false` restores it."
              }
            }
          }
        },
        "required": [
          "data"
        ]
      },
      "MobMoveModel": {
        "type": "object",
        "description": "Nested JSON:API request body (Content-Type application/vnd.api+json) for a mob move. The resource type is the mob's own, so a client can build the body from the same serializer it uses for an update. Equivalent flat (attributes-adapter) shape: `MobMoveModelAttributes` sent as application/json.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "type": {
                "type": "string",
                "enum": [
                  "mob"
                ]
              },
              "attributes": {
                "type": "object",
                "properties": {
                  "completed_at": {
                    "type": "string",
                    "format": "date-time",
                    "nullable": true,
                    "description": "When the move happened. Omit or send null for \"now\"; supply a date to record a move after the fact."
                  },
                  "capture_source": {
                    "type": "string",
                    "nullable": true,
                    "description": "How the move was captured — `voice`, `typed`, `agent` or `import`. `form` is accepted and stored as `typed`. Free text, deliberately not an enum, because the vocabulary is still moving and a schema stricter than the model rejects valid traffic before authentication runs. Defaults to `typed`."
                  }
                }
              },
              "relationships": {
                "type": "object",
                "properties": {
                  "to_paddock": {
                    "type": "object",
                    "properties": {
                      "data": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "type": {
                            "type": "string",
                            "enum": [
                              "paddock"
                            ]
                          }
                        },
                        "required": [
                          "id"
                        ]
                      }
                    },
                    "required": [
                      "data"
                    ]
                  }
                },
                "required": [
                  "to_paddock"
                ]
              }
            },
            "required": [
              "type",
              "relationships"
            ]
          }
        },
        "required": [
          "data"
        ]
      },
      "MobMoveModelAttributes": {
        "type": "object",
        "description": "Flat attributes-adapter request body (Content-Type application/json) for a mob move. The destination is a resource identifier object ({ id, type }) directly under `data`. Equivalent JSON:API (nested) shape: `MobMoveModel` sent as application/vnd.api+json.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "completed_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "When the move happened. Omit or send null for \"now\"."
              },
              "capture_source": {
                "type": "string",
                "nullable": true,
                "description": "How the move was captured — `voice`, `typed`, `agent` or `import`. `form` is stored as `typed`. Defaults to `typed`."
              },
              "to_paddock": {
                "type": "object",
                "description": "The destination — a live paddock on the mob's own property.",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "type": {
                    "type": "string",
                    "enum": [
                      "paddock"
                    ]
                  }
                },
                "required": [
                  "id"
                ]
              }
            },
            "required": [
              "to_paddock"
            ]
          }
        },
        "required": [
          "data"
        ]
      },
      "SprayProductRead": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "nullable": true,
            "description": "What went in the tank. Blank is valid — a spray recorded before anyone said what it was."
          },
          "active": {
            "type": "string",
            "nullable": true,
            "description": "Label active constituent, e.g. `Clopyralid 300 g/L`."
          },
          "batch": {
            "type": "string",
            "nullable": true,
            "description": "Batch number from the pack. Null when it was not recorded. Never invented."
          },
          "rate": {
            "type": "string",
            "nullable": true,
            "description": "As entered, e.g. `300 mL / ha`."
          },
          "grazing_whp_days": {
            "type": "integer",
            "minimum": 0,
            "description": "Grazing withholding period, days."
          },
          "harvest_whp_days": {
            "type": "integer",
            "minimum": 0,
            "description": "Harvest withholding period, days."
          },
          "position": {
            "type": "integer",
            "description": "Order the products were entered in."
          },
          "role": {
            "type": "string",
            "nullable": true,
            "enum": [
              "herbicide",
              "fungicide",
              "insecticide",
              "adjuvant"
            ],
            "description": "What the product is in the tank."
          },
          "expiry": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "description": "Expiry date on the pack, `YYYY-MM-DD`. Never invent one; unset prints `not recorded`."
          },
          "rate_value": {
            "type": "number",
            "nullable": true
          },
          "rate_unit": {
            "type": "string",
            "nullable": true,
            "enum": [
              "g",
              "kg",
              "t",
              "mL",
              "L",
              "seeds",
              "%"
            ],
            "description": "Unit of `rate_value`. A rate with a number and no unit is refused (422)."
          },
          "rate_basis": {
            "type": "string",
            "nullable": true,
            "enum": [
              "ha",
              "head",
              "100_kg_seed",
              "100_l",
              "total"
            ],
            "description": "What the rate is per."
          },
          "esi_days": {
            "type": "integer",
            "minimum": 0
          },
          "egi_days": {
            "type": "integer",
            "minimum": 0
          },
          "eafi_days": {
            "type": "integer",
            "minimum": 0
          },
          "do_not_graze": {
            "type": "boolean",
            "description": "The label says DO NOT graze. Recorded on the spray and shown on the record; it gives the paddock no status, since it has no date."
          },
          "product_id": {
            "type": "string",
            "nullable": true,
            "description": "Optional link to the catalogue; the values here are copied at save."
          }
        }
      },
      "SprayRead": {
        "type": "object",
        "description": "The spray of a task or record. `safe_to_graze_on` and `safe_to_harvest_on` are DERIVED (never stored) from the completion date plus the longest grazing / harvest WHP among named products, and null while the task is open or until a named product carries a value above zero for that period.",
        "properties": {
          "id": {
            "type": "string",
            "description": "The SprayRecord id — the same before and after the task is completed."
          },
          "area_ha": {
            "type": "number",
            "nullable": true,
            "minimum": 0,
            "description": "Hectares sprayed. Defaults to the paddock's area when the child is created without one and again when the task closes; null when the paddock has no area either."
          },
          "products": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SprayProductRead"
            }
          },
          "safe_to_graze_on": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "safe_to_harvest_on": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "method": {
            "type": "string",
            "nullable": true,
            "enum": [
              "boom",
              "boomless",
              "aerial",
              "drone",
              "spot",
              "weed_wiper",
              "knapsack",
              "airblast"
            ],
            "description": "How it was applied."
          },
          "water_rate_l_ha": {
            "type": "number",
            "nullable": true
          },
          "nozzle": {
            "type": "string",
            "nullable": true,
            "description": "Brand, model and size, as entered."
          },
          "pressure_kpa": {
            "type": "integer",
            "nullable": true,
            "minimum": 0,
            "description": "Boom pressure, kPa."
          },
          "boom_height_m": {
            "type": "number",
            "nullable": true
          },
          "speed_kmh": {
            "type": "number",
            "nullable": true
          },
          "droplet_category": {
            "type": "string",
            "nullable": true,
            "enum": [
              "extremely_fine",
              "very_fine",
              "fine",
              "medium",
              "coarse",
              "very_coarse",
              "extremely_coarse",
              "ultra_coarse"
            ],
            "description": "Droplet size category."
          },
          "no_spray_zone_m": {
            "type": "integer",
            "nullable": true,
            "minimum": 0,
            "description": "Downwind no-spray zone kept, metres."
          },
          "re_entry_note": {
            "type": "string",
            "nullable": true,
            "description": "Re-entry period, worker safety, informational."
          },
          "neighbour_drift": {
            "type": "boolean",
            "nullable": true,
            "description": "Drift from a neighbour was a factor."
          },
          "do_not_graze": {
            "type": "boolean",
            "description": "A named product's label says DO NOT graze. Recorded on the spray; it gives the paddock no status, since it has no date."
          },
          "weather_readings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SprayWeatherReadingRead"
            }
          }
        }
      },
      "SprayProductWrite": {
        "type": "object",
        "description": "One product in the tank. A tank mix is several of these on the one record. `rate` is typed as the farmer said it (\"300 mL / ha\") and is kept verbatim and split into `rate_value`, `rate_unit` and `rate_basis`; a number with no unit is refused. Never invent a batch, expiry, rate or withholding period.",
        "properties": {
          "name": {
            "type": "string",
            "nullable": true,
            "description": "Product trade name, for example \"Lontrel\". Blank is valid, but a blank name does not contribute to the derived withholding dates."
          },
          "active": {
            "type": "string",
            "nullable": true,
            "description": "Label active constituent, for example \"Clopyralid 300 g/L\"."
          },
          "batch": {
            "type": "string",
            "nullable": true,
            "description": "Batch number from the pack. Never invent one; leave it unset until the farmer confirms it."
          },
          "rate": {
            "type": "string",
            "nullable": true,
            "description": "Value, unit and basis, as entered, for example \"300 mL / ha\". Ask if the unit or the basis is missing."
          },
          "grazing_whp_days": {
            "description": "Grazing withholding period from the label, in whole days. \"7 d\" is read as 7. Never invent it.",
            "oneOf": [
              {
                "type": "integer",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "harvest_whp_days": {
            "description": "Harvest withholding period from the label, in whole days. \"28 d\" is read as 28. If the label gives one withholding period for both, send it in both. Never invent it.",
            "oneOf": [
              {
                "type": "integer",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "role": {
            "type": "string",
            "nullable": true,
            "enum": [
              "herbicide",
              "fungicide",
              "insecticide",
              "adjuvant"
            ],
            "description": "What the product is in the tank."
          },
          "expiry": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "description": "Expiry date on the pack, `YYYY-MM-DD`. Never invent one; unset prints `not recorded`."
          },
          "rate_value": {
            "description": "The number in the rate. Parsed from `rate` when omitted.",
            "oneOf": [
              {
                "type": "number",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "rate_unit": {
            "type": "string",
            "nullable": true,
            "enum": [
              "g",
              "kg",
              "t",
              "mL",
              "L",
              "seeds",
              "%"
            ],
            "description": "Unit of `rate_value`. A rate with a number and no unit is refused (422)."
          },
          "rate_basis": {
            "type": "string",
            "nullable": true,
            "enum": [
              "ha",
              "head",
              "100_kg_seed",
              "100_l",
              "total"
            ],
            "description": "What the rate is per."
          },
          "esi_days": {
            "description": "Export slaughter interval from the label, in whole days. Never invent it.",
            "oneOf": [
              {
                "type": "integer",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "egi_days": {
            "description": "Export grazing interval from the label, in whole days. Never invent it.",
            "oneOf": [
              {
                "type": "integer",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "eafi_days": {
            "description": "Export animal feed interval from the label, in whole days. Never invent it.",
            "oneOf": [
              {
                "type": "integer",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "do_not_graze": {
            "type": "boolean",
            "description": "The label says DO NOT graze. Recorded on the spray and shown on the record; it gives the paddock no status, since it has no date."
          },
          "product_id": {
            "type": "string",
            "nullable": true,
            "description": "Optional link to the catalogue; the values here are copied at save."
          }
        }
      },
      "SprayWrite": {
        "type": "object",
        "description": "Writes the spray child. `area_ha` strips separators (\"1,240.5\" is 1240.5) and defaults to the paddock's area. `products` REPLACES the set (at most 10) and `weather_readings` REPLACES the list. A completed task needs at least one product row (a blank name is fine); an open task may have none. Never invent a batch, expiry, rate or withholding period.",
        "properties": {
          "area_ha": {
            "description": "Hectares sprayed; separators are stripped. May be less than the paddock.",
            "oneOf": [
              {
                "type": "number",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "products": {
            "type": "array",
            "maxItems": 10,
            "items": {
              "$ref": "#/components/schemas/SprayProductWrite"
            }
          },
          "method": {
            "type": "string",
            "nullable": true,
            "enum": [
              "boom",
              "boomless",
              "aerial",
              "drone",
              "spot",
              "weed_wiper",
              "knapsack",
              "airblast"
            ],
            "description": "How it was applied."
          },
          "water_rate": {
            "description": "Water rate in litres per hectare: 80 or \"80 L/ha\". Anything that is not litres per hectare is read as none, not guessed.",
            "oneOf": [
              {
                "type": "number",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "nozzle": {
            "type": "string",
            "nullable": true,
            "description": "Brand, model and size, as entered."
          },
          "pressure_kpa": {
            "type": "integer",
            "nullable": true,
            "minimum": 0,
            "description": "Boom pressure, kPa."
          },
          "boom_height_m": {
            "description": "Boom height, metres.",
            "oneOf": [
              {
                "type": "number",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "speed_kmh": {
            "description": "Travel speed, km/h.",
            "oneOf": [
              {
                "type": "number",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "droplet_category": {
            "type": "string",
            "nullable": true,
            "enum": [
              "extremely_fine",
              "very_fine",
              "fine",
              "medium",
              "coarse",
              "very_coarse",
              "extremely_coarse",
              "ultra_coarse"
            ],
            "description": "Droplet size category."
          },
          "no_spray_zone_m": {
            "type": "integer",
            "nullable": true,
            "minimum": 0,
            "description": "Downwind no-spray zone kept, metres."
          },
          "re_entry_note": {
            "type": "string",
            "nullable": true,
            "description": "Re-entry period, worker safety, informational."
          },
          "neighbour_drift": {
            "type": "boolean",
            "nullable": true,
            "description": "Drift from a neighbour was a factor."
          },
          "weather_readings": {
            "type": "array",
            "description": "The weather readings, as a list (Queensland wants readings before, during and after). On update this REPLACES the list.",
            "items": {
              "$ref": "#/components/schemas/SprayWeatherReadingWrite"
            }
          }
        }
      },
      "SprayRecordCreateModel": {
        "type": "object",
        "description": "Nested JSON:API request body (application/vnd.api+json) for POST /paddocks/{paddock_id}/spray-records: `data.attributes`. Equivalent flat shape: `SprayRecordCreateModelAttributes` as application/json.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "spray-record"
                ]
              },
              "attributes": {
                "$ref": "#/components/schemas/SprayRecordFields"
              }
            }
          }
        }
      },
      "SprayRecordCreateModelAttributes": {
        "type": "object",
        "description": "Flat request body (application/json) for POST /paddocks/{paddock_id}/spray-records: the fields directly under `data`. Equivalent nested shape: `SprayRecordCreateModel`.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/SprayRecordFields"
          }
        }
      },
      "SprayRecordUpdateModel": {
        "type": "object",
        "description": "Nested JSON:API request body (application/vnd.api+json) for PUT /spray-records/{id}: `data.attributes` with `area_ha` and/or `products` (products REPLACES the set). Equivalent flat shape: `SprayRecordUpdateModelAttributes`.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "type": {
                "type": "string",
                "enum": [
                  "spray-record"
                ]
              },
              "attributes": {
                "$ref": "#/components/schemas/SprayRecordFields"
              }
            }
          }
        }
      },
      "SprayRecordUpdateModelAttributes": {
        "type": "object",
        "description": "Flat request body (application/json) for PUT /spray-records/{id}. Equivalent nested shape: `SprayRecordUpdateModel`.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/SprayRecordFields"
          }
        }
      },
      "SprayRecordFields": {
        "type": "object",
        "properties": {
          "image_upload_ids": {
            "$ref": "#/components/schemas/ImageUploadIdList"
          },
          "image_urls": {
            "$ref": "#/components/schemas/ImageUrlList"
          },
          "image_base64s": {
            "$ref": "#/components/schemas/ImageBase64List"
          },
          "image_mode": {
            "$ref": "#/components/schemas/ImageMode"
          },
          "remove_image_ids": {
            "$ref": "#/components/schemas/ImageRemoveIdList"
          },
          "date": {
            "type": "string",
            "format": "date",
            "description": "The day the spray was done, `YYYY-MM-DD` (create only; default today). Back-date with a past date. The server stores midday in the application time zone, so the derived withholding dates do not slip a day. Do not send an offset datetime here — that belongs on a task's `completed_at`. A future date creates a pending task with no completed_at. Product expiry dates are supplied separately as products[].expiry.",
            "example": "2026-09-28"
          },
          "area_ha": {
            "description": "Hectares sprayed (\"1,240.5\" is 1240.5). May be less than the paddock. Defaults to the paddock's area when omitted.",
            "example": 72,
            "oneOf": [
              {
                "type": "number",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "products": {
            "type": "array",
            "maxItems": 10,
            "description": "One entry per product in the tank. On update this REPLACES the set (position is the array index). A completed record with none is refused (422, \"A spray needs at least one product\").",
            "items": {
              "$ref": "#/components/schemas/SprayProductWrite"
            }
          },
          "description": {
            "type": "string",
            "description": "Create only: the record's title. Defaults to \"Sprayed {paddock}\".",
            "example": "Spot spray the thistles"
          },
          "method": {
            "type": "string",
            "nullable": true,
            "enum": [
              "boom",
              "boomless",
              "aerial",
              "drone",
              "spot",
              "weed_wiper",
              "knapsack",
              "airblast"
            ],
            "description": "How it was applied."
          },
          "water_rate": {
            "description": "Water rate in litres per hectare: 80 or \"80 L/ha\". Anything that is not litres per hectare is read as none, not guessed.",
            "oneOf": [
              {
                "type": "number",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "nozzle": {
            "type": "string",
            "nullable": true,
            "description": "Brand, model and size, as entered."
          },
          "pressure_kpa": {
            "type": "integer",
            "nullable": true,
            "minimum": 0,
            "description": "Boom pressure, kPa."
          },
          "boom_height_m": {
            "description": "Boom height, metres.",
            "oneOf": [
              {
                "type": "number",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "speed_kmh": {
            "description": "Travel speed, km/h.",
            "oneOf": [
              {
                "type": "number",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "droplet_category": {
            "type": "string",
            "nullable": true,
            "enum": [
              "extremely_fine",
              "very_fine",
              "fine",
              "medium",
              "coarse",
              "very_coarse",
              "extremely_coarse",
              "ultra_coarse"
            ],
            "description": "Droplet size category."
          },
          "no_spray_zone_m": {
            "type": "integer",
            "nullable": true,
            "minimum": 0,
            "description": "Downwind no-spray zone kept, metres."
          },
          "re_entry_note": {
            "type": "string",
            "nullable": true,
            "description": "Re-entry period, worker safety, informational."
          },
          "neighbour_drift": {
            "type": "boolean",
            "nullable": true,
            "description": "Drift from a neighbour was a factor."
          },
          "weather_readings": {
            "type": "array",
            "description": "The weather readings, as a list (Queensland wants readings before, during and after). On update this REPLACES the list.",
            "items": {
              "$ref": "#/components/schemas/SprayWeatherReadingWrite"
            }
          }
        }
      },
      "SprayWeatherReadingWrite": {
        "type": "object",
        "description": "One weather reading taken for the spray. Every value is optional: a reading nobody took is empty, never a guess.",
        "properties": {
          "read_at": {
            "type": "string",
            "nullable": true,
            "description": "When it was taken, an ISO 8601 time.",
            "format": "date-time"
          },
          "wind_kmh": {
            "description": "Wind speed, km/h.",
            "oneOf": [
              {
                "type": "number",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "gust_kmh": {
            "description": "Gust, km/h.",
            "oneOf": [
              {
                "type": "number",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "wind_dir": {
            "type": "string",
            "nullable": true,
            "description": "Compass point, e.g. `SW`."
          },
          "temp_c": {
            "type": "number",
            "nullable": true,
            "description": "Temperature, degrees C."
          },
          "rh_pct": {
            "type": "integer",
            "nullable": true,
            "minimum": 0,
            "maximum": 100,
            "description": "Relative humidity, per cent."
          },
          "delta_t": {
            "type": "number",
            "nullable": true,
            "description": "Delta T."
          },
          "cloud_pct": {
            "type": "integer",
            "nullable": true,
            "minimum": 0,
            "maximum": 100,
            "description": "Cloud cover, per cent."
          },
          "rain_mm": {
            "description": "Rain, mm.",
            "oneOf": [
              {
                "type": "number",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "inversion_observed": {
            "type": "boolean",
            "nullable": true,
            "description": "An inversion was observed."
          },
          "source": {
            "type": "string",
            "nullable": true,
            "description": "On-site, or a named station with its distance."
          }
        }
      },
      "SprayWeatherReadingRead": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "read_at": {
            "type": "string",
            "nullable": true,
            "description": "When it was taken, an ISO 8601 time.",
            "format": "date-time"
          },
          "wind_kmh": {
            "description": "Wind speed, km/h.",
            "oneOf": [
              {
                "type": "number",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "gust_kmh": {
            "description": "Gust, km/h.",
            "oneOf": [
              {
                "type": "number",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "wind_dir": {
            "type": "string",
            "nullable": true,
            "description": "Compass point, e.g. `SW`."
          },
          "temp_c": {
            "type": "number",
            "nullable": true,
            "description": "Temperature, degrees C."
          },
          "rh_pct": {
            "type": "integer",
            "nullable": true,
            "minimum": 0,
            "maximum": 100,
            "description": "Relative humidity, per cent."
          },
          "delta_t": {
            "type": "number",
            "nullable": true,
            "description": "Delta T."
          },
          "cloud_pct": {
            "type": "integer",
            "nullable": true,
            "minimum": 0,
            "maximum": 100,
            "description": "Cloud cover, per cent."
          },
          "rain_mm": {
            "description": "Rain, mm.",
            "oneOf": [
              {
                "type": "number",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "inversion_observed": {
            "type": "boolean",
            "nullable": true,
            "description": "An inversion was observed."
          },
          "source": {
            "type": "string",
            "nullable": true,
            "description": "On-site, or a named station with its distance."
          },
          "position": {
            "type": "integer"
          }
        }
      },
      "AddressModel": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string"
          },
          "attributes": {
            "type": "object",
            "properties": {
              "place_id": {
                "type": "string",
                "nullable": true
              },
              "street_number": {
                "type": "string"
              },
              "route": {
                "type": "string"
              },
              "city": {
                "type": "string"
              },
              "subdivision": {
                "type": "string"
              },
              "country": {
                "type": "string"
              }
            }
          },
          "relationships": {
            "type": "object",
            "properties": {
              "city": {
                "type": "object",
                "properties": {
                  "data": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "type": {
                        "type": "string"
                      }
                    },
                    "nullable": true
                  }
                }
              },
              "location": {
                "type": "object",
                "properties": {
                  "data": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "type": {
                        "type": "string"
                      }
                    },
                    "nullable": true
                  }
                }
              }
            }
          }
        }
      },
      "AddressesShowResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/AddressModel"
          }
        }
      },
      "AddressModelAttributes": {
        "type": "object",
        "description": "Address in attributes-adapter shape (Accept application/json); fields at top level.",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "example": "address"
          },
          "place_id": {
            "type": "string",
            "nullable": true
          },
          "street_number": {
            "type": "string"
          },
          "route": {
            "type": "string"
          },
          "city": {
            "type": "string"
          },
          "subdivision": {
            "type": "string"
          },
          "country": {
            "type": "string"
          },
          "included_city": {
            "type": "object",
            "description": "Inlined when include=city"
          },
          "included_location": {
            "type": "object",
            "description": "Inlined when include=location"
          }
        }
      },
      "AddressesShowResponseAttributes": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/AddressModelAttributes"
          }
        }
      },
      "CityModel": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string"
          },
          "attributes": {
            "type": "object",
            "properties": {
              "place_id": {
                "type": "string",
                "nullable": true
              },
              "name": {
                "type": "string"
              },
              "subdivision": {
                "type": "string"
              },
              "country": {
                "type": "string"
              }
            }
          },
          "relationships": {
            "type": "object",
            "properties": {
              "location": {
                "type": "object",
                "properties": {
                  "data": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "type": {
                        "type": "string"
                      }
                    },
                    "nullable": true
                  }
                }
              }
            }
          }
        }
      },
      "CitiesShowResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/CityModel"
          }
        }
      },
      "CityModelAttributes": {
        "type": "object",
        "description": "City in attributes-adapter shape (Accept application/json); fields at top level.",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "example": "city"
          },
          "place_id": {
            "type": "string",
            "nullable": true
          },
          "name": {
            "type": "string"
          },
          "subdivision": {
            "type": "string"
          },
          "country": {
            "type": "string"
          },
          "location": {
            "type": "object",
            "description": "Inlined when include=location"
          }
        }
      },
      "CitiesShowResponseAttributes": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/CityModelAttributes"
          }
        }
      },
      "CategoryModel": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string"
          },
          "attributes": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string",
                "nullable": false
              },
              "type": {
                "type": "string",
                "nullable": false,
                "enum": [
                  "ServiceCategory"
                ]
              }
            }
          }
        }
      },
      "CategoriesIndexResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CategoryModel"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta"
          }
        }
      },
      "CategoriesShowResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/CategoryModel"
          }
        }
      },
      "CategoryModelAttributes": {
        "type": "object",
        "description": "Category in attributes-adapter shape (Accept application/json); fields at top level.",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "example": "category"
          },
          "name": {
            "type": "string"
          },
          "category_type": {
            "type": "string",
            "enum": [
              "ServiceCategory"
            ],
            "description": "Attribute \"type\" in JSON:API; renamed here to avoid clash with resource type"
          }
        }
      },
      "CategoriesIndexResponseAttributes": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CategoryModelAttributes"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta"
          }
        }
      },
      "CategoriesShowResponseAttributes": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/CategoryModelAttributes"
          }
        }
      },
      "LocationModel": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string"
          },
          "attributes": {
            "type": "object",
            "properties": {
              "accuracy": {
                "type": "number",
                "nullable": true
              },
              "lat": {
                "type": "number"
              },
              "lon": {
                "type": "number"
              }
            }
          }
        }
      },
      "LocationsShowResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/LocationModel"
          }
        }
      },
      "LocationModelAttributes": {
        "type": "object",
        "description": "Location in attributes-adapter shape (Accept application/json); fields at top level.",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "example": "location"
          },
          "accuracy": {
            "type": "number",
            "nullable": true
          },
          "lat": {
            "type": "number"
          },
          "lon": {
            "type": "number"
          }
        }
      },
      "LocationsShowResponseAttributes": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/LocationModelAttributes"
          }
        }
      },
      "PredictionModel": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string"
          },
          "attributes": {
            "type": "object",
            "properties": {
              "created_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "relationships": {
            "type": "object",
            "properties": {
              "place": {
                "type": "object",
                "properties": {
                  "data": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "type": {
                        "type": "string"
                      }
                    },
                    "nullable": true
                  }
                }
              }
            }
          }
        }
      },
      "PredictionsShowResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/PredictionModel"
          }
        }
      },
      "PredictionModelAttributes": {
        "type": "object",
        "description": "Prediction in attributes-adapter shape (Accept application/json); fields at top level.",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "example": "prediction"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "place": {
            "type": "object",
            "description": "Inlined when include=place"
          }
        }
      },
      "PredictionsShowResponseAttributes": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/PredictionModelAttributes"
          }
        }
      },
      "TreatmentRegisterRow": {
        "type": "object",
        "description": "One PRODUCT of one completed treatment or husbandry record: a row of the LPA Section 2 treatment\nregister. A value the record does not carry is the literal text `not recorded` (never empty),\nand `not_recorded` lists those keys so a client can set them in amber italic. Dates are ISO\n8601 (`YYYY-MM-DD`) or `not recorded`. `expiry` and `route` are what was entered, and `not\nrecorded` where nobody did (a husbandry record has no route).\n",
        "properties": {
          "record_type": {
            "type": "string",
            "enum": [
              "treatment",
              "husbandry"
            ],
            "description": "Whether the record is a treatment given to the mob or husbandry done to it (a product given at marking)."
          },
          "what": {
            "type": "string",
            "description": "What it was, in a farmer's words: `Drench`, `Vaccine`, `Lice`, or for husbandry the procedures, e.g. `Marking, Tail docking`.",
            "example": "Drench"
          },
          "record_id": {
            "type": "string",
            "description": "The drench record. Omitted on a shared link."
          },
          "task_id": {
            "type": "string",
            "description": "The task that is the record. Omitted on a shared link."
          },
          "mob_id": {
            "type": "string",
            "description": "Omitted on a shared link."
          },
          "treatment_date": {
            "type": "string",
            "example": "2026-08-08"
          },
          "mob": {
            "type": "string"
          },
          "location": {
            "type": "string",
            "description": "The paddock the mob was in on the treatment date, or `not recorded`."
          },
          "head": {
            "type": "string",
            "example": "1240",
            "description": "Head treated, as a string, or `not recorded`."
          },
          "product": {
            "type": "string",
            "description": "Product trade name, or `not recorded`."
          },
          "batch": {
            "type": "string"
          },
          "expiry": {
            "type": "string"
          },
          "dose": {
            "type": "string"
          },
          "route": {
            "type": "string"
          },
          "whp_days": {
            "type": "string",
            "example": "14",
            "description": "This product's withholding period in days, or `not recorded` (0 days is treated as not entered)."
          },
          "esi_days": {
            "type": "string",
            "example": "42"
          },
          "safe_for_slaughter_on": {
            "type": "string",
            "example": "2026-08-22",
            "description": "Treatment date plus the longest WHP among the record's named products, or `not recorded`."
          },
          "export_eligible_on": {
            "type": "string",
            "example": "2026-09-19",
            "description": "Treatment date plus the longest ESI among the record's named products, or `not recorded`."
          },
          "treated_by": {
            "type": "string"
          },
          "under_withholding": {
            "type": "boolean",
            "description": "Whether either safe date is still ahead of today (the whole record, not just this product)."
          },
          "not_recorded": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "location",
                "head",
                "product",
                "batch",
                "expiry",
                "dose",
                "route",
                "whp_days",
                "esi_days",
                "safe_for_slaughter_on",
                "export_eligible_on",
                "treated_by"
              ]
            }
          }
        },
        "required": [
          "treatment_date",
          "mob",
          "location",
          "head",
          "product",
          "batch",
          "expiry",
          "dose",
          "route",
          "whp_days",
          "esi_days",
          "safe_for_slaughter_on",
          "export_eligible_on",
          "treated_by",
          "under_withholding",
          "not_recorded"
        ]
      },
      "TreatmentRegisterMob": {
        "type": "object",
        "description": "The treatments of one mob in the register, with the status a shared page shows as a badge.",
        "properties": {
          "id": {
            "type": "string",
            "description": "Omitted on a shared link."
          },
          "name": {
            "type": "string"
          },
          "head": {
            "type": "integer",
            "nullable": true,
            "description": "The mob's head count, null until counted."
          },
          "status": {
            "type": "string",
            "enum": [
              "under_withholding",
              "clear"
            ]
          },
          "safe_for_slaughter_on": {
            "type": "string",
            "nullable": true,
            "description": "The latest safe-for-slaughter date among the treatments, or null when none is recorded."
          },
          "export_eligible_on": {
            "type": "string",
            "nullable": true
          },
          "treatments": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TreatmentRegisterRow"
            }
          }
        },
        "required": [
          "name",
          "head",
          "status",
          "safe_for_slaughter_on",
          "export_eligible_on",
          "treatments"
        ]
      },
      "TreatmentRegisterSummary": {
        "type": "object",
        "description": "What the Export dialog's count line, amber note and empty states read.",
        "properties": {
          "drenches": {
            "type": "integer",
            "description": "The number of records (kept under its old name). The count line says `drench`, `treatment` or `record` by what they are."
          },
          "records": {
            "type": "integer"
          },
          "mobs": {
            "type": "integer"
          },
          "products": {
            "type": "integer",
            "description": "Rows (one per product)."
          },
          "line": {
            "type": "string",
            "example": "4 drenches across 3 mobs"
          },
          "not_recorded": {
            "type": "object",
            "description": "How many are missing, per field. A product's own fields count once per row; `head`, `location`, `treated_by` and the two safe dates once per drench. Only fields missing somewhere appear.",
            "additionalProperties": {
              "type": "integer"
            }
          },
          "under_withholding_mobs": {
            "type": "integer"
          },
          "empty": {
            "type": "boolean"
          },
          "empty_reason": {
            "type": "string",
            "nullable": true,
            "enum": [
              "no_drenches",
              "none_under_withholding",
              null
            ]
          }
        },
        "required": [
          "drenches",
          "mobs",
          "products",
          "line",
          "not_recorded",
          "under_withholding_mobs",
          "empty",
          "empty_reason"
        ]
      },
      "TreatmentRegisterResponse": {
        "type": "object",
        "description": "A register as data: the treatment register and the NVD list (about mobs), or the spray and paddock\ninputs registers (about paddocks). Plain JSON (not JSON:API): the same body is returned for both\nAccept types. The CSV, the PDF and the shared page are drawn from the same rows. A register of\nmobs carries `mobs`; one of paddocks carries `paddocks`, `columns` (the table's headings, so any\nclient draws it the same way) and `definitions` (the footer line).\n",
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "register",
              "nvd",
              "spray",
              "paddock-inputs"
            ]
          },
          "title": {
            "type": "string",
            "example": "Livestock treatment register"
          },
          "scope": {
            "type": "string",
            "enum": [
              "property",
              "mobs",
              "paddocks"
            ]
          },
          "property": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "pic": {
                "type": "string",
                "nullable": true,
                "description": "The property identification code. The property has no PIC field yet, so this is null and a file prints `not recorded`."
              }
            }
          },
          "range": {
            "type": "object",
            "properties": {
              "from": {
                "type": "string",
                "format": "date"
              },
              "to": {
                "type": "string",
                "format": "date"
              }
            }
          },
          "today": {
            "type": "string",
            "format": "date"
          },
          "columns": {
            "type": "array",
            "description": "Paddock registers only. The headings of the table, in order.",
            "items": {
              "$ref": "#/components/schemas/PaddockRegisterColumn"
            }
          },
          "definitions": {
            "type": "string",
            "description": "Paddock registers only. The footer line that defines the terms used."
          },
          "rows": {
            "type": "array",
            "items": {
              "anyOf": [
                {
                  "$ref": "#/components/schemas/TreatmentRegisterRow"
                },
                {
                  "$ref": "#/components/schemas/PaddockRegisterRow"
                }
              ]
            }
          },
          "mobs": {
            "type": "array",
            "description": "The treatment register and the NVD list only.",
            "items": {
              "$ref": "#/components/schemas/TreatmentRegisterMob"
            }
          },
          "paddocks": {
            "type": "array",
            "description": "The spray and paddock inputs registers only.",
            "items": {
              "$ref": "#/components/schemas/PaddockRegisterGroup"
            }
          },
          "summary": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/TreatmentRegisterSummary"
              },
              {
                "$ref": "#/components/schemas/PaddockRegisterSummary"
              }
            ]
          }
        },
        "required": [
          "kind",
          "title",
          "scope",
          "property",
          "range",
          "today",
          "rows",
          "summary"
        ]
      },
      "PaddockRegisterColumn": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string",
            "description": "The key of the value in each row."
          },
          "label": {
            "type": "string",
            "description": "The heading to print."
          },
          "width": {
            "type": "integer",
            "description": "A relative width; scale to the space available."
          }
        }
      },
      "PaddockRegisterRow": {
        "type": "object",
        "description": "One PRODUCT line of one completed spray, fertiliser or sowing: a row of a paddock register. The\nkeys are the register's `columns`. A value the record should carry and does not is the literal\ntext `not recorded` (listed in `not_recorded`); a value that is genuinely optional and absent is\nnull (print an em dash). Dates are ISO 8601 (`YYYY-MM-DD`). Spray rows carry `spray_date`, `paddock`,\n`crop`, `area_ha`, `product`, `batch`, `rate`, `water_rate`, `method`, `applicator`, `done_for`,\n`weather` and the two end dates `safe_to_graze_on` and `safe_to_harvest_on`. Paddock input rows carry\n`input_date`, `paddock`, `area_ha`, `operation`, `role`, `product`, `rate`, `total_kg`, `method`,\n`analysis`, `batch`, `applicator` and `safe_to_graze_on` (a seed treatment only).\n",
        "additionalProperties": true,
        "properties": {
          "record_id": {
            "type": "string",
            "description": "Omitted on a shared link."
          },
          "task_id": {
            "type": "string",
            "description": "Omitted on a shared link."
          },
          "paddock_id": {
            "type": "string",
            "description": "Omitted on a shared link."
          },
          "paddock": {
            "type": "string"
          },
          "under_withholding": {
            "type": "boolean",
            "description": "A spray row whose grazing or harvest date is still ahead today. Always false for paddock inputs."
          },
          "not_recorded": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "PaddockRegisterGroup": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Omitted on a shared link."
          },
          "name": {
            "type": "string"
          },
          "area_ha": {
            "type": "number",
            "nullable": true
          },
          "status": {
            "type": "string",
            "enum": [
              "clear",
              "under_withholding"
            ]
          },
          "safe_to_graze_on": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "safe_to_harvest_on": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "records": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PaddockRegisterRow"
            }
          }
        }
      },
      "PaddockRegisterSummary": {
        "type": "object",
        "description": "What the Export dialog's count line, amber note and empty states read, for a paddock register.",
        "properties": {
          "records": {
            "type": "integer"
          },
          "paddocks": {
            "type": "integer"
          },
          "products": {
            "type": "integer",
            "description": "Rows (one per product line)."
          },
          "line": {
            "type": "string",
            "example": "3 sprays across 2 paddocks"
          },
          "not_recorded": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            }
          },
          "under_withholding_paddocks": {
            "type": "integer"
          },
          "empty": {
            "type": "boolean"
          },
          "empty_reason": {
            "type": "string",
            "nullable": true,
            "enum": [
              "no_sprays",
              "no_inputs",
              null
            ]
          }
        },
        "required": [
          "records",
          "paddocks",
          "products",
          "line",
          "not_recorded",
          "under_withholding_paddocks",
          "empty",
          "empty_reason"
        ]
      },
      "RecordExportRequest": {
        "type": "object",
        "description": "The register to export. Flat JSON body (`application/json`); there is no JSON:API form.",
        "properties": {
          "scope": {
            "type": "string",
            "enum": [
              "property",
              "mobs",
              "paddocks"
            ],
            "description": "Defaults to `mobs` when `mob_ids` is sent, `paddocks` when `paddock_ids` is, else `property`. `mobs` is for the treatment register and the NVD list; `paddocks` is for the spray and paddock inputs registers."
          },
          "mob_ids": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The chosen mobs, all on this property. Required when `scope` is `mobs`."
          },
          "paddock_ids": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The chosen paddocks, all on this property. Required when `scope` is `paddocks`."
          },
          "from": {
            "type": "string",
            "format": "date",
            "description": "First treatment date. Defaults to a year before `to`."
          },
          "to": {
            "type": "string",
            "format": "date",
            "description": "Last treatment date. Defaults to today."
          },
          "kind": {
            "type": "string",
            "enum": [
              "register",
              "nvd",
              "spray",
              "paddock-inputs"
            ],
            "default": "register",
            "description": "`register` (the LPA treatment register), `nvd` (treatments still under a withholding period), `spray` (the chemical use register) or `paddock-inputs` (fertiliser and sowing)."
          },
          "format": {
            "type": "string",
            "enum": [
              "pdf",
              "csv"
            ],
            "default": "pdf"
          }
        }
      },
      "RecordExport": {
        "type": "object",
        "description": "A requested export. The file is rendered on request (`file_url` is always null).",
        "properties": {
          "id": {
            "type": "string"
          },
          "farm_id": {
            "type": "string"
          },
          "scope": {
            "type": "string",
            "enum": [
              "property",
              "mobs",
              "paddocks"
            ]
          },
          "mob_ids": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "paddock_ids": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "from": {
            "type": "string",
            "format": "date"
          },
          "to": {
            "type": "string",
            "format": "date"
          },
          "kind": {
            "type": "string",
            "enum": [
              "register",
              "nvd",
              "spray",
              "paddock-inputs"
            ]
          },
          "format": {
            "type": "string",
            "enum": [
              "pdf",
              "csv"
            ]
          },
          "generated_by_id": {
            "type": "string",
            "nullable": true
          },
          "file_name": {
            "type": "string",
            "example": "treatment-register-yarrabee-2025-10-05-2026-10-05.pdf"
          },
          "file_url": {
            "type": "string",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "farm_id",
          "scope",
          "mob_ids",
          "paddock_ids",
          "from",
          "to",
          "kind",
          "format",
          "file_name",
          "created_at"
        ]
      },
      "RecordExportCreateResponse": {
        "type": "object",
        "properties": {
          "export": {
            "$ref": "#/components/schemas/RecordExport"
          },
          "summary": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/TreatmentRegisterSummary"
              },
              {
                "$ref": "#/components/schemas/PaddockRegisterSummary"
              }
            ]
          }
        },
        "required": [
          "export",
          "summary"
        ]
      },
      "RecordShareRequest": {
        "type": "object",
        "description": "Who to send an export to. Flat JSON body (`application/json`); there is no JSON:API form. The full address or number is sent here and nowhere else.",
        "properties": {
          "via": {
            "type": "string",
            "enum": [
              "email",
              "sms",
              "download"
            ],
            "description": "`download` sends nothing and only records the share for the history. Defaults to the earlier share' via with `again_of`."
          },
          "to": {
            "type": "string",
            "description": "An email address, or an Australian mobile (`0412 345 678`, `+61412345678`). Not used for `download`. Omit with `again_of` to reuse the earlier recipient."
          },
          "message": {
            "type": "string",
            "maxLength": 500,
            "description": "An optional note, shown in the email. Ignored for text and download."
          },
          "expires_in_days": {
            "type": "integer",
            "enum": [
              1,
              7,
              30
            ],
            "default": 7
          },
          "again_of": {
            "type": "string",
            "description": "A share on this property to send again. Its recipient is reused when `to` is omitted."
          }
        }
      },
      "RecordShare": {
        "type": "object",
        "description": "One act of sharing. The recipient is always masked (`04•• ••• 318`, `j•••@farm.com.au`);\nthe full value is never returned. `state` is derived: revoked, else expired, else failed,\nelse opened, else sent.\n",
        "properties": {
          "id": {
            "type": "string"
          },
          "via": {
            "type": "string",
            "enum": [
              "email",
              "sms",
              "download"
            ]
          },
          "to": {
            "type": "string",
            "nullable": true,
            "description": "The masked recipient. Null for a download.",
            "example": "04•• ••• 318"
          },
          "state": {
            "type": "string",
            "enum": [
              "sent",
              "opened",
              "expired",
              "revoked",
              "failed"
            ]
          },
          "delivery": {
            "type": "string",
            "enum": [
              "sent",
              "failed",
              "recorded"
            ],
            "description": "Whether the email or text left. `recorded` is a download that sends nothing."
          },
          "message": {
            "type": "string",
            "nullable": true
          },
          "url": {
            "type": "string",
            "description": "The view-only link on the web app (`/s/<token>`). A credential: the history is only visible to people who may share."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "opened_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "revoked_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "export": {
            "$ref": "#/components/schemas/RecordExport"
          }
        },
        "required": [
          "id",
          "via",
          "to",
          "state",
          "delivery",
          "url",
          "created_at",
          "expires_at",
          "opened_at",
          "revoked_at",
          "export"
        ]
      },
      "RecordShareResponse": {
        "type": "object",
        "properties": {
          "share": {
            "$ref": "#/components/schemas/RecordShare"
          }
        },
        "required": [
          "share"
        ]
      },
      "RecordSharesResponse": {
        "type": "object",
        "properties": {
          "shares": {
            "type": "array",
            "description": "Newest first, at most 100.",
            "items": {
              "$ref": "#/components/schemas/RecordShare"
            }
          }
        },
        "required": [
          "shares"
        ]
      },
      "RecordShareFailureResponse": {
        "type": "object",
        "description": "The share was stored (it is in the history as `failed`) but nothing reached the recipient. Show `errors[0].title` (\"Email bounced\" / \"Text not delivered\") with Try again, Change address or number, and Download instead.",
        "properties": {
          "errors": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "status": {
                  "type": "string",
                  "example": "502"
                },
                "code": {
                  "type": "string",
                  "enum": [
                    "email_bounced",
                    "text_not_delivered"
                  ]
                },
                "title": {
                  "type": "string"
                },
                "detail": {
                  "type": "string"
                }
              }
            }
          },
          "share": {
            "$ref": "#/components/schemas/RecordShare"
          }
        },
        "required": [
          "errors",
          "share"
        ]
      },
      "SharedRecordsResponse": {
        "type": "object",
        "description": "What `/s/<token>` shows for a live link: only the register the owner chose to export, with no\nids, emails or phone numbers. Plain JSON, same for both Accept types.\n",
        "properties": {
          "state": {
            "type": "string",
            "enum": [
              "active"
            ]
          },
          "kind": {
            "type": "string",
            "enum": [
              "register",
              "nvd",
              "spray",
              "paddock-inputs"
            ]
          },
          "title": {
            "type": "string"
          },
          "property": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "pic": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "range": {
            "type": "object",
            "properties": {
              "from": {
                "type": "string",
                "format": "date"
              },
              "to": {
                "type": "string",
                "format": "date"
              }
            }
          },
          "shared_by": {
            "type": "string",
            "description": "The sender's name (never an email or phone), or `the sender` when they have none."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "formats": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "pdf",
                "csv"
              ]
            }
          },
          "summary": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/TreatmentRegisterSummary"
              },
              {
                "$ref": "#/components/schemas/PaddockRegisterSummary"
              }
            ]
          },
          "mobs": {
            "type": "array",
            "description": "The treatment register and the NVD list only.",
            "items": {
              "$ref": "#/components/schemas/TreatmentRegisterMob"
            }
          },
          "columns": {
            "type": "array",
            "description": "Paddock registers only.",
            "items": {
              "$ref": "#/components/schemas/PaddockRegisterColumn"
            }
          },
          "definitions": {
            "type": "string",
            "description": "Paddock registers only."
          },
          "paddocks": {
            "type": "array",
            "description": "The spray and paddock inputs registers only.",
            "items": {
              "$ref": "#/components/schemas/PaddockRegisterGroup"
            }
          },
          "rows": {
            "type": "array",
            "items": {
              "anyOf": [
                {
                  "$ref": "#/components/schemas/TreatmentRegisterRow"
                },
                {
                  "$ref": "#/components/schemas/PaddockRegisterRow"
                }
              ]
            }
          }
        },
        "required": [
          "state",
          "kind",
          "title",
          "property",
          "range",
          "shared_by",
          "expires_at",
          "formats",
          "summary",
          "rows"
        ]
      },
      "SharedRecordsGoneResponse": {
        "type": "object",
        "description": "The 410 body for a link that was real. Say \"This link has expired.\" or \"This link has been revoked.\", then \"Ask {shared_by} to send a new one.\"",
        "properties": {
          "state": {
            "type": "string",
            "enum": [
              "expired",
              "revoked"
            ]
          },
          "shared_by": {
            "type": "string"
          }
        },
        "required": [
          "state",
          "shared_by"
        ]
      },
      "ApiCredentialModel": {
        "type": "object",
        "description": "Server-to-server API credential — an api-kind OAuth application owned by a\nUser or Organization. The resource id is the OAuth client_id (uid).\n",
        "properties": {
          "id": {
            "type": "string",
            "description": "The OAuth client_id (application uid)."
          },
          "type": {
            "type": "string",
            "example": "api-credential"
          },
          "attributes": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "scopes": {
                "type": "string",
                "description": "Space-separated granted scopes (\"public read\" or \"public read write\")."
              },
              "owner_type": {
                "type": "string",
                "enum": [
                  "User",
                  "Organization"
                ]
              },
              "owner_id": {
                "type": "string",
                "format": "uuid"
              },
              "created_at": {
                "type": "string",
                "format": "date-time"
              },
              "client_secret": {
                "type": "string",
                "description": "Present ONLY in create and rotate responses (show-once — secrets\nare hashed at rest and cannot be re-read). Store it immediately.\n"
              }
            }
          }
        }
      },
      "ApiCredentialsIndexResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ApiCredentialModel"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta"
          }
        }
      },
      "ApiCredentialsShowResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/ApiCredentialModel"
          }
        }
      },
      "ApiCredentialModelAttributes": {
        "type": "object",
        "description": "Single API credential in attributes-adapter shape (Accept application/json):\nid, type and attribute fields at top level.\n",
        "properties": {
          "id": {
            "type": "string",
            "description": "The OAuth client_id (application uid)."
          },
          "type": {
            "type": "string",
            "example": "api-credential"
          },
          "name": {
            "type": "string"
          },
          "scopes": {
            "type": "string"
          },
          "owner_type": {
            "type": "string",
            "enum": [
              "User",
              "Organization"
            ]
          },
          "owner_id": {
            "type": "string",
            "format": "uuid"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "client_secret": {
            "type": "string",
            "description": "Present ONLY in create and rotate responses (show-once)."
          }
        }
      },
      "ApiCredentialsIndexResponseAttributes": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ApiCredentialModelAttributes"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta"
          }
        }
      },
      "ApiCredentialsShowResponseAttributes": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/ApiCredentialModelAttributes"
          }
        }
      },
      "ApiCredentialCreateModel": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "api-credential"
                ]
              },
              "attributes": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "scopes": {
                    "type": "string",
                    "description": "\"read\" or \"read write\" — anything else falls back to the default (\"public read\"). \"public\" always rides along."
                  }
                }
              },
              "relationships": {
                "type": "object",
                "properties": {
                  "owner": {
                    "type": "object",
                    "description": "Owning entity; a bare-null data means \"default to the acting entity\".",
                    "properties": {
                      "data": {
                        "type": "object",
                        "nullable": true,
                        "properties": {
                          "type": {
                            "type": "string",
                            "enum": [
                              "user",
                              "organization"
                            ]
                          },
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "ApiCredentialCreateModelAttributes": {
        "type": "object",
        "description": "Flat attributes-adapter request body (Content-Type application/json). Relationships are resource identifier objects ({ id, type }). Equivalent JSON:API shape: ApiCredentialCreateModel.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "scopes": {
                "type": "string"
              },
              "owner": {
                "type": "object",
                "description": "Owning entity — a User or an Organization the caller can edit. Defaults to the acting entity when omitted.",
                "nullable": true,
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "type": {
                    "type": "string",
                    "enum": [
                      "user",
                      "organization"
                    ]
                  }
                }
              }
            }
          }
        }
      },
      "ApiCredentialTokenCreateModel": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "oauth-token"
                ]
              },
              "attributes": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Display label for the token (\"CI server\", \"Zapier\")."
                  },
                  "expires_in_days": {
                    "type": "integer",
                    "enum": [
                      30,
                      90,
                      365,
                      0
                    ],
                    "description": "Lifetime menu — 30, 90 or 365 days (default), or 0 for non-expiring."
                  }
                }
              }
            }
          }
        }
      },
      "ApiCredentialTokenCreateModelAttributes": {
        "type": "object",
        "description": "Flat attributes-adapter request body (Content-Type application/json). Equivalent JSON:API shape: ApiCredentialTokenCreateModel.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string",
                "description": "Display label for the token (\"CI server\", \"Zapier\")."
              },
              "expires_in_days": {
                "type": "integer",
                "enum": [
                  30,
                  90,
                  365,
                  0
                ],
                "description": "Lifetime menu — 30, 90 or 365 days (default), or 0 for non-expiring."
              }
            }
          }
        }
      },
      "PropertyBlock": {
        "type": "object",
        "description": "One live block of a property, as a property read lists it: its name, kind and number, the member paddocks, and the area and outline derived from them. Nothing here is stored except the label itself.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "example": "Racecourse block"
          },
          "kind": {
            "type": "string",
            "enum": [
              "outblock",
              "irrigation_farm",
              "management"
            ]
          },
          "block_number": {
            "type": "string",
            "nullable": true,
            "description": "An optional number the business uses for the block, such as \"Farm 89\"."
          },
          "paddock_ids": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The ids of the live paddocks in the block, ordered by paddock name. Each of those paddocks has this block's id as its `block_id`."
          },
          "area_ha": {
            "type": "number",
            "nullable": true,
            "description": "The sum of the member paddocks' areas, in hectares. Null when no member has an area."
          },
          "hull": {
            "type": "object",
            "nullable": true,
            "description": "The convex hull of the member paddocks' boundaries as a GeoJSON geometry (usually a Polygon) in [lng, lat]. Null when no member has a boundary. For the map's dashed outline; not a boundary of its own.",
            "properties": {
              "type": {
                "type": "string",
                "example": "Polygon"
              },
              "coordinates": {
                "type": "array",
                "items": {}
              }
            }
          }
        }
      },
      "BlockAttributes": {
        "type": "object",
        "description": "A block's fields, as the block write endpoints answer them.",
        "properties": {
          "name": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "outblock",
              "irrigation_farm",
              "management"
            ]
          },
          "block_number": {
            "type": "string",
            "nullable": true
          },
          "property_id": {
            "type": "string",
            "format": "uuid"
          },
          "paddock_ids": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            }
          },
          "area_ha": {
            "type": "number",
            "nullable": true
          },
          "hull": {
            "type": "object",
            "nullable": true,
            "properties": {
              "type": {
                "type": "string"
              },
              "coordinates": {
                "type": "array",
                "items": {}
              }
            }
          },
          "archived_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "BlockResponse": {
        "type": "object",
        "description": "One block (JSON:API, Accept application/vnd.api+json).",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "type": {
                "type": "string",
                "enum": [
                  "block"
                ]
              },
              "attributes": {
                "$ref": "#/components/schemas/BlockAttributes"
              }
            }
          }
        }
      },
      "BlockResponseAttributes": {
        "type": "object",
        "description": "One block in attributes-adapter shape (Accept application/json).",
        "properties": {
          "data": {
            "allOf": [
              {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "type": {
                    "type": "string",
                    "example": "block"
                  }
                }
              },
              {
                "$ref": "#/components/schemas/BlockAttributes"
              }
            ]
          }
        }
      },
      "BlockCreateModel": {
        "type": "object",
        "description": "Nested JSON:API request body (Content-Type application/vnd.api+json) for \"Group into a block\". `paddock_ids` groups those live paddocks of the property into the new block. Equivalent flat shape: `BlockCreateModelAttributes` sent as application/json.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "block"
                ]
              },
              "attributes": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Unique among the property's live blocks, ignoring case."
                  },
                  "kind": {
                    "type": "string",
                    "enum": [
                      "outblock",
                      "irrigation_farm",
                      "management"
                    ]
                  },
                  "block_number": {
                    "type": "string",
                    "nullable": true,
                    "description": "Optional, for example \"Farm 89\"."
                  },
                  "paddock_ids": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "description": "Live paddocks of this property to put in the block."
                  }
                },
                "required": [
                  "name",
                  "kind"
                ]
              }
            }
          }
        },
        "required": [
          "data"
        ]
      },
      "BlockCreateModelAttributes": {
        "type": "object",
        "description": "Flat attributes-adapter request body (Content-Type application/json): the fields directly under `data`. Equivalent JSON:API shape: `BlockCreateModel`.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "kind": {
                "type": "string",
                "enum": [
                  "outblock",
                  "irrigation_farm",
                  "management"
                ]
              },
              "block_number": {
                "type": "string",
                "nullable": true
              },
              "paddock_ids": {
                "type": "array",
                "items": {
                  "type": "string",
                  "format": "uuid"
                }
              }
            },
            "required": [
              "name",
              "kind"
            ]
          }
        },
        "required": [
          "data"
        ]
      },
      "BlockUpdateModel": {
        "type": "object",
        "description": "Nested JSON:API request body (Content-Type application/vnd.api+json). Every field is optional. `paddock_ids`, when sent, replaces the membership; omitted, the members stay. Equivalent flat shape: `BlockUpdateModelAttributes`.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "type": {
                "type": "string",
                "enum": [
                  "block"
                ]
              },
              "attributes": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "kind": {
                    "type": "string",
                    "enum": [
                      "outblock",
                      "irrigation_farm",
                      "management"
                    ]
                  },
                  "block_number": {
                    "type": "string",
                    "nullable": true
                  },
                  "paddock_ids": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          }
        },
        "required": [
          "data"
        ]
      },
      "BlockUpdateModelAttributes": {
        "type": "object",
        "description": "Flat attributes-adapter request body (Content-Type application/json). Every field is optional. Equivalent JSON:API shape: `BlockUpdateModel`.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "kind": {
                "type": "string",
                "enum": [
                  "outblock",
                  "irrigation_farm",
                  "management"
                ]
              },
              "block_number": {
                "type": "string",
                "nullable": true
              },
              "paddock_ids": {
                "type": "array",
                "items": {
                  "type": "string",
                  "format": "uuid"
                }
              }
            }
          }
        },
        "required": [
          "data"
        ]
      },
      "ProductModel": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "product"
            ]
          },
          "attributes": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "active": {
                "type": "string",
                "nullable": true,
                "description": "Label active constituent."
              },
              "kind": {
                "type": "string",
                "enum": [
                  "drench",
                  "herbicide",
                  "fertiliser",
                  "seed",
                  "seed_treatment",
                  "inoculant"
                ]
              },
              "whp_days": {
                "type": "integer",
                "description": "The stored withholding period, days. What it means depends on `kind` (a herbicide's grazing period): prefer the explicit `meat_whp_days`, `grazing_whp_days` and so on below."
              },
              "esi_days": {
                "type": "integer",
                "description": "The stored second interval, days. For a drench the export slaughter interval, for a herbicide the harvest period: prefer the explicit names below."
              },
              "analysis": {
                "type": "object",
                "additionalProperties": {
                  "type": "number"
                },
                "description": "Fertiliser nutrient percentages by weight, lower-case keys, e.g. `{ \"n\": 46 }`. Copied onto a record line at save."
              },
              "advisory_note": {
                "type": "string",
                "nullable": true,
                "description": "Grey advice for a fertiliser (urea: wait three to four weeks before grazing, nitrate risk). Advice to pass on, never a status or a withholding period."
              },
              "meat_whp_days": {
                "type": "integer",
                "nullable": true,
                "description": "Meat withholding period, days. Drenches only; null for any other kind."
              },
              "export_esi_days": {
                "type": "integer",
                "nullable": true,
                "description": "Export slaughter interval, days. Drenches and seed treatments; null otherwise."
              },
              "grazing_whp_days": {
                "type": "integer",
                "nullable": true,
                "description": "Grazing withholding period, days. Herbicides and seed treatments; null otherwise."
              },
              "harvest_whp_days": {
                "type": "integer",
                "nullable": true,
                "description": "Harvest withholding period, days. Herbicides only; null otherwise."
              },
              "egi_days": {
                "type": "integer",
                "nullable": true,
                "description": "Export grazing interval, days. Herbicides only; null otherwise."
              }
            }
          }
        }
      },
      "ProductsIndexResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ProductModel"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta"
          }
        }
      },
      "ProductModelAttributes": {
        "type": "object",
        "description": "Product in attributes-adapter shape (Accept application/json); fields at top level.",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "active": {
            "type": "string",
            "nullable": true
          },
          "kind": {
            "type": "string",
            "enum": [
              "drench",
              "herbicide",
              "fertiliser",
              "seed",
              "seed_treatment",
              "inoculant"
            ]
          },
          "whp_days": {
            "type": "integer",
            "description": "The stored withholding period; what it means depends on `kind`. Prefer the explicit names."
          },
          "esi_days": {
            "type": "integer",
            "description": "The stored second interval; what it means depends on `kind`. Prefer the explicit names."
          },
          "analysis": {
            "type": "object",
            "additionalProperties": {
              "type": "number"
            },
            "description": "Fertiliser nutrient percentages by weight, lower-case keys, e.g. `{ \"n\": 46 }`. Copied onto a record line at save."
          },
          "advisory_note": {
            "type": "string",
            "nullable": true,
            "description": "Grey advice for a fertiliser (urea: wait three to four weeks before grazing, nitrate risk). Advice to pass on, never a status or a withholding period."
          },
          "meat_whp_days": {
            "type": "integer",
            "nullable": true,
            "description": "Meat withholding period, days. Drenches only; null for any other kind."
          },
          "export_esi_days": {
            "type": "integer",
            "nullable": true,
            "description": "Export slaughter interval, days. Drenches and seed treatments; null otherwise."
          },
          "grazing_whp_days": {
            "type": "integer",
            "nullable": true,
            "description": "Grazing withholding period, days. Herbicides and seed treatments; null otherwise."
          },
          "harvest_whp_days": {
            "type": "integer",
            "nullable": true,
            "description": "Harvest withholding period, days. Herbicides only; null otherwise."
          },
          "egi_days": {
            "type": "integer",
            "nullable": true,
            "description": "Export grazing interval, days. Herbicides only; null otherwise."
          }
        }
      },
      "ProductsIndexResponseAttributes": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ProductModelAttributes"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta"
          }
        }
      },
      "DrenchRecordCreateModel": {
        "type": "object",
        "description": "Nested JSON:API request body (application/vnd.api+json) for POST /mobs/{mob_id}/drench-records: `data.attributes`. Equivalent flat shape: `DrenchRecordCreateModelAttributes` as application/json.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "drench-record"
                ]
              },
              "attributes": {
                "$ref": "#/components/schemas/DrenchRecordFields"
              }
            }
          }
        }
      },
      "DrenchRecordCreateModelAttributes": {
        "type": "object",
        "description": "Flat request body (application/json) for POST /mobs/{mob_id}/drench-records: the fields directly under `data`. Equivalent nested shape: `DrenchRecordCreateModel`.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/DrenchRecordFields"
          }
        }
      },
      "DrenchRecordUpdateModel": {
        "type": "object",
        "description": "Nested JSON:API request body (application/vnd.api+json) for PUT /drench-records/{id}: `data.attributes` with `head` and/or `products` (products REPLACES the set). Equivalent flat shape: `DrenchRecordUpdateModelAttributes`.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "type": {
                "type": "string",
                "enum": [
                  "drench-record"
                ]
              },
              "attributes": {
                "$ref": "#/components/schemas/DrenchRecordFields"
              }
            }
          }
        }
      },
      "DrenchRecordUpdateModelAttributes": {
        "type": "object",
        "description": "Flat request body (application/json) for PUT /drench-records/{id}. Equivalent nested shape: `DrenchRecordUpdateModel`.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/DrenchRecordFields"
          }
        }
      },
      "DrenchRecordFields": {
        "type": "object",
        "properties": {
          "image_upload_ids": {
            "$ref": "#/components/schemas/ImageUploadIdList"
          },
          "image_urls": {
            "$ref": "#/components/schemas/ImageUrlList"
          },
          "image_base64s": {
            "$ref": "#/components/schemas/ImageBase64List"
          },
          "image_mode": {
            "$ref": "#/components/schemas/ImageMode"
          },
          "remove_image_ids": {
            "$ref": "#/components/schemas/ImageRemoveIdList"
          },
          "date": {
            "type": "string",
            "format": "date",
            "description": "The day the drench was done, `YYYY-MM-DD` (create only; default today). Back-date with a past date. The server stores midday in the application time zone, so the derived withholding dates do not slip a day. Do not send an offset datetime here — that belongs on a task's `completed_at`. A future date records a plan: it creates the open task instead of a done record. A treatment date after a product's expiry is a farmer decision; a product's `expiry` sits on its own line under `products`, not here.",
            "example": "2025-12-23"
          },
          "head": {
            "description": "Head treated (\"1,240\" is 1240). May be less than the mob. Defaults to the mob's current head count when omitted on create. The API does not compare the two, with one exception: a mob that has no head count yet takes this number as its count. A count already set is never changed by a drench.",
            "example": 11,
            "oneOf": [
              {
                "type": "integer",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "products": {
            "type": "array",
            "maxItems": 10,
            "description": "One entry per product given together. On update this REPLACES the set (position is the array index). A completed record with none is refused (422, \"A drench needs at least one product\").",
            "items": {
              "$ref": "#/components/schemas/DrenchProductWrite"
            }
          },
          "description": {
            "type": "string",
            "description": "Create only: the record's title. Defaults to \"Drenched {mob}\".",
            "example": "Drench rams"
          }
        }
      },
      "FertiliserProductRead": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "nullable": true,
            "description": "Product, e.g. `Urea`. Blank is valid."
          },
          "analysis": {
            "type": "object",
            "additionalProperties": {
              "type": "number"
            },
            "description": "Nutrient percentages, e.g. `{ \"N\": 46 }`, copied from the catalogue so a later edit never changes a past record."
          },
          "rate": {
            "type": "string",
            "nullable": true,
            "description": "The rate as entered, e.g. `100 kg/ha`. Kept verbatim."
          },
          "rate_value": {
            "type": "number",
            "minimum": 0,
            "nullable": true,
            "description": "The number in the rate. Parsed from `rate` when omitted."
          },
          "rate_unit": {
            "type": "string",
            "nullable": true,
            "enum": [
              "g",
              "kg",
              "t",
              "mL",
              "L"
            ],
            "description": "Unit of `rate_value`. Required when `rate_value` is set."
          },
          "rate_basis": {
            "type": "string",
            "nullable": true,
            "enum": [
              "ha",
              "total"
            ],
            "description": "What the rate is per."
          },
          "batch": {
            "type": "string",
            "nullable": true,
            "description": "Batch number. Never invent one."
          },
          "supplier": {
            "type": "string",
            "nullable": true,
            "description": "Who supplied it."
          },
          "advisory_note": {
            "type": "string",
            "nullable": true,
            "description": "Grey advice, never a status: e.g. wait three to four weeks before grazing."
          },
          "product_id": {
            "type": "string",
            "nullable": true,
            "description": "Optional link to the catalogue."
          },
          "position": {
            "type": "integer",
            "description": "Order the products were entered in."
          },
          "total_kg": {
            "type": "number",
            "nullable": true,
            "description": "The whole quantity put on, in kilograms: the rate per hectare times the area covered. Null when either is missing."
          }
        }
      },
      "FertiliserRead": {
        "type": "object",
        "description": "The fertiliser of a task or record. Null on a task that has none.",
        "properties": {
          "id": {
            "type": "string",
            "description": "The FertiliserRecord id."
          },
          "nutrients_applied": {
            "type": "object",
            "additionalProperties": {
              "type": "number"
            },
            "description": "Kilograms per hectare of each nutrient across the lines that state an analysis and a rate per hectare in kg or t. Worked out when read."
          },
          "area_ha": {
            "type": "number",
            "minimum": 0,
            "nullable": true,
            "description": "Hectares covered; separators are stripped. Defaults to the paddock's area."
          },
          "method": {
            "type": "string",
            "nullable": true,
            "enum": [
              "broadcast",
              "banded",
              "drilled",
              "in_furrow",
              "fertigation",
              "foliar"
            ],
            "description": "How it was applied."
          },
          "incorporation": {
            "type": "string",
            "nullable": true,
            "enum": [
              "none",
              "mechanical",
              "rain",
              "irrigation"
            ],
            "description": "How it was incorporated."
          },
          "incorporated_at": {
            "type": "string",
            "nullable": true,
            "description": "When it was incorporated, an ISO 8601 time.",
            "format": "date-time"
          },
          "spreader": {
            "type": "string",
            "nullable": true,
            "description": "The spreader used."
          },
          "bout_width_m": {
            "type": "number",
            "minimum": 0,
            "nullable": true,
            "description": "Spread width, metres."
          },
          "cost_per_ha": {
            "type": "number",
            "minimum": 0,
            "nullable": true,
            "description": "Cost per hectare, for gross margins."
          },
          "products": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FertiliserProductRead"
            }
          }
        }
      },
      "FertiliserProductWrite": {
        "type": "object",
        "description": "One fertiliser product line. Never invent a batch, expiry, rate or withholding period.",
        "properties": {
          "name": {
            "type": "string",
            "nullable": true,
            "description": "Product, e.g. `Urea`. Blank is valid."
          },
          "analysis": {
            "type": "object",
            "additionalProperties": {
              "type": "number"
            },
            "description": "Nutrient percentages, e.g. `{ \"N\": 46 }`, copied from the catalogue so a later edit never changes a past record."
          },
          "rate": {
            "type": "string",
            "nullable": true,
            "description": "The rate as entered, e.g. `100 kg/ha`. Kept verbatim."
          },
          "rate_value": {
            "description": "The number in the rate. Parsed from `rate` when omitted.",
            "oneOf": [
              {
                "type": "number",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "rate_unit": {
            "type": "string",
            "nullable": true,
            "enum": [
              "g",
              "kg",
              "t",
              "mL",
              "L"
            ],
            "description": "Unit of `rate_value`. Required when `rate_value` is set."
          },
          "rate_basis": {
            "type": "string",
            "nullable": true,
            "enum": [
              "ha",
              "total"
            ],
            "description": "What the rate is per."
          },
          "batch": {
            "type": "string",
            "nullable": true,
            "description": "Batch number. Never invent one."
          },
          "supplier": {
            "type": "string",
            "nullable": true,
            "description": "Who supplied it."
          },
          "advisory_note": {
            "type": "string",
            "nullable": true,
            "description": "Grey advice, never a status: e.g. wait three to four weeks before grazing."
          },
          "product_id": {
            "type": "string",
            "nullable": true,
            "description": "Optional link to the catalogue."
          }
        }
      },
      "FertiliserWrite": {
        "type": "object",
        "description": "Writes the fertiliser child of a task. `products` REPLACES the set (at most 10).",
        "properties": {
          "area_ha": {
            "description": "Hectares covered; separators are stripped. Defaults to the paddock's area.",
            "oneOf": [
              {
                "type": "number",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "method": {
            "type": "string",
            "nullable": true,
            "enum": [
              "broadcast",
              "banded",
              "drilled",
              "in_furrow",
              "fertigation",
              "foliar"
            ],
            "description": "How it was applied."
          },
          "incorporation": {
            "type": "string",
            "nullable": true,
            "enum": [
              "none",
              "mechanical",
              "rain",
              "irrigation"
            ],
            "description": "How it was incorporated."
          },
          "incorporated_at": {
            "type": "string",
            "nullable": true,
            "description": "When it was incorporated, an ISO 8601 time.",
            "format": "date-time"
          },
          "spreader": {
            "type": "string",
            "nullable": true,
            "description": "The spreader used."
          },
          "bout_width_m": {
            "description": "Spread width, metres.",
            "oneOf": [
              {
                "type": "number",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "cost_per_ha": {
            "description": "Cost per hectare, for gross margins.",
            "oneOf": [
              {
                "type": "number",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "products": {
            "type": "array",
            "maxItems": 10,
            "description": "One entry per product line. On update this REPLACES the set (position is the array index).",
            "items": {
              "$ref": "#/components/schemas/FertiliserProductWrite"
            }
          }
        }
      },
      "FertiliserRecordFields": {
        "type": "object",
        "properties": {
          "image_upload_ids": {
            "$ref": "#/components/schemas/ImageUploadIdList"
          },
          "image_urls": {
            "$ref": "#/components/schemas/ImageUrlList"
          },
          "image_base64s": {
            "$ref": "#/components/schemas/ImageBase64List"
          },
          "image_mode": {
            "$ref": "#/components/schemas/ImageMode"
          },
          "remove_image_ids": {
            "$ref": "#/components/schemas/ImageRemoveIdList"
          },
          "date": {
            "type": "string",
            "format": "date",
            "description": "The day it was done, `YYYY-MM-DD` (create only; default today). The server stores midday in the application time zone so derived dates do not slip a day. A future date records a plan: it creates the open task instead of a done record.",
            "example": "2026-09-28"
          },
          "description": {
            "type": "string",
            "description": "Create only: the record's title."
          },
          "area_ha": {
            "description": "Hectares covered; separators are stripped. Defaults to the paddock's area.",
            "oneOf": [
              {
                "type": "number",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "method": {
            "type": "string",
            "nullable": true,
            "enum": [
              "broadcast",
              "banded",
              "drilled",
              "in_furrow",
              "fertigation",
              "foliar"
            ],
            "description": "How it was applied."
          },
          "incorporation": {
            "type": "string",
            "nullable": true,
            "enum": [
              "none",
              "mechanical",
              "rain",
              "irrigation"
            ],
            "description": "How it was incorporated."
          },
          "incorporated_at": {
            "type": "string",
            "nullable": true,
            "description": "When it was incorporated, an ISO 8601 time.",
            "format": "date-time"
          },
          "spreader": {
            "type": "string",
            "nullable": true,
            "description": "The spreader used."
          },
          "bout_width_m": {
            "description": "Spread width, metres.",
            "oneOf": [
              {
                "type": "number",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "cost_per_ha": {
            "description": "Cost per hectare, for gross margins.",
            "oneOf": [
              {
                "type": "number",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "products": {
            "type": "array",
            "maxItems": 10,
            "description": "One entry per product line. On update this REPLACES the set (position is the array index).",
            "items": {
              "$ref": "#/components/schemas/FertiliserProductWrite"
            }
          }
        }
      },
      "FertiliserRecordCreateModel": {
        "type": "object",
        "description": "Nested JSON:API request body (application/vnd.api+json) for POST /paddocks/{paddock_id}/fertiliser-records: `data.attributes`. Equivalent flat shape: `FertiliserRecordCreateModelAttributes` as application/json.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "fertiliser-record"
                ]
              },
              "attributes": {
                "$ref": "#/components/schemas/FertiliserRecordFields"
              }
            }
          }
        }
      },
      "FertiliserRecordCreateModelAttributes": {
        "type": "object",
        "description": "Flat request body (application/json) for POST /paddocks/{paddock_id}/fertiliser-records: the fields directly under `data`. Equivalent nested shape: `FertiliserRecordCreateModel`.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/FertiliserRecordFields"
          }
        }
      },
      "FertiliserRecordUpdateModel": {
        "type": "object",
        "description": "Nested JSON:API request body (application/vnd.api+json) for PUT /fertiliser-records/{id}: `data.attributes` with any of the fields (products REPLACES the set). Equivalent flat shape: `FertiliserRecordUpdateModelAttributes`.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "type": {
                "type": "string",
                "enum": [
                  "fertiliser-record"
                ]
              },
              "attributes": {
                "$ref": "#/components/schemas/FertiliserRecordFields"
              }
            }
          }
        }
      },
      "FertiliserRecordUpdateModelAttributes": {
        "type": "object",
        "description": "Flat request body (application/json) for PUT /fertiliser-records/{id}. Equivalent nested shape: `FertiliserRecordUpdateModel`.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/FertiliserRecordFields"
          }
        }
      },
      "FieldworkProductRead": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "nullable": true,
            "description": "Optional: a hay preservative. Blank is valid."
          },
          "active": {
            "type": "string",
            "nullable": true,
            "description": "Active constituent."
          },
          "batch": {
            "type": "string",
            "nullable": true,
            "description": "Batch number. Never invent one."
          },
          "expiry": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "description": "Expiry date, `YYYY-MM-DD`."
          },
          "rate": {
            "type": "string",
            "nullable": true,
            "description": "The rate as entered. Kept verbatim."
          },
          "rate_value": {
            "type": "number",
            "minimum": 0,
            "nullable": true,
            "description": "The number in the rate. Parsed from `rate` when omitted."
          },
          "rate_unit": {
            "type": "string",
            "nullable": true,
            "enum": [
              "g",
              "kg",
              "t",
              "mL",
              "L",
              "seeds",
              "%"
            ],
            "description": "Unit of `rate_value`."
          },
          "rate_basis": {
            "type": "string",
            "nullable": true,
            "enum": [
              "ha",
              "head",
              "100_kg_seed",
              "100_l",
              "total"
            ],
            "description": "What the rate is per."
          },
          "product_id": {
            "type": "string",
            "nullable": true,
            "description": "Optional link to the catalogue."
          },
          "position": {
            "type": "integer",
            "description": "Order the products were entered in."
          }
        }
      },
      "FieldworkRead": {
        "type": "object",
        "description": "The field work of a task or record. Null on a task that has none.",
        "properties": {
          "id": {
            "type": "string",
            "description": "The FieldworkRecord id."
          },
          "kind": {
            "type": "string",
            "nullable": true,
            "enum": [
              "cultivate",
              "harvest",
              "hay_silage",
              "slash",
              "roll",
              "burn",
              "irrigate"
            ],
            "description": "What was done."
          },
          "area_ha": {
            "type": "number",
            "minimum": 0,
            "nullable": true,
            "description": "Hectares worked; separators are stripped. Defaults to the paddock's area."
          },
          "implement": {
            "type": "string",
            "nullable": true,
            "description": "The implement or machine, e.g. `Case 8250`."
          },
          "outcomes": {
            "type": "object",
            "additionalProperties": true,
            "description": "One block per kind, e.g. `{ \"harvest\": { \"tonnes\": 399, \"yield_t_ha\": 4.2, \"grade\": \"Malt 1\" } }`."
          },
          "products": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FieldworkProductRead"
            }
          }
        }
      },
      "FieldworkProductWrite": {
        "type": "object",
        "description": "One field work product line. Never invent a batch, expiry, rate or withholding period.",
        "properties": {
          "name": {
            "type": "string",
            "nullable": true,
            "description": "Optional: a hay preservative. Blank is valid."
          },
          "active": {
            "type": "string",
            "nullable": true,
            "description": "Active constituent."
          },
          "batch": {
            "type": "string",
            "nullable": true,
            "description": "Batch number. Never invent one."
          },
          "expiry": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "description": "Expiry date, `YYYY-MM-DD`."
          },
          "rate": {
            "type": "string",
            "nullable": true,
            "description": "The rate as entered. Kept verbatim."
          },
          "rate_value": {
            "description": "The number in the rate. Parsed from `rate` when omitted.",
            "oneOf": [
              {
                "type": "number",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "rate_unit": {
            "type": "string",
            "nullable": true,
            "enum": [
              "g",
              "kg",
              "t",
              "mL",
              "L",
              "seeds",
              "%"
            ],
            "description": "Unit of `rate_value`."
          },
          "rate_basis": {
            "type": "string",
            "nullable": true,
            "enum": [
              "ha",
              "head",
              "100_kg_seed",
              "100_l",
              "total"
            ],
            "description": "What the rate is per."
          },
          "product_id": {
            "type": "string",
            "nullable": true,
            "description": "Optional link to the catalogue."
          }
        }
      },
      "FieldworkWrite": {
        "type": "object",
        "description": "Writes the field work child of a task. `products` REPLACES the set (at most 10).",
        "properties": {
          "kind": {
            "type": "string",
            "nullable": true,
            "enum": [
              "cultivate",
              "harvest",
              "hay_silage",
              "slash",
              "roll",
              "burn",
              "irrigate"
            ],
            "description": "What was done."
          },
          "area_ha": {
            "description": "Hectares worked; separators are stripped. Defaults to the paddock's area.",
            "oneOf": [
              {
                "type": "number",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "implement": {
            "type": "string",
            "nullable": true,
            "description": "The implement or machine, e.g. `Case 8250`."
          },
          "outcomes": {
            "type": "object",
            "additionalProperties": true,
            "description": "One block per kind, e.g. `{ \"harvest\": { \"tonnes\": 399, \"yield_t_ha\": 4.2, \"grade\": \"Malt 1\" } }`."
          },
          "products": {
            "type": "array",
            "maxItems": 10,
            "description": "One entry per product line. On update this REPLACES the set (position is the array index).",
            "items": {
              "$ref": "#/components/schemas/FieldworkProductWrite"
            }
          }
        }
      },
      "FieldworkRecordFields": {
        "type": "object",
        "properties": {
          "image_upload_ids": {
            "$ref": "#/components/schemas/ImageUploadIdList"
          },
          "image_urls": {
            "$ref": "#/components/schemas/ImageUrlList"
          },
          "image_base64s": {
            "$ref": "#/components/schemas/ImageBase64List"
          },
          "image_mode": {
            "$ref": "#/components/schemas/ImageMode"
          },
          "remove_image_ids": {
            "$ref": "#/components/schemas/ImageRemoveIdList"
          },
          "date": {
            "type": "string",
            "format": "date",
            "description": "The day it was done, `YYYY-MM-DD` (create only; default today). The server stores midday in the application time zone so derived dates do not slip a day. A future date records a plan: it creates the open task instead of a done record.",
            "example": "2026-09-28"
          },
          "description": {
            "type": "string",
            "description": "Create only: the record's title."
          },
          "kind": {
            "type": "string",
            "nullable": true,
            "enum": [
              "cultivate",
              "harvest",
              "hay_silage",
              "slash",
              "roll",
              "burn",
              "irrigate"
            ],
            "description": "What was done."
          },
          "area_ha": {
            "description": "Hectares worked; separators are stripped. Defaults to the paddock's area.",
            "oneOf": [
              {
                "type": "number",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "implement": {
            "type": "string",
            "nullable": true,
            "description": "The implement or machine, e.g. `Case 8250`."
          },
          "outcomes": {
            "type": "object",
            "additionalProperties": true,
            "description": "One block per kind, e.g. `{ \"harvest\": { \"tonnes\": 399, \"yield_t_ha\": 4.2, \"grade\": \"Malt 1\" } }`."
          },
          "products": {
            "type": "array",
            "maxItems": 10,
            "description": "One entry per product line. On update this REPLACES the set (position is the array index).",
            "items": {
              "$ref": "#/components/schemas/FieldworkProductWrite"
            }
          }
        }
      },
      "FieldworkRecordCreateModel": {
        "type": "object",
        "description": "Nested JSON:API request body (application/vnd.api+json) for POST /paddocks/{paddock_id}/fieldwork-records: `data.attributes`. Equivalent flat shape: `FieldworkRecordCreateModelAttributes` as application/json.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "fieldwork-record"
                ]
              },
              "attributes": {
                "$ref": "#/components/schemas/FieldworkRecordFields"
              }
            }
          }
        }
      },
      "FieldworkRecordCreateModelAttributes": {
        "type": "object",
        "description": "Flat request body (application/json) for POST /paddocks/{paddock_id}/fieldwork-records: the fields directly under `data`. Equivalent nested shape: `FieldworkRecordCreateModel`.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/FieldworkRecordFields"
          }
        }
      },
      "FieldworkRecordUpdateModel": {
        "type": "object",
        "description": "Nested JSON:API request body (application/vnd.api+json) for PUT /fieldwork-records/{id}: `data.attributes` with any of the fields (products REPLACES the set). Equivalent flat shape: `FieldworkRecordUpdateModelAttributes`.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "type": {
                "type": "string",
                "enum": [
                  "fieldwork-record"
                ]
              },
              "attributes": {
                "$ref": "#/components/schemas/FieldworkRecordFields"
              }
            }
          }
        }
      },
      "FieldworkRecordUpdateModelAttributes": {
        "type": "object",
        "description": "Flat request body (application/json) for PUT /fieldwork-records/{id}. Equivalent nested shape: `FieldworkRecordUpdateModel`.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/FieldworkRecordFields"
          }
        }
      },
      "HusbandryProductRead": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "nullable": true,
            "description": "Product trade name. Blank is valid, but a blank name contributes nothing to the derived withholding dates."
          },
          "active": {
            "type": "string",
            "nullable": true,
            "description": "Label active constituent, e.g. `Moxidectin 1 g/L`."
          },
          "batch": {
            "type": "string",
            "nullable": true,
            "description": "Batch number from the pack. Never invent one; leave it unset until the farmer confirms it."
          },
          "expiry": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "description": "Expiry date on the pack, `YYYY-MM-DD`. Never invent one; unset prints `not recorded`."
          },
          "dose": {
            "type": "string",
            "nullable": true,
            "description": "The dose as entered, e.g. `5 mL / 20 kg`. Kept verbatim."
          },
          "dose_value": {
            "type": "number",
            "minimum": 0,
            "nullable": true,
            "description": "The number in the dose, e.g. 5. Only what the caller sends; it is never parsed from `dose`."
          },
          "dose_unit": {
            "type": "string",
            "nullable": true,
            "enum": [
              "mL",
              "L",
              "g",
              "kg"
            ],
            "description": "Unit of `dose_value`."
          },
          "dose_basis": {
            "type": "string",
            "nullable": true,
            "enum": [
              "per_head",
              "per_kg",
              "per_mob"
            ],
            "description": "What `dose_value` is per."
          },
          "drench_group": {
            "type": "string",
            "nullable": true,
            "description": "Chemical group, e.g. `BZ`, `LV`, `ML`, `AD`, `SI`, `OP`, `TZ`, `SA`, `IQ` or a combination, for resistance tracking."
          },
          "whp_days": {
            "type": "integer",
            "minimum": 0,
            "nullable": true,
            "description": "Meat withholding period from the label, in whole days. \"14 d\" is read as 14. Never invent it."
          },
          "esi_days": {
            "type": "integer",
            "minimum": 0,
            "nullable": true,
            "description": "Export slaughter interval from the label, in whole days. If the label gives none, send the WHP. Never invent it."
          },
          "wool_interval_days": {
            "type": "integer",
            "minimum": 0,
            "nullable": true,
            "description": "Wool harvest interval, days. 0 is none stated."
          },
          "milk_whp_days": {
            "type": "integer",
            "minimum": 0,
            "nullable": true,
            "description": "Milk withholding period, days. 0 is none stated."
          },
          "product_id": {
            "type": "string",
            "nullable": true,
            "description": "Optional link to the catalogue (`listProducts`). The values here are copied at save, so a later catalogue edit never changes a past record."
          },
          "position": {
            "type": "integer",
            "description": "Order the products were entered in."
          }
        }
      },
      "HusbandryRead": {
        "type": "object",
        "description": "The husbandry of a task or record. Null on a task that has none.",
        "properties": {
          "id": {
            "type": "string",
            "description": "The HusbandryRecord id."
          },
          "marking_percent": {
            "type": "integer",
            "nullable": true,
            "description": "Lambs marked as a share of ewes joined, worked out when read. Null until both are said."
          },
          "safe_for_slaughter_on": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "description": "DERIVED from the products' meat WHP; null when no named product carries one."
          },
          "export_eligible_on": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "description": "DERIVED from the products' ESI."
          },
          "head": {
            "type": "integer",
            "minimum": 0,
            "nullable": true,
            "description": "Head the work was done to; separators are stripped. Defaults to the mob's head count."
          },
          "procedures": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "mark",
                "eid_tag",
                "tail_dock",
                "castrate",
                "mules",
                "shear",
                "crutch",
                "wean",
                "scan",
                "class",
                "weigh",
                "dehorn",
                "spay",
                "other"
              ]
            },
            "description": "What was done. One or more."
          },
          "method": {
            "type": "string",
            "nullable": true,
            "description": "Free text, e.g. rings or knife."
          },
          "outcomes": {
            "type": "object",
            "additionalProperties": true,
            "description": "One block per procedure, e.g. `{ \"mark\": { \"lambs_marked\": 412, \"ewes_joined\": 350 } }`. Figures are numbers, zero or more, except `shearer`, which is text."
          },
          "products": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/HusbandryProductRead"
            }
          }
        }
      },
      "HusbandryProductWrite": {
        "type": "object",
        "description": "One husbandry product line. Never invent a batch, expiry, rate or withholding period.",
        "properties": {
          "name": {
            "type": "string",
            "nullable": true,
            "description": "Product trade name. Blank is valid, but a blank name contributes nothing to the derived withholding dates."
          },
          "active": {
            "type": "string",
            "nullable": true,
            "description": "Label active constituent, e.g. `Moxidectin 1 g/L`."
          },
          "batch": {
            "type": "string",
            "nullable": true,
            "description": "Batch number from the pack. Never invent one; leave it unset until the farmer confirms it."
          },
          "expiry": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "description": "Expiry date on the pack, `YYYY-MM-DD`. Never invent one; unset prints `not recorded`."
          },
          "dose": {
            "type": "string",
            "nullable": true,
            "description": "The dose as entered, e.g. `5 mL / 20 kg`. Kept verbatim."
          },
          "dose_value": {
            "description": "The number in the dose, e.g. 5. Only what the caller sends; it is never parsed from `dose`.",
            "oneOf": [
              {
                "type": "number",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "dose_unit": {
            "type": "string",
            "nullable": true,
            "enum": [
              "mL",
              "L",
              "g",
              "kg"
            ],
            "description": "Unit of `dose_value`."
          },
          "dose_basis": {
            "type": "string",
            "nullable": true,
            "enum": [
              "per_head",
              "per_kg",
              "per_mob"
            ],
            "description": "What `dose_value` is per."
          },
          "drench_group": {
            "type": "string",
            "nullable": true,
            "description": "Chemical group, e.g. `BZ`, `LV`, `ML`, `AD`, `SI`, `OP`, `TZ`, `SA`, `IQ` or a combination, for resistance tracking."
          },
          "whp_days": {
            "description": "Meat withholding period from the label, in whole days. \"14 d\" is read as 14. Never invent it.",
            "oneOf": [
              {
                "type": "integer",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "esi_days": {
            "description": "Export slaughter interval from the label, in whole days. If the label gives none, send the WHP. Never invent it.",
            "oneOf": [
              {
                "type": "integer",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "wool_interval_days": {
            "description": "Wool harvest interval, days. 0 is none stated.",
            "oneOf": [
              {
                "type": "integer",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "milk_whp_days": {
            "description": "Milk withholding period, days. 0 is none stated.",
            "oneOf": [
              {
                "type": "integer",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "product_id": {
            "type": "string",
            "nullable": true,
            "description": "Optional link to the catalogue (`listProducts`). The values here are copied at save, so a later catalogue edit never changes a past record."
          }
        }
      },
      "HusbandryWrite": {
        "type": "object",
        "description": "Writes the husbandry child of a task. `products` REPLACES the set (at most 10).",
        "properties": {
          "head": {
            "description": "Head the work was done to; separators are stripped. Defaults to the mob's head count.",
            "example": 412,
            "oneOf": [
              {
                "type": "integer",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "procedures": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "mark",
                "eid_tag",
                "tail_dock",
                "castrate",
                "mules",
                "shear",
                "crutch",
                "wean",
                "scan",
                "class",
                "weigh",
                "dehorn",
                "spay",
                "other"
              ]
            },
            "description": "What was done. One or more."
          },
          "method": {
            "type": "string",
            "nullable": true,
            "description": "Free text, e.g. rings or knife."
          },
          "outcomes": {
            "type": "object",
            "additionalProperties": true,
            "description": "One block per procedure, e.g. `{ \"mark\": { \"lambs_marked\": 412, \"ewes_joined\": 350 } }`. Figures are numbers, zero or more, except `shearer`, which is text."
          },
          "products": {
            "type": "array",
            "maxItems": 10,
            "description": "One entry per product line. On update this REPLACES the set (position is the array index).",
            "items": {
              "$ref": "#/components/schemas/HusbandryProductWrite"
            }
          }
        }
      },
      "HusbandryRecordFields": {
        "type": "object",
        "properties": {
          "image_upload_ids": {
            "$ref": "#/components/schemas/ImageUploadIdList"
          },
          "image_urls": {
            "$ref": "#/components/schemas/ImageUrlList"
          },
          "image_base64s": {
            "$ref": "#/components/schemas/ImageBase64List"
          },
          "image_mode": {
            "$ref": "#/components/schemas/ImageMode"
          },
          "remove_image_ids": {
            "$ref": "#/components/schemas/ImageRemoveIdList"
          },
          "date": {
            "type": "string",
            "format": "date",
            "description": "The day it was done, `YYYY-MM-DD` (create only; default today). The server stores midday in the application time zone so derived dates do not slip a day. A future date records a plan: it creates the open task instead of a done record.",
            "example": "2026-09-28"
          },
          "description": {
            "type": "string",
            "description": "Create only: the record's title."
          },
          "head": {
            "description": "Head the work was done to; separators are stripped. Defaults to the mob's head count.",
            "example": 412,
            "oneOf": [
              {
                "type": "integer",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "procedures": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "mark",
                "eid_tag",
                "tail_dock",
                "castrate",
                "mules",
                "shear",
                "crutch",
                "wean",
                "scan",
                "class",
                "weigh",
                "dehorn",
                "spay",
                "other"
              ]
            },
            "description": "What was done. One or more."
          },
          "method": {
            "type": "string",
            "nullable": true,
            "description": "Free text, e.g. rings or knife."
          },
          "outcomes": {
            "type": "object",
            "additionalProperties": true,
            "description": "One block per procedure, e.g. `{ \"mark\": { \"lambs_marked\": 412, \"ewes_joined\": 350 } }`. Figures are numbers, zero or more, except `shearer`, which is text."
          },
          "products": {
            "type": "array",
            "maxItems": 10,
            "description": "One entry per product line. On update this REPLACES the set (position is the array index).",
            "items": {
              "$ref": "#/components/schemas/HusbandryProductWrite"
            }
          }
        }
      },
      "HusbandryRecordCreateModel": {
        "type": "object",
        "description": "Nested JSON:API request body (application/vnd.api+json) for POST /mobs/{mob_id}/husbandry-records: `data.attributes`. Equivalent flat shape: `HusbandryRecordCreateModelAttributes` as application/json.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "husbandry-record"
                ]
              },
              "attributes": {
                "$ref": "#/components/schemas/HusbandryRecordFields"
              }
            }
          }
        }
      },
      "HusbandryRecordCreateModelAttributes": {
        "type": "object",
        "description": "Flat request body (application/json) for POST /mobs/{mob_id}/husbandry-records: the fields directly under `data`. Equivalent nested shape: `HusbandryRecordCreateModel`.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/HusbandryRecordFields"
          }
        }
      },
      "HusbandryRecordUpdateModel": {
        "type": "object",
        "description": "Nested JSON:API request body (application/vnd.api+json) for PUT /husbandry-records/{id}: `data.attributes` with any of the fields (products REPLACES the set). Equivalent flat shape: `HusbandryRecordUpdateModelAttributes`.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "type": {
                "type": "string",
                "enum": [
                  "husbandry-record"
                ]
              },
              "attributes": {
                "$ref": "#/components/schemas/HusbandryRecordFields"
              }
            }
          }
        }
      },
      "HusbandryRecordUpdateModelAttributes": {
        "type": "object",
        "description": "Flat request body (application/json) for PUT /husbandry-records/{id}. Equivalent nested shape: `HusbandryRecordUpdateModel`.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/HusbandryRecordFields"
          }
        }
      },
      "JoinLinkPreviewResponse": {
        "type": "object",
        "description": "Public preview of a business join link (`GET /join/{join_code}`). Plain JSON\n(not JSON:API) — the same shape is returned for both Accept types. A link\nthat cannot be used, for any reason, is a 404 and says nothing about why.\n",
        "properties": {
          "join": {
            "type": "object",
            "properties": {
              "organization_name": {
                "type": "string"
              },
              "organization_avatar_url": {
                "type": "string",
                "nullable": true
              },
              "shared_by": {
                "type": "string",
                "description": "Who shared the link — the owner who turned it on; falls back to the organization's owner or its name. Name only."
              },
              "role": {
                "type": "string",
                "enum": [
                  "member",
                  "viewer"
                ],
                "description": "What the visitor will become."
              }
            },
            "required": [
              "organization_name",
              "shared_by",
              "role"
            ]
          }
        },
        "required": [
          "join"
        ]
      },
      "OauthTokenModel": {
        "type": "object",
        "description": "Access-token metadata (a session, MCP connection, or api-key token).\nThe token and refresh-token strings are credentials and are never\nreturned by any endpoint.\n",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "example": "oauth-token"
          },
          "attributes": {
            "type": "object",
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "web_or_mobile",
                  "api",
                  "mcp"
                ],
                "description": "Channel of the application the token was issued to."
              },
              "name": {
                "type": "string",
                "nullable": true,
                "description": "Display label given at mint time (UI-minted api-credential tokens only)."
              },
              "application_name": {
                "type": "string"
              },
              "application_uid": {
                "type": "string",
                "description": "The application's client_id (matches api-credential ids)."
              },
              "scopes": {
                "type": "string"
              },
              "organization_id": {
                "type": "string",
                "format": "uuid",
                "nullable": true,
                "description": "Acting-as organization carried by the token, if any."
              },
              "created_at": {
                "type": "string",
                "format": "date-time"
              },
              "expires_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "current": {
                "type": "boolean",
                "description": "True for the token making this request (\"this device\")."
              },
              "last_used_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "When an AI-assistant connection last made a request, to the minute; null until it has, and on sessions and api-key tokens."
              },
              "new_organization_access": {
                "type": "string",
                "enum": [
                  "none",
                  "read",
                  "role"
                ],
                "description": "An AI-assistant connection's access in a business the person joins later. `role` is read and write where the role allows it, read only elsewhere."
              },
              "grants": {
                "type": "array",
                "description": "An AI-assistant connection's grants, one per business plus the person's own account (`organization_id` null). Empty on a session, an api-key token, or a connection made before grants existed, which acts on its scopes alone.",
                "items": {
                  "$ref": "#/components/schemas/OauthTokenGrant"
                }
              }
            }
          }
        }
      },
      "OauthTokensIndexResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OauthTokenModel"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta"
          }
        }
      },
      "OauthTokenModelAttributes": {
        "type": "object",
        "description": "Single token in attributes-adapter shape (Accept application/json).",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "example": "oauth-token"
          },
          "kind": {
            "type": "string",
            "enum": [
              "web_or_mobile",
              "api",
              "mcp"
            ]
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "application_name": {
            "type": "string"
          },
          "application_uid": {
            "type": "string"
          },
          "scopes": {
            "type": "string"
          },
          "organization_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "current": {
            "type": "boolean"
          },
          "last_used_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When an AI-assistant connection last made a request, to the minute; null until it has, and on sessions and api-key tokens."
          },
          "new_organization_access": {
            "type": "string",
            "enum": [
              "none",
              "read",
              "role"
            ],
            "description": "An AI-assistant connection's access in a business the person joins later. `role` is read and write where the role allows it, read only elsewhere."
          },
          "grants": {
            "type": "array",
            "description": "An AI-assistant connection's grants, one per business plus the person's own account (`organization_id` null). Empty on a session, an api-key token, or a connection made before grants existed, which acts on its scopes alone.",
            "items": {
              "$ref": "#/components/schemas/OauthTokenGrant"
            }
          }
        }
      },
      "OauthTokensIndexResponseAttributes": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OauthTokenModelAttributes"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta"
          }
        }
      },
      "MintedOauthTokenModel": {
        "type": "object",
        "description": "Mint response ONLY (POST /api-credentials/{id}/tokens): the token\nmetadata plus the token string, which appears here exactly once and is\nnever returned by any other endpoint.\n",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "example": "oauth-token"
          },
          "attributes": {
            "type": "object",
            "properties": {
              "token": {
                "type": "string",
                "description": "The bearer token string (show-once — store it immediately)."
              },
              "kind": {
                "type": "string",
                "enum": [
                  "web_or_mobile",
                  "api",
                  "mcp"
                ]
              },
              "name": {
                "type": "string",
                "nullable": true
              },
              "application_name": {
                "type": "string"
              },
              "application_uid": {
                "type": "string"
              },
              "scopes": {
                "type": "string"
              },
              "organization_id": {
                "type": "string",
                "format": "uuid",
                "nullable": true
              },
              "created_at": {
                "type": "string",
                "format": "date-time"
              },
              "expires_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "Null for non-expiring tokens (expires_in_days 0)."
              },
              "current": {
                "type": "boolean"
              }
            }
          }
        }
      },
      "MintedOauthTokenModelAttributes": {
        "type": "object",
        "description": "Mint response in attributes-adapter shape (Accept application/json) — the one place the token string appears.",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "example": "oauth-token"
          },
          "token": {
            "type": "string",
            "description": "The bearer token string (show-once — store it immediately)."
          },
          "kind": {
            "type": "string",
            "enum": [
              "web_or_mobile",
              "api",
              "mcp"
            ]
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "application_name": {
            "type": "string"
          },
          "application_uid": {
            "type": "string"
          },
          "scopes": {
            "type": "string"
          },
          "organization_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Null for non-expiring tokens (expires_in_days 0)."
          },
          "current": {
            "type": "boolean"
          }
        }
      },
      "MintedOauthTokenResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/MintedOauthTokenModel"
          }
        }
      },
      "MintedOauthTokenResponseAttributes": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/MintedOauthTokenModelAttributes"
          }
        }
      },
      "OauthTokenGrant": {
        "type": "object",
        "description": "What an AI-assistant connection may do in one business, or on the person's own account.",
        "properties": {
          "organization_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "The business; null is the person's own account."
          },
          "access": {
            "type": "string",
            "enum": [
              "none",
              "read",
              "write"
            ]
          }
        },
        "required": [
          "access"
        ]
      },
      "OauthTokenResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/OauthTokenModel"
          }
        }
      },
      "OauthTokenResponseAttributes": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/OauthTokenModelAttributes"
          }
        }
      },
      "OauthTokenUpdate": {
        "type": "object",
        "description": "Manage connection write (Content-Type application/vnd.api+json). Send only what changes.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "example": "oauth-token"
              },
              "id": {
                "type": "string"
              },
              "attributes": {
                "type": "object",
                "properties": {
                  "new_organization_access": {
                    "type": "string",
                    "enum": [
                      "none",
                      "read",
                      "role"
                    ]
                  },
                  "grants": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/OauthTokenGrant"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "OauthTokenUpdateAttributes": {
        "type": "object",
        "description": "Manage connection write in flat shape (Content-Type application/json). Send only what changes.",
        "properties": {
          "new_organization_access": {
            "type": "string",
            "enum": [
              "none",
              "read",
              "role"
            ]
          },
          "grants": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OauthTokenGrant"
            }
          }
        }
      },
      "ConnectionAccessRow": {
        "type": "object",
        "description": "What the connection may do in one place, and the most the person's role there allows (GET /connection).",
        "properties": {
          "organization_id": {
            "type": "string",
            "nullable": true,
            "description": "The business; null is the person's own account."
          },
          "name": {
            "type": "string",
            "description": "The business's name, or \"Your own account\"."
          },
          "permission": {
            "type": "string",
            "nullable": true,
            "description": "The person's role in the business (owner, admin, manager, contractor, viewer); null on their own account."
          },
          "access": {
            "type": "string",
            "enum": [
              "none",
              "read",
              "write"
            ],
            "description": "What the connection may do there today."
          },
          "most_allowed": {
            "type": "string",
            "enum": [
              "read",
              "write"
            ],
            "description": "The most the person's role can grant there."
          },
          "reason": {
            "type": "string",
            "nullable": true,
            "description": "Why not more, when `most_allowed` is read; null otherwise."
          }
        }
      },
      "ConnectionMeta": {
        "type": "object",
        "properties": {
          "access": {
            "type": "array",
            "description": "One row per business the person belongs to, then their own account.",
            "items": {
              "$ref": "#/components/schemas/ConnectionAccessRow"
            }
          }
        }
      },
      "ConnectionResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/OauthTokenModel"
          },
          "meta": {
            "$ref": "#/components/schemas/ConnectionMeta"
          }
        }
      },
      "ConnectionResponseAttributes": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/OauthTokenModelAttributes"
          },
          "meta": {
            "$ref": "#/components/schemas/ConnectionMeta"
          }
        }
      },
      "OrganizationJoinLinkModel": {
        "type": "object",
        "description": "A business's join link in JSON:API shape (`application/vnd.api+json`) — one\nshareable link per business that lets anyone who holds it join as a member\nor a viewer. Only an owner of the business can read it: the `code` is a\ncredential.\n",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "example": "organization_join_link"
          },
          "attributes": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "description": "The URL-safe, unguessable code in `/join/<code>`."
              },
              "url": {
                "type": "string",
                "description": "The full URL to share (web app origin + `/join/<code>`)."
              },
              "role": {
                "type": "string",
                "enum": [
                  "member",
                  "viewer"
                ],
                "description": "What people who join with this link become. A member is the `manager` permission the team list calls \"Member\"; the link can never grant owner or admin."
              },
              "expiry": {
                "type": "string",
                "enum": [
                  "24h",
                  "7d",
                  "30d",
                  "never"
                ],
                "description": "The expiry option the owner chose."
              },
              "expires_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "When the link stops working; null when it never expires. Choosing a new expiry restarts the clock from then."
              },
              "enabled": {
                "type": "boolean",
                "description": "False once the owner has turned the link off."
              },
              "status": {
                "type": "string",
                "enum": [
                  "active",
                  "expired",
                  "off"
                ],
                "description": "`active` (usable now), `expired` (switched on but lapsed) or `off` (turned off)."
              },
              "uses_count": {
                "type": "integer",
                "description": "How many people have become members through this code."
              },
              "created_at": {
                "type": "string",
                "format": "date-time"
              },
              "updated_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          }
        }
      },
      "OrganizationJoinLinkShowResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/OrganizationJoinLinkModel"
          }
        }
      },
      "OrganizationJoinLinkUpdateModel": {
        "type": "object",
        "description": "JSON:API request body (`Content-Type: application/vnd.api+json`) for\n`PUT /organizations/{organization_id}/join-link`. All attributes are\noptional. Flat counterpart: `OrganizationJoinLinkUpdateModelAttributes`\n(`application/json`).\n",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "example": "organization_join_link"
              },
              "attributes": {
                "type": "object",
                "properties": {
                  "enabled": {
                    "type": "boolean",
                    "description": "`true` turns the link on (a new code if there was none or it was off); `false` turns it off and keeps the row so the count still reads."
                  },
                  "role": {
                    "type": "string",
                    "enum": [
                      "member",
                      "viewer"
                    ]
                  },
                  "expiry": {
                    "type": "string",
                    "enum": [
                      "24h",
                      "7d",
                      "30d",
                      "never"
                    ]
                  }
                }
              }
            }
          }
        },
        "required": [
          "data"
        ]
      },
      "OrganizationJoinLinkModelAttributes": {
        "type": "object",
        "description": "A business's join link in attributes-adapter shape (`application/json`); fields at the top level.",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "example": "organization_join_link"
          },
          "code": {
            "type": "string"
          },
          "url": {
            "type": "string"
          },
          "role": {
            "type": "string",
            "enum": [
              "member",
              "viewer"
            ]
          },
          "expiry": {
            "type": "string",
            "enum": [
              "24h",
              "7d",
              "30d",
              "never"
            ]
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "enabled": {
            "type": "boolean"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "expired",
              "off"
            ]
          },
          "uses_count": {
            "type": "integer"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "OrganizationJoinLinkShowResponseAttributes": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/OrganizationJoinLinkModelAttributes"
          }
        }
      },
      "OrganizationJoinLinkUpdateModelAttributes": {
        "type": "object",
        "description": "Flat attributes-adapter request body (Content-Type application/json): the same optional fields as `OrganizationJoinLinkUpdateModel`, directly under `data`.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean"
              },
              "role": {
                "type": "string",
                "enum": [
                  "member",
                  "viewer"
                ]
              },
              "expiry": {
                "type": "string",
                "enum": [
                  "24h",
                  "7d",
                  "30d",
                  "never"
                ]
              }
            }
          }
        },
        "required": [
          "data"
        ]
      },
      "PublicPropertyRing": {
        "type": "array",
        "description": "One polygon part as an array of `[lng, lat]` pairs (closed ring, longitude first), the same wire format as every other boundary — never GeoJSON.",
        "items": {
          "type": "array",
          "items": {
            "type": "number"
          }
        }
      },
      "PublicPropertyBoundary": {
        "type": "array",
        "nullable": true,
        "description": "The outline as an array of rings (one per polygon part); null when none is drawn.",
        "items": {
          "$ref": "#/components/schemas/PublicPropertyRing"
        }
      },
      "PublicPropertyWithholding": {
        "type": "object",
        "nullable": true,
        "description": "Present only while a withholding date is still ahead. A mob carries the two drench dates, a paddock the two spray dates.",
        "properties": {
          "task_id": {
            "type": "string"
          },
          "record_id": {
            "type": "string"
          },
          "safe_for_slaughter_on": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "export_eligible_on": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "safe_to_graze_on": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "safe_to_harvest_on": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "products": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "PublicPropertyPaddock": {
        "type": "object",
        "description": "A live paddock that has a boundary. Paddocks with no boundary, and archived ones, are not listed.",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "area_ha": {
            "type": "number",
            "nullable": true
          },
          "land_use": {
            "type": "string",
            "nullable": true
          },
          "current_crop": {
            "type": "string",
            "nullable": true
          },
          "boundary": {
            "$ref": "#/components/schemas/PublicPropertyBoundary"
          },
          "last_record_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "open_task_count": {
            "type": "integer"
          },
          "withholding": {
            "$ref": "#/components/schemas/PublicPropertyWithholding"
          }
        }
      },
      "PublicPropertyMob": {
        "type": "object",
        "description": "A live mob. `paddock` is where it is now (derived from its last completed move) and is null while unplaced or when that paddock is not listed.",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "head_count": {
            "type": "integer",
            "nullable": true
          },
          "paddock_id": {
            "type": "string",
            "nullable": true
          },
          "paddock": {
            "type": "string",
            "nullable": true
          },
          "last_record_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "open_task_count": {
            "type": "integer"
          },
          "withholding": {
            "$ref": "#/components/schemas/PublicPropertyWithholding"
          }
        }
      },
      "PublicPropertyTask": {
        "type": "object",
        "description": "A task on the property's board, open or the plan a record closed. The task text is the description (a task has no title).",
        "properties": {
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "asset_name": {
            "type": "string",
            "nullable": true,
            "description": "The paddock or mob it is about; null for work on the property itself."
          },
          "asset_kind": {
            "type": "string",
            "nullable": true,
            "enum": [
              "paddock",
              "mob"
            ]
          },
          "parent_id": {
            "type": "string",
            "nullable": true
          },
          "sequential": {
            "type": "boolean"
          },
          "completed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "PublicPropertyRecord": {
        "type": "object",
        "description": "A completed row, as a record. `task_id` is the plan it closed, null for a record captured already done. `drench` and `spray` are the typed child, at most one.",
        "properties": {
          "id": {
            "type": "string"
          },
          "task_id": {
            "type": "string",
            "nullable": true
          },
          "asset_name": {
            "type": "string",
            "nullable": true
          },
          "asset_kind": {
            "type": "string",
            "nullable": true,
            "enum": [
              "paddock",
              "mob"
            ]
          },
          "type": {
            "type": "string",
            "nullable": true,
            "description": "The operation type, free text by design (`spray`, `sow`, `drench`, `move_stock`, ...)."
          },
          "date": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "title": {
            "type": "string"
          },
          "by": {
            "type": "string",
            "nullable": true,
            "description": "The operator's name as entered. Never an email or a phone number."
          },
          "source": {
            "type": "string",
            "nullable": true,
            "description": "How it was captured (`voice`, `typed`, `agent`, `import`)."
          },
          "notes": {
            "type": "string",
            "nullable": true
          },
          "photos": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uri"
            }
          },
          "drench": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/DrenchRead"
              }
            ]
          },
          "spray": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/SprayRead"
              }
            ]
          }
        }
      },
      "PublicPropertyResponse": {
        "type": "object",
        "description": "A property as a visitor reads it. The sections named in `include` are present, and only those.\nEverything here follows the shared-link rules: the place is the town and state, never the street\nor postcode; nothing from archived or unmapped paddocks or from archived mobs; nothing about\nsharing with contractors; no emails or phone numbers.\n",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "visibility": {
            "type": "string",
            "enum": [
              "private",
              "link",
              "public"
            ]
          },
          "owner": {
            "type": "object",
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "organization",
                  "user"
                ]
              },
              "slug": {
                "type": "string"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "web_path": {
            "type": "string",
            "description": "The property's own address on the web."
          },
          "location": {
            "type": "string",
            "nullable": true,
            "description": "Town and state, e.g. `Antwerp VIC`. Null when the label is only a street."
          },
          "area_ha": {
            "type": "number",
            "nullable": true
          },
          "centroid_lat": {
            "type": "number",
            "nullable": true
          },
          "centroid_lon": {
            "type": "number",
            "nullable": true
          },
          "boundary": {
            "$ref": "#/components/schemas/PublicPropertyBoundary"
          },
          "boundary_source": {
            "type": "string",
            "nullable": true
          },
          "paddock_count": {
            "type": "integer"
          },
          "mob_count": {
            "type": "integer"
          },
          "paddocks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublicPropertyPaddock"
            }
          },
          "mobs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublicPropertyMob"
            }
          },
          "tasks": {
            "type": "array",
            "description": "The newest 300 at most.",
            "items": {
              "$ref": "#/components/schemas/PublicPropertyTask"
            }
          },
          "records": {
            "type": "array",
            "description": "The newest 300 at most.",
            "items": {
              "$ref": "#/components/schemas/PublicPropertyRecord"
            }
          }
        }
      },
      "ResolveSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "name": {
            "type": "string"
          }
        }
      },
      "ResolvePreview": {
        "type": "object",
        "description": "The facts behind a link preview (an SMS or chat card, a search snippet, the Share\npanel). Built after the access check, so it holds only what the caller may already\nsee; a viewer and a link holder read the same facts. Counts are of live (non-archived)\nassets. Nothing in it identifies a person beyond the owner's display name. A paddock\nor mob block is null when the address names an archived one.\n",
        "required": [
          "kind",
          "owner",
          "property",
          "paddock",
          "mob"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "owner",
              "property",
              "paddock",
              "mob"
            ],
            "description": "What the address names, by its deepest segment."
          },
          "owner": {
            "type": "object",
            "required": [
              "name",
              "kind"
            ],
            "properties": {
              "name": {
                "type": "string",
                "description": "The business name, or the person's name (their handle if they have none; never an email or phone)."
              },
              "kind": {
                "type": "string",
                "enum": [
                  "organization",
                  "user"
                ]
              }
            }
          },
          "property": {
            "type": "object",
            "nullable": true,
            "description": "Present whenever the address names a property (also for its paddocks and mobs).",
            "required": [
              "name",
              "location",
              "area_ha",
              "paddock_count",
              "mob_count",
              "open_task_count"
            ],
            "properties": {
              "name": {
                "type": "string"
              },
              "location": {
                "type": "string",
                "nullable": true,
                "description": "The town and state of the property's place label (\"Currawarna NSW\"), never the street or postcode, or null. A link preview is read by people who were only sent a link, so it never names a street."
              },
              "area_ha": {
                "type": "number",
                "nullable": true
              },
              "paddock_count": {
                "type": "integer",
                "description": "Live paddocks."
              },
              "mob_count": {
                "type": "integer",
                "description": "Live mobs."
              },
              "open_task_count": {
                "type": "integer",
                "description": "Pending tasks on the property."
              }
            }
          },
          "paddock": {
            "type": "object",
            "nullable": true,
            "required": [
              "name",
              "area_ha",
              "land_use",
              "crop",
              "mob_names",
              "open_task_count"
            ],
            "properties": {
              "name": {
                "type": "string"
              },
              "area_ha": {
                "type": "number",
                "nullable": true
              },
              "land_use": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "cropping",
                  "grazing",
                  "mixed",
                  "fallow"
                ]
              },
              "crop": {
                "type": "string",
                "nullable": true
              },
              "mob_names": {
                "type": "array",
                "maxItems": 5,
                "description": "Live mobs currently in the paddock, by name.",
                "items": {
                  "type": "string"
                }
              },
              "open_task_count": {
                "type": "integer",
                "description": "Pending tasks on the paddock."
              }
            }
          },
          "mob": {
            "type": "object",
            "nullable": true,
            "required": [
              "name",
              "head_count",
              "paddock_name",
              "open_task_count",
              "withholding"
            ],
            "properties": {
              "name": {
                "type": "string"
              },
              "head_count": {
                "type": "integer",
                "nullable": true
              },
              "paddock_name": {
                "type": "string",
                "nullable": true,
                "description": "The paddock the mob is in now; null while it is unplaced."
              },
              "open_task_count": {
                "type": "integer",
                "description": "Pending tasks on the mob."
              },
              "withholding": {
                "type": "object",
                "nullable": true,
                "description": "Present only while a withholding or export date is still ahead.",
                "properties": {
                  "safe_for_slaughter_on": {
                    "type": "string",
                    "format": "date",
                    "nullable": true
                  },
                  "export_eligible_on": {
                    "type": "string",
                    "format": "date",
                    "nullable": true
                  }
                }
              }
            }
          }
        }
      },
      "ResolveResponse": {
        "type": "object",
        "properties": {
          "resolved": {
            "type": "object",
            "properties": {
              "owner": {
                "type": "object",
                "properties": {
                  "kind": {
                    "type": "string",
                    "enum": [
                      "organization",
                      "user"
                    ]
                  },
                  "id": {
                    "type": "string"
                  },
                  "slug": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  },
                  "contact_name": {
                    "type": "string",
                    "description": "Who to ask for a change of role or an invite (a name only)."
                  },
                  "path": {
                    "type": "string"
                  }
                }
              },
              "property": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/ResolveSummary"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "visibility": {
                        "type": "string",
                        "enum": [
                          "private",
                          "link",
                          "public"
                        ]
                      }
                    }
                  }
                ],
                "nullable": true
              },
              "paddock": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/ResolveSummary"
                  }
                ],
                "nullable": true
              },
              "mob": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/ResolveSummary"
                  }
                ],
                "nullable": true
              },
              "canonical_path": {
                "type": "string",
                "description": "The address with every slug current."
              },
              "redirect": {
                "type": "boolean",
                "description": "True when a slug in the request was an old one; the page should go to `canonical_path`."
              },
              "access": {
                "type": "object",
                "properties": {
                  "signed_in": {
                    "type": "boolean"
                  },
                  "member": {
                    "type": "boolean",
                    "description": "Whether the caller is a member of the owner (or the person)."
                  },
                  "can_edit": {
                    "type": "boolean"
                  },
                  "role": {
                    "type": "string",
                    "nullable": true,
                    "description": "The caller's role in the business, when they have one."
                  },
                  "via": {
                    "type": "string",
                    "enum": [
                      "member",
                      "public",
                      "link"
                    ],
                    "description": "Why they can see it."
                  }
                }
              },
              "preview": {
                "$ref": "#/components/schemas/ResolvePreview"
              }
            }
          }
        }
      },
      "SetupDraftRing": {
        "type": "array",
        "nullable": true,
        "maxItems": 2000,
        "description": "The corners placed so far: an OPEN array of `[longitude, latitude]` pairs\nin RFC 7946 order — **longitude first** — 0 to 2000 of them. A draft may\nhold one or two corners, so this is neither a polygon nor GeoJSON; it\nbecomes one only when the boundary is saved (`updateFarmBoundary`,\n`updatePaddockBoundary`). Longitude within ±180, latitude within ±90.\n",
        "items": {
          "type": "array",
          "description": "[longitude, latitude]",
          "minItems": 2,
          "maxItems": 2,
          "items": {
            "type": "number"
          }
        }
      },
      "SetupDraftLocation": {
        "type": "object",
        "nullable": true,
        "description": "The place the editor found for the property. `label` is required; `lng` and `lat` are both present or both absent. No other keys.",
        "additionalProperties": false,
        "properties": {
          "label": {
            "type": "string",
            "maxLength": 500,
            "example": "Currawarna NSW 2650"
          },
          "lng": {
            "type": "number",
            "minimum": -180,
            "maximum": 180
          },
          "lat": {
            "type": "number",
            "minimum": -90,
            "maximum": 90
          }
        },
        "required": [
          "label"
        ]
      },
      "PropertySetupDraftAttributes": {
        "type": "object",
        "properties": {
          "owner_type": {
            "type": "string",
            "enum": [
              "User",
              "Organization"
            ]
          },
          "owner_id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "location": {
            "$ref": "#/components/schemas/SetupDraftLocation"
          },
          "ring": {
            "$ref": "#/components/schemas/SetupDraftRing"
          },
          "step": {
            "type": "string",
            "nullable": true,
            "enum": [
              "owner",
              "name",
              "location",
              "boundary"
            ],
            "description": "The checklist row the person was on."
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the draft was last written; the editor's \"saved 25 min ago\"."
          }
        }
      },
      "PropertySetupDraftModel": {
        "type": "object",
        "nullable": true,
        "description": "The caller's unsaved property for one owner (JSON:API resource object), or null when there is none.",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "property_setup_drafts"
            ]
          },
          "attributes": {
            "$ref": "#/components/schemas/PropertySetupDraftAttributes"
          }
        }
      },
      "PropertySetupDraftResponse": {
        "type": "object",
        "description": "JSON:API response (Accept application/vnd.api+json). `data` is null when the caller has no draft for the owner.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/PropertySetupDraftModel"
          }
        }
      },
      "PropertySetupDraftModelAttributes": {
        "type": "object",
        "nullable": true,
        "description": "The caller's unsaved property for one owner in attributes-adapter shape (Accept application/json), or null when there is none.",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "property_setup_drafts"
            ]
          },
          "owner_type": {
            "type": "string",
            "enum": [
              "User",
              "Organization"
            ]
          },
          "owner_id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "location": {
            "$ref": "#/components/schemas/SetupDraftLocation"
          },
          "ring": {
            "$ref": "#/components/schemas/SetupDraftRing"
          },
          "step": {
            "type": "string",
            "nullable": true,
            "enum": [
              "owner",
              "name",
              "location",
              "boundary"
            ]
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PropertySetupDraftResponseAttributes": {
        "type": "object",
        "description": "Attributes-adapter response (Accept application/json). `data` is null when the caller has no draft for the owner.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/PropertySetupDraftModelAttributes"
          }
        }
      },
      "PropertySetupDraftUpdateModel": {
        "type": "object",
        "description": "Nested JSON:API request body (Content-Type application/vnd.api+json) for the caller's property setup draft. `owner_type` and `owner_id` name whose property it will be; of the rest, only the keys sent change, and `null` clears one. Equivalent flat shape: `PropertySetupDraftUpdateModelAttributes` sent as application/json.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "property_setup_drafts"
                ]
              },
              "attributes": {
                "type": "object",
                "properties": {
                  "owner_type": {
                    "type": "string",
                    "enum": [
                      "User",
                      "Organization"
                    ]
                  },
                  "owner_id": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "name": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 255
                  },
                  "location": {
                    "$ref": "#/components/schemas/SetupDraftLocation"
                  },
                  "ring": {
                    "$ref": "#/components/schemas/SetupDraftRing"
                  },
                  "step": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "owner",
                      "name",
                      "location",
                      "boundary"
                    ]
                  }
                },
                "required": [
                  "owner_type",
                  "owner_id"
                ]
              }
            },
            "required": [
              "type",
              "attributes"
            ]
          }
        },
        "required": [
          "data"
        ]
      },
      "PropertySetupDraftUpdateModelAttributes": {
        "type": "object",
        "description": "Flat attributes-adapter request body (Content-Type application/json) for the caller's property setup draft: the same fields directly under `data`. Equivalent JSON:API (nested) shape: `PropertySetupDraftUpdateModel` sent as application/vnd.api+json.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "owner_type": {
                "type": "string",
                "enum": [
                  "User",
                  "Organization"
                ]
              },
              "owner_id": {
                "type": "string",
                "format": "uuid"
              },
              "name": {
                "type": "string",
                "nullable": true,
                "maxLength": 255
              },
              "location": {
                "$ref": "#/components/schemas/SetupDraftLocation"
              },
              "ring": {
                "$ref": "#/components/schemas/SetupDraftRing"
              },
              "step": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "owner",
                  "name",
                  "location",
                  "boundary"
                ]
              }
            },
            "required": [
              "owner_type",
              "owner_id"
            ]
          }
        },
        "required": [
          "data"
        ]
      },
      "BoundaryDraftAttributes": {
        "type": "object",
        "properties": {
          "target_type": {
            "type": "string",
            "enum": [
              "Farm",
              "Paddock"
            ]
          },
          "target_id": {
            "type": "string",
            "format": "uuid"
          },
          "ring": {
            "$ref": "#/components/schemas/SetupDraftRing"
          },
          "location": {
            "$ref": "#/components/schemas/SetupDraftLocation"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the draft was last written."
          }
        }
      },
      "BoundaryDraftModel": {
        "type": "object",
        "nullable": true,
        "description": "The caller's unsaved boundary on one property or paddock (JSON:API resource object), or null when there is none.",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "boundary_drafts"
            ]
          },
          "attributes": {
            "$ref": "#/components/schemas/BoundaryDraftAttributes"
          }
        }
      },
      "BoundaryDraftResponse": {
        "type": "object",
        "description": "JSON:API response (Accept application/vnd.api+json). `data` is null when the caller has no draft for the target.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/BoundaryDraftModel"
          }
        }
      },
      "BoundaryDraftModelAttributes": {
        "type": "object",
        "nullable": true,
        "description": "The caller's unsaved boundary in attributes-adapter shape (Accept application/json), or null when there is none.",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "boundary_drafts"
            ]
          },
          "target_type": {
            "type": "string",
            "enum": [
              "Farm",
              "Paddock"
            ]
          },
          "target_id": {
            "type": "string",
            "format": "uuid"
          },
          "ring": {
            "$ref": "#/components/schemas/SetupDraftRing"
          },
          "location": {
            "$ref": "#/components/schemas/SetupDraftLocation"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "BoundaryDraftResponseAttributes": {
        "type": "object",
        "description": "Attributes-adapter response (Accept application/json). `data` is null when the caller has no draft for the target.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/BoundaryDraftModelAttributes"
          }
        }
      },
      "BoundaryDraftUpdateModel": {
        "type": "object",
        "description": "Nested JSON:API request body (Content-Type application/vnd.api+json) for the caller's boundary draft. Only the keys sent change; `null` clears one. Equivalent flat shape: `BoundaryDraftUpdateModelAttributes` sent as application/json.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "boundary_drafts"
                ]
              },
              "attributes": {
                "type": "object",
                "properties": {
                  "ring": {
                    "$ref": "#/components/schemas/SetupDraftRing"
                  },
                  "location": {
                    "$ref": "#/components/schemas/SetupDraftLocation"
                  }
                }
              }
            },
            "required": [
              "type",
              "attributes"
            ]
          }
        },
        "required": [
          "data"
        ]
      },
      "BoundaryDraftUpdateModelAttributes": {
        "type": "object",
        "description": "Flat attributes-adapter request body (Content-Type application/json) for the caller's boundary draft: the same fields directly under `data`. Equivalent JSON:API (nested) shape: `BoundaryDraftUpdateModel` sent as application/vnd.api+json.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "ring": {
                "$ref": "#/components/schemas/SetupDraftRing"
              },
              "location": {
                "$ref": "#/components/schemas/SetupDraftLocation"
              }
            }
          }
        },
        "required": [
          "data"
        ]
      },
      "RegistrationPropertyDraft": {
        "type": "object",
        "nullable": true,
        "description": "The property a visitor mapped on the landing page before registering\n(design handoff 29). Created in the same transaction as the account —\nthe business first when `owner_kind` is `business` (the new person its\nowner), then the property owned by the person or that business, named\n`name`, with `location.label` as its map label and `location.lng/lat` as\nits map point, then the ring saved exactly as `updateFarmBoundary` saves\none (area measured server-side). An invalid draft fails the whole request\nwith 422 and nothing is created; its errors point at\n`/data/attributes/property_draft.<field>`.\n",
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 255,
            "description": "Required."
          },
          "owner_kind": {
            "type": "string",
            "enum": [
              "personal",
              "business"
            ],
            "description": "Defaults to personal."
          },
          "business_name": {
            "type": "string",
            "nullable": true,
            "maxLength": 120,
            "description": "Required when `owner_kind` is business."
          },
          "location": {
            "$ref": "#/components/schemas/SetupDraftLocation"
          },
          "ring": {
            "type": "array",
            "nullable": true,
            "maxItems": 2001,
            "description": "The corners, `[longitude, latitude]`, open or closed (first repeated\nlast). Fewer than three distinct corners is not an error: the property\nis made without a boundary. Three or more must make a valid polygon\n(the boundary save's checks) enclosing less than 100,000 ha.\n",
            "items": {
              "type": "array",
              "minItems": 2,
              "maxItems": 2,
              "items": {
                "type": "number"
              }
            }
          },
          "area_ha": {
            "type": "number",
            "nullable": true,
            "description": "The client's own figure. Ignored; the area is measured from the ring."
          }
        },
        "required": [
          "name"
        ]
      },
      "SitemapServiceItem": {
        "type": "object",
        "required": [
          "id",
          "slug",
          "lastmod",
          "indexable"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "slug": {
            "type": "string"
          },
          "lastmod": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "indexable": {
            "type": "boolean"
          }
        }
      },
      "SitemapSlugItem": {
        "type": "object",
        "required": [
          "slug",
          "lastmod",
          "indexable"
        ],
        "properties": {
          "slug": {
            "type": "string"
          },
          "lastmod": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "indexable": {
            "type": "boolean"
          }
        }
      },
      "SitemapIdItem": {
        "type": "object",
        "required": [
          "id",
          "lastmod",
          "indexable"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "lastmod": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "indexable": {
            "type": "boolean"
          }
        }
      },
      "SitemapServicesResponse": {
        "type": "object",
        "required": [
          "items",
          "total"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SitemapServiceItem"
            }
          },
          "next_cursor": {
            "type": "string",
            "nullable": true,
            "description": "Opaque base64-encoded cursor for the next page; absent or null on last page."
          },
          "total": {
            "type": "integer",
            "description": "Total count of indexable records (for shard planning)."
          }
        }
      },
      "SitemapUsersResponse": {
        "type": "object",
        "required": [
          "items",
          "total"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SitemapSlugItem"
            }
          },
          "next_cursor": {
            "type": "string",
            "nullable": true
          },
          "total": {
            "type": "integer"
          }
        }
      },
      "SitemapOrganizationsResponse": {
        "type": "object",
        "required": [
          "items",
          "total"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SitemapSlugItem"
            }
          },
          "next_cursor": {
            "type": "string",
            "nullable": true
          },
          "total": {
            "type": "integer"
          }
        }
      },
      "SitemapTasksResponse": {
        "type": "object",
        "required": [
          "items",
          "total"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SitemapIdItem"
            }
          },
          "next_cursor": {
            "type": "string",
            "nullable": true
          },
          "total": {
            "type": "integer"
          }
        }
      },
      "SitemapPathItem": {
        "type": "object",
        "required": [
          "path",
          "lastmod",
          "indexable"
        ],
        "properties": {
          "path": {
            "type": "string",
            "description": "The address under the web app, e.g. `/organizations/hillcrest/properties/yarrabee`."
          },
          "lastmod": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "indexable": {
            "type": "boolean"
          }
        }
      },
      "SitemapPropertiesResponse": {
        "type": "object",
        "required": [
          "items",
          "next_cursor",
          "total"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SitemapPathItem"
            }
          },
          "next_cursor": {
            "type": "string",
            "nullable": true
          },
          "total": {
            "type": "integer",
            "description": "The number of public properties (the cursor pages over properties; each page also carries their paddocks and mobs)."
          }
        }
      },
      "SowingProductRead": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "role": {
            "type": "string",
            "nullable": true,
            "enum": [
              "seed",
              "fertiliser",
              "seed_treatment",
              "inoculant"
            ],
            "description": "What this line is."
          },
          "name": {
            "type": "string",
            "nullable": true,
            "description": "Product or seed, e.g. `Spartacus CL barley`. Blank is valid."
          },
          "variety": {
            "type": "string",
            "nullable": true,
            "description": "Seed lines only. Completing the sowing sets the paddock's current crop to this."
          },
          "seed_source": {
            "type": "string",
            "nullable": true,
            "enum": [
              "retained",
              "purchased",
              "shared"
            ],
            "description": "Seed lines only."
          },
          "seed_lot": {
            "type": "string",
            "nullable": true,
            "description": "Seed lot or certificate number."
          },
          "germination_pct": {
            "type": "number",
            "minimum": 0,
            "nullable": true,
            "description": "Germination, 0 to 100."
          },
          "tgw_g": {
            "type": "number",
            "minimum": 0,
            "nullable": true,
            "description": "Thousand-grain weight, grams."
          },
          "rate": {
            "type": "string",
            "nullable": true,
            "description": "The rate as entered, e.g. `80 kg/ha`. Kept verbatim."
          },
          "rate_value": {
            "type": "number",
            "minimum": 0,
            "nullable": true,
            "description": "The number in the rate. Parsed from `rate` when omitted."
          },
          "rate_unit": {
            "type": "string",
            "nullable": true,
            "enum": [
              "g",
              "kg",
              "t",
              "mL",
              "L",
              "seeds",
              "%"
            ],
            "description": "Unit of `rate_value`. Required when `rate_value` is set."
          },
          "rate_basis": {
            "type": "string",
            "nullable": true,
            "enum": [
              "ha",
              "head",
              "100_kg_seed",
              "100_l",
              "total"
            ],
            "description": "What the rate is per."
          },
          "placement": {
            "type": "string",
            "nullable": true,
            "enum": [
              "with_seed",
              "banded"
            ],
            "description": "Fertiliser lines only."
          },
          "active": {
            "type": "string",
            "nullable": true,
            "description": "Seed treatments: the active constituent."
          },
          "analysis": {
            "type": "object",
            "additionalProperties": {
              "type": "number"
            },
            "description": "Fertiliser lines: nutrient percentages by weight, lower-case keys, e.g. `{ \"n\": 10, \"p\": 22 }`, copied from the catalogue."
          },
          "batch": {
            "type": "string",
            "nullable": true,
            "description": "Batch number. Never invent one."
          },
          "expiry": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "description": "Expiry date, `YYYY-MM-DD`."
          },
          "grazing_whp_days": {
            "type": "integer",
            "minimum": 0,
            "nullable": true,
            "description": "Seed treatments only: the grazing withholding period from the label, in whole days. Never invent it."
          },
          "esi_days": {
            "type": "integer",
            "minimum": 0,
            "nullable": true,
            "description": "Seed treatments only: the export slaughter interval, in whole days."
          },
          "inoculant_group": {
            "type": "string",
            "nullable": true,
            "description": "Inoculants only: the group, e.g. `E` for lentils."
          },
          "product_id": {
            "type": "string",
            "nullable": true,
            "description": "Optional link to the catalogue."
          },
          "position": {
            "type": "integer",
            "description": "Order the products were entered in."
          }
        }
      },
      "SowingRead": {
        "type": "object",
        "description": "The sowing of a task or record. Null on a task that has none.",
        "properties": {
          "id": {
            "type": "string",
            "description": "The SowingRecord id."
          },
          "crop_sown": {
            "type": "string",
            "nullable": true,
            "description": "What the paddock carries once this is completed: the seed line's variety, else `crop`."
          },
          "safe_to_graze_on": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "description": "DERIVED: the completion date plus the longest grazing WHP among named seed-treatment lines. Null while the task is open or until one carries it."
          },
          "area_ha": {
            "type": "number",
            "minimum": 0,
            "nullable": true,
            "description": "Hectares sown; separators are stripped. Defaults to the paddock's area."
          },
          "crop": {
            "type": "string",
            "nullable": true,
            "description": "e.g. `Barley`. The variety goes on the seed line."
          },
          "row_spacing_mm": {
            "type": "integer",
            "minimum": 0,
            "description": "Row spacing, millimetres.",
            "nullable": true
          },
          "depth_mm": {
            "type": "integer",
            "minimum": 0,
            "description": "Sowing depth, millimetres.",
            "nullable": true
          },
          "seeder": {
            "type": "string",
            "nullable": true,
            "description": "The seeder used."
          },
          "opener": {
            "type": "string",
            "nullable": true,
            "description": "Opener type."
          },
          "speed_kmh": {
            "type": "number",
            "minimum": 0,
            "nullable": true,
            "description": "Sowing speed, km/h."
          },
          "stubble": {
            "type": "string",
            "nullable": true,
            "description": "Stubble at sowing."
          },
          "soil_moisture": {
            "type": "string",
            "nullable": true,
            "description": "Soil moisture at sowing."
          },
          "target_plants_m2": {
            "type": "number",
            "minimum": 0,
            "nullable": true,
            "description": "Target plant density, plants per square metre."
          },
          "products": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SowingProductRead"
            }
          }
        }
      },
      "SowingProductWrite": {
        "type": "object",
        "description": "One sowing product line. Never invent a batch, expiry, rate or withholding period.",
        "properties": {
          "role": {
            "type": "string",
            "nullable": true,
            "enum": [
              "seed",
              "fertiliser",
              "seed_treatment",
              "inoculant"
            ],
            "description": "What this line is."
          },
          "name": {
            "type": "string",
            "nullable": true,
            "description": "Product or seed, e.g. `Spartacus CL barley`. Blank is valid."
          },
          "variety": {
            "type": "string",
            "nullable": true,
            "description": "Seed lines only. Completing the sowing sets the paddock's current crop to this."
          },
          "seed_source": {
            "type": "string",
            "nullable": true,
            "enum": [
              "retained",
              "purchased",
              "shared"
            ],
            "description": "Seed lines only."
          },
          "seed_lot": {
            "type": "string",
            "nullable": true,
            "description": "Seed lot or certificate number."
          },
          "germination_pct": {
            "description": "Germination, 0 to 100.",
            "oneOf": [
              {
                "type": "number",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "tgw_g": {
            "description": "Thousand-grain weight, grams.",
            "oneOf": [
              {
                "type": "number",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "rate": {
            "type": "string",
            "nullable": true,
            "description": "The rate as entered, e.g. `80 kg/ha`. Kept verbatim."
          },
          "rate_value": {
            "description": "The number in the rate. Parsed from `rate` when omitted.",
            "oneOf": [
              {
                "type": "number",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "rate_unit": {
            "type": "string",
            "nullable": true,
            "enum": [
              "g",
              "kg",
              "t",
              "mL",
              "L",
              "seeds",
              "%"
            ],
            "description": "Unit of `rate_value`. Required when `rate_value` is set."
          },
          "rate_basis": {
            "type": "string",
            "nullable": true,
            "enum": [
              "ha",
              "head",
              "100_kg_seed",
              "100_l",
              "total"
            ],
            "description": "What the rate is per."
          },
          "placement": {
            "type": "string",
            "nullable": true,
            "enum": [
              "with_seed",
              "banded"
            ],
            "description": "Fertiliser lines only."
          },
          "active": {
            "type": "string",
            "nullable": true,
            "description": "Seed treatments: the active constituent."
          },
          "analysis": {
            "type": "object",
            "additionalProperties": {
              "type": "number"
            },
            "description": "Fertiliser lines: nutrient percentages by weight, lower-case keys, e.g. `{ \"n\": 10, \"p\": 22 }`, copied from the catalogue."
          },
          "batch": {
            "type": "string",
            "nullable": true,
            "description": "Batch number. Never invent one."
          },
          "expiry": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "description": "Expiry date, `YYYY-MM-DD`."
          },
          "grazing_whp_days": {
            "description": "Seed treatments only: the grazing withholding period from the label, in whole days. Never invent it.",
            "oneOf": [
              {
                "type": "integer",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "esi_days": {
            "description": "Seed treatments only: the export slaughter interval, in whole days.",
            "oneOf": [
              {
                "type": "integer",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "inoculant_group": {
            "type": "string",
            "nullable": true,
            "description": "Inoculants only: the group, e.g. `E` for lentils."
          },
          "product_id": {
            "type": "string",
            "nullable": true,
            "description": "Optional link to the catalogue."
          }
        }
      },
      "SowingWrite": {
        "type": "object",
        "description": "Writes the sowing child of a task. `products` REPLACES the set (at most 10).",
        "properties": {
          "area_ha": {
            "description": "Hectares sown; separators are stripped. Defaults to the paddock's area.",
            "oneOf": [
              {
                "type": "number",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "crop": {
            "type": "string",
            "nullable": true,
            "description": "e.g. `Barley`. The variety goes on the seed line."
          },
          "row_spacing_mm": {
            "type": "integer",
            "minimum": 0,
            "description": "Row spacing, millimetres.",
            "nullable": true
          },
          "depth_mm": {
            "type": "integer",
            "minimum": 0,
            "description": "Sowing depth, millimetres.",
            "nullable": true
          },
          "seeder": {
            "type": "string",
            "nullable": true,
            "description": "The seeder used."
          },
          "opener": {
            "type": "string",
            "nullable": true,
            "description": "Opener type."
          },
          "speed_kmh": {
            "description": "Sowing speed, km/h.",
            "oneOf": [
              {
                "type": "number",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "stubble": {
            "type": "string",
            "nullable": true,
            "description": "Stubble at sowing."
          },
          "soil_moisture": {
            "type": "string",
            "nullable": true,
            "description": "Soil moisture at sowing."
          },
          "target_plants_m2": {
            "description": "Target plant density, plants per square metre.",
            "oneOf": [
              {
                "type": "number",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "products": {
            "type": "array",
            "maxItems": 10,
            "description": "One entry per product line. On update this REPLACES the set (position is the array index).",
            "items": {
              "$ref": "#/components/schemas/SowingProductWrite"
            }
          }
        }
      },
      "SowingRecordFields": {
        "type": "object",
        "properties": {
          "image_upload_ids": {
            "$ref": "#/components/schemas/ImageUploadIdList"
          },
          "image_urls": {
            "$ref": "#/components/schemas/ImageUrlList"
          },
          "image_base64s": {
            "$ref": "#/components/schemas/ImageBase64List"
          },
          "image_mode": {
            "$ref": "#/components/schemas/ImageMode"
          },
          "remove_image_ids": {
            "$ref": "#/components/schemas/ImageRemoveIdList"
          },
          "date": {
            "type": "string",
            "format": "date",
            "description": "The day it was done, `YYYY-MM-DD` (create only; default today). The server stores midday in the application time zone so derived dates do not slip a day. A future date records a plan: it creates the open task instead of a done record.",
            "example": "2026-09-28"
          },
          "description": {
            "type": "string",
            "description": "Create only: the record's title."
          },
          "area_ha": {
            "description": "Hectares sown; separators are stripped. Defaults to the paddock's area.",
            "oneOf": [
              {
                "type": "number",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "crop": {
            "type": "string",
            "nullable": true,
            "description": "e.g. `Barley`. The variety goes on the seed line."
          },
          "row_spacing_mm": {
            "type": "integer",
            "minimum": 0,
            "description": "Row spacing, millimetres.",
            "nullable": true
          },
          "depth_mm": {
            "type": "integer",
            "minimum": 0,
            "description": "Sowing depth, millimetres.",
            "nullable": true
          },
          "seeder": {
            "type": "string",
            "nullable": true,
            "description": "The seeder used."
          },
          "opener": {
            "type": "string",
            "nullable": true,
            "description": "Opener type."
          },
          "speed_kmh": {
            "description": "Sowing speed, km/h.",
            "oneOf": [
              {
                "type": "number",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "stubble": {
            "type": "string",
            "nullable": true,
            "description": "Stubble at sowing."
          },
          "soil_moisture": {
            "type": "string",
            "nullable": true,
            "description": "Soil moisture at sowing."
          },
          "target_plants_m2": {
            "description": "Target plant density, plants per square metre.",
            "oneOf": [
              {
                "type": "number",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "products": {
            "type": "array",
            "maxItems": 10,
            "description": "One entry per product line. On update this REPLACES the set (position is the array index).",
            "items": {
              "$ref": "#/components/schemas/SowingProductWrite"
            }
          }
        }
      },
      "SowingRecordCreateModel": {
        "type": "object",
        "description": "Nested JSON:API request body (application/vnd.api+json) for POST /paddocks/{paddock_id}/sowing-records: `data.attributes`. Equivalent flat shape: `SowingRecordCreateModelAttributes` as application/json.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "sowing-record"
                ]
              },
              "attributes": {
                "$ref": "#/components/schemas/SowingRecordFields"
              }
            }
          }
        }
      },
      "SowingRecordCreateModelAttributes": {
        "type": "object",
        "description": "Flat request body (application/json) for POST /paddocks/{paddock_id}/sowing-records: the fields directly under `data`. Equivalent nested shape: `SowingRecordCreateModel`.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/SowingRecordFields"
          }
        }
      },
      "SowingRecordUpdateModel": {
        "type": "object",
        "description": "Nested JSON:API request body (application/vnd.api+json) for PUT /sowing-records/{id}: `data.attributes` with any of the fields (products REPLACES the set). Equivalent flat shape: `SowingRecordUpdateModelAttributes`.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "type": {
                "type": "string",
                "enum": [
                  "sowing-record"
                ]
              },
              "attributes": {
                "$ref": "#/components/schemas/SowingRecordFields"
              }
            }
          }
        }
      },
      "SowingRecordUpdateModelAttributes": {
        "type": "object",
        "description": "Flat request body (application/json) for PUT /sowing-records/{id}. Equivalent nested shape: `SowingRecordUpdateModel`.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/SowingRecordFields"
          }
        }
      },
      "TreatmentProductRead": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "nullable": true,
            "description": "Product trade name. Blank is valid, but a blank name contributes nothing to the derived withholding dates."
          },
          "active": {
            "type": "string",
            "nullable": true,
            "description": "Label active constituent, e.g. `Moxidectin 1 g/L`."
          },
          "batch": {
            "type": "string",
            "nullable": true,
            "description": "Batch number from the pack. Never invent one; leave it unset until the farmer confirms it."
          },
          "expiry": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "description": "Expiry date on the pack, `YYYY-MM-DD`. Never invent one; unset prints `not recorded`."
          },
          "dose": {
            "type": "string",
            "nullable": true,
            "description": "The dose as entered, e.g. `5 mL / 20 kg`. Kept verbatim."
          },
          "dose_value": {
            "type": "number",
            "minimum": 0,
            "nullable": true,
            "description": "The number in the dose, e.g. 5. Only what the caller sends; it is never parsed from `dose`."
          },
          "dose_unit": {
            "type": "string",
            "nullable": true,
            "enum": [
              "mL",
              "L",
              "g",
              "kg"
            ],
            "description": "Unit of `dose_value`."
          },
          "dose_basis": {
            "type": "string",
            "nullable": true,
            "enum": [
              "per_head",
              "per_kg",
              "per_mob"
            ],
            "description": "What `dose_value` is per."
          },
          "drench_group": {
            "type": "string",
            "nullable": true,
            "description": "Chemical group, e.g. `BZ`, `LV`, `ML`, `AD`, `SI`, `OP`, `TZ`, `SA`, `IQ` or a combination, for resistance tracking."
          },
          "whp_days": {
            "type": "integer",
            "minimum": 0,
            "nullable": true,
            "description": "Meat withholding period from the label, in whole days. \"14 d\" is read as 14. Never invent it."
          },
          "esi_days": {
            "type": "integer",
            "minimum": 0,
            "nullable": true,
            "description": "Export slaughter interval from the label, in whole days. If the label gives none, send the WHP. Never invent it."
          },
          "wool_interval_days": {
            "type": "integer",
            "minimum": 0,
            "nullable": true,
            "description": "Wool harvest interval, days. 0 is none stated."
          },
          "milk_whp_days": {
            "type": "integer",
            "minimum": 0,
            "nullable": true,
            "description": "Milk withholding period, days. 0 is none stated."
          },
          "product_id": {
            "type": "string",
            "nullable": true,
            "description": "Optional link to the catalogue (`listProducts`). The values here are copied at save, so a later catalogue edit never changes a past record."
          },
          "position": {
            "type": "integer",
            "description": "Order the products were entered in."
          }
        }
      },
      "TreatmentRead": {
        "type": "object",
        "description": "The treatment of a task or record. Null on a task that has none.",
        "properties": {
          "id": {
            "type": "string",
            "description": "The TreatmentRecord id: the same before and after the task is completed."
          },
          "safe_for_slaughter_on": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "description": "DERIVED, never stored: the completion date plus the longest meat WHP among named products. Null while the task is open or until a named product carries one."
          },
          "export_eligible_on": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "description": "DERIVED: the completion date plus the longest ESI among named products."
          },
          "category": {
            "type": "string",
            "nullable": true,
            "enum": [
              "drench",
              "fluke",
              "lice",
              "flystrike",
              "other_external_parasite",
              "vaccine",
              "vitamin_mineral",
              "antibiotic",
              "pain_relief",
              "feed_water_additive",
              "foot_treatment",
              "reproductive",
              "hgp",
              "other"
            ],
            "description": "What kind of treatment. Required on create. A `drench` is the everyday word for category `drench`."
          },
          "route": {
            "type": "string",
            "nullable": true,
            "enum": [
              "oral",
              "injection",
              "backline",
              "jetting",
              "dip",
              "capsule",
              "topical",
              "in_feed_water"
            ],
            "description": "How it was given. Leave it unset if the farmer did not say; it prints `not recorded`."
          },
          "head": {
            "type": "integer",
            "minimum": 0,
            "nullable": true,
            "description": "Head treated; separators are stripped (\"1,240\" is 1240). Defaults to the mob's head count."
          },
          "equipment_cleaned": {
            "type": "boolean",
            "nullable": true,
            "description": "ISC checklist item; unset is not recorded."
          },
          "adverse_reaction": {
            "type": "string",
            "nullable": true,
            "description": "Any adverse reaction or broken needle. LPA asks for both to be recorded and passed on."
          },
          "hgp_earmarked": {
            "type": "boolean",
            "nullable": true,
            "description": "HGP implant: triangle ear mark applied (cattle)."
          },
          "vet_authorisation": {
            "type": "string",
            "nullable": true,
            "description": "Off-label use: who authorised it."
          },
          "products": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TreatmentProductRead"
            }
          }
        }
      },
      "TreatmentProductWrite": {
        "type": "object",
        "description": "One treatment product line. Never invent a batch, expiry, rate or withholding period.",
        "properties": {
          "name": {
            "type": "string",
            "nullable": true,
            "description": "Product trade name. Blank is valid, but a blank name contributes nothing to the derived withholding dates."
          },
          "active": {
            "type": "string",
            "nullable": true,
            "description": "Label active constituent, e.g. `Moxidectin 1 g/L`."
          },
          "batch": {
            "type": "string",
            "nullable": true,
            "description": "Batch number from the pack. Never invent one; leave it unset until the farmer confirms it."
          },
          "expiry": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "description": "Expiry date on the pack, `YYYY-MM-DD`. Never invent one; unset prints `not recorded`."
          },
          "dose": {
            "type": "string",
            "nullable": true,
            "description": "The dose as entered, e.g. `5 mL / 20 kg`. Kept verbatim."
          },
          "dose_value": {
            "description": "The number in the dose, e.g. 5. Only what the caller sends; it is never parsed from `dose`.",
            "oneOf": [
              {
                "type": "number",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "dose_unit": {
            "type": "string",
            "nullable": true,
            "enum": [
              "mL",
              "L",
              "g",
              "kg"
            ],
            "description": "Unit of `dose_value`."
          },
          "dose_basis": {
            "type": "string",
            "nullable": true,
            "enum": [
              "per_head",
              "per_kg",
              "per_mob"
            ],
            "description": "What `dose_value` is per."
          },
          "drench_group": {
            "type": "string",
            "nullable": true,
            "description": "Chemical group, e.g. `BZ`, `LV`, `ML`, `AD`, `SI`, `OP`, `TZ`, `SA`, `IQ` or a combination, for resistance tracking."
          },
          "whp_days": {
            "description": "Meat withholding period from the label, in whole days. \"14 d\" is read as 14. Never invent it.",
            "oneOf": [
              {
                "type": "integer",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "esi_days": {
            "description": "Export slaughter interval from the label, in whole days. If the label gives none, send the WHP. Never invent it.",
            "oneOf": [
              {
                "type": "integer",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "wool_interval_days": {
            "description": "Wool harvest interval, days. 0 is none stated.",
            "oneOf": [
              {
                "type": "integer",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "milk_whp_days": {
            "description": "Milk withholding period, days. 0 is none stated.",
            "oneOf": [
              {
                "type": "integer",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "product_id": {
            "type": "string",
            "nullable": true,
            "description": "Optional link to the catalogue (`listProducts`). The values here are copied at save, so a later catalogue edit never changes a past record."
          }
        }
      },
      "TreatmentWrite": {
        "type": "object",
        "description": "Writes the treatment child of a task. `products` REPLACES the set (at most 10).",
        "properties": {
          "category": {
            "type": "string",
            "nullable": true,
            "enum": [
              "drench",
              "fluke",
              "lice",
              "flystrike",
              "other_external_parasite",
              "vaccine",
              "vitamin_mineral",
              "antibiotic",
              "pain_relief",
              "feed_water_additive",
              "foot_treatment",
              "reproductive",
              "hgp",
              "other"
            ],
            "description": "What kind of treatment. Required on create. A `drench` is the everyday word for category `drench`."
          },
          "route": {
            "type": "string",
            "nullable": true,
            "enum": [
              "oral",
              "injection",
              "backline",
              "jetting",
              "dip",
              "capsule",
              "topical",
              "in_feed_water"
            ],
            "description": "How it was given. Leave it unset if the farmer did not say; it prints `not recorded`."
          },
          "head": {
            "description": "Head treated; separators are stripped (\"1,240\" is 1240). Defaults to the mob's head count.",
            "example": 1240,
            "oneOf": [
              {
                "type": "integer",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "equipment_cleaned": {
            "type": "boolean",
            "nullable": true,
            "description": "ISC checklist item; unset is not recorded."
          },
          "adverse_reaction": {
            "type": "string",
            "nullable": true,
            "description": "Any adverse reaction or broken needle. LPA asks for both to be recorded and passed on."
          },
          "hgp_earmarked": {
            "type": "boolean",
            "nullable": true,
            "description": "HGP implant: triangle ear mark applied (cattle)."
          },
          "vet_authorisation": {
            "type": "string",
            "nullable": true,
            "description": "Off-label use: who authorised it."
          },
          "products": {
            "type": "array",
            "maxItems": 10,
            "description": "One entry per product line. On update this REPLACES the set (position is the array index).",
            "items": {
              "$ref": "#/components/schemas/TreatmentProductWrite"
            }
          }
        }
      },
      "TreatmentRecordFields": {
        "type": "object",
        "properties": {
          "image_upload_ids": {
            "$ref": "#/components/schemas/ImageUploadIdList"
          },
          "image_urls": {
            "$ref": "#/components/schemas/ImageUrlList"
          },
          "image_base64s": {
            "$ref": "#/components/schemas/ImageBase64List"
          },
          "image_mode": {
            "$ref": "#/components/schemas/ImageMode"
          },
          "remove_image_ids": {
            "$ref": "#/components/schemas/ImageRemoveIdList"
          },
          "date": {
            "type": "string",
            "format": "date",
            "description": "The day it was done, `YYYY-MM-DD` (create only; default today). The server stores midday in the application time zone so derived dates do not slip a day. A future date records a plan: it creates the open task instead of a done record.",
            "example": "2026-09-28"
          },
          "description": {
            "type": "string",
            "description": "Create only: the record's title."
          },
          "category": {
            "type": "string",
            "nullable": true,
            "enum": [
              "drench",
              "fluke",
              "lice",
              "flystrike",
              "other_external_parasite",
              "vaccine",
              "vitamin_mineral",
              "antibiotic",
              "pain_relief",
              "feed_water_additive",
              "foot_treatment",
              "reproductive",
              "hgp",
              "other"
            ],
            "description": "What kind of treatment. Required on create. A `drench` is the everyday word for category `drench`."
          },
          "route": {
            "type": "string",
            "nullable": true,
            "enum": [
              "oral",
              "injection",
              "backline",
              "jetting",
              "dip",
              "capsule",
              "topical",
              "in_feed_water"
            ],
            "description": "How it was given. Leave it unset if the farmer did not say; it prints `not recorded`."
          },
          "head": {
            "description": "Head treated; separators are stripped (\"1,240\" is 1240). Defaults to the mob's head count.",
            "example": 1240,
            "oneOf": [
              {
                "type": "integer",
                "minimum": 0
              },
              {
                "type": "string"
              }
            ]
          },
          "equipment_cleaned": {
            "type": "boolean",
            "nullable": true,
            "description": "ISC checklist item; unset is not recorded."
          },
          "adverse_reaction": {
            "type": "string",
            "nullable": true,
            "description": "Any adverse reaction or broken needle. LPA asks for both to be recorded and passed on."
          },
          "hgp_earmarked": {
            "type": "boolean",
            "nullable": true,
            "description": "HGP implant: triangle ear mark applied (cattle)."
          },
          "vet_authorisation": {
            "type": "string",
            "nullable": true,
            "description": "Off-label use: who authorised it."
          },
          "products": {
            "type": "array",
            "maxItems": 10,
            "description": "One entry per product line. On update this REPLACES the set (position is the array index).",
            "items": {
              "$ref": "#/components/schemas/TreatmentProductWrite"
            }
          }
        }
      },
      "TreatmentRecordCreateModel": {
        "type": "object",
        "description": "Nested JSON:API request body (application/vnd.api+json) for POST /mobs/{mob_id}/treatment-records: `data.attributes`. Equivalent flat shape: `TreatmentRecordCreateModelAttributes` as application/json.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "treatment-record"
                ]
              },
              "attributes": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/TreatmentRecordFields"
                  }
                ],
                "required": [
                  "category"
                ]
              }
            }
          }
        }
      },
      "TreatmentRecordCreateModelAttributes": {
        "type": "object",
        "description": "Flat request body (application/json) for POST /mobs/{mob_id}/treatment-records: the fields directly under `data`. Equivalent nested shape: `TreatmentRecordCreateModel`.",
        "properties": {
          "data": {
            "allOf": [
              {
                "$ref": "#/components/schemas/TreatmentRecordFields"
              }
            ],
            "required": [
              "category"
            ]
          }
        }
      },
      "TreatmentRecordUpdateModel": {
        "type": "object",
        "description": "Nested JSON:API request body (application/vnd.api+json) for PUT /treatment-records/{id}: `data.attributes` with any of the fields (products REPLACES the set). Equivalent flat shape: `TreatmentRecordUpdateModelAttributes`.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "type": {
                "type": "string",
                "enum": [
                  "treatment-record"
                ]
              },
              "attributes": {
                "$ref": "#/components/schemas/TreatmentRecordFields"
              }
            }
          }
        }
      },
      "TreatmentRecordUpdateModelAttributes": {
        "type": "object",
        "description": "Flat request body (application/json) for PUT /treatment-records/{id}. Equivalent nested shape: `TreatmentRecordUpdateModel`.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/TreatmentRecordFields"
          }
        }
      }
    }
  }
}