---
title: "Update Paddock Boundary"
method: PATCH
path: "/paddocks/{id}/boundary"
tags: ["Paddocks"]
---

# Update Paddock Boundary

`PATCH /paddocks/{id}/boundary`

## Request

- Base URL: `https://api.purplegrain.com/api/v1`
- URL: `https://api.purplegrain.com/api/v1/paddocks/{id}/boundary`
- Auth: HTTP bearer

Save (or clear) a paddock's outline.

A dedicated endpoint rather than attributes on `updatePaddock`, because
it is the single place two rules are enforced:

* **A typed area is never overwritten by a drawn one.**
  `area_source: "drawn"` recomputes `area_ha` from the ring;
  `area_source: "typed"` saves the ring and leaves `area_ha` exactly as
  it is. A request that names no source keeps the paddock's own, so
  omitting the field can never clobber a typed number; a paddock that has
  never had one gets `drawn`.
* **Geometry is repaired before it is measured.** Rings are validated
  (at least 4 positions, closed, finite, latitude within ±90 and
  longitude within ±180), repaired with `ST_MakeValid`, wound
  counter-clockwise and stored as a MultiPolygon — so a self-crossing
  ring cannot report a plausible-but-wrong hectare figure. The geometry
  read back may therefore be reordered or split into parts.

* **A paddock ring must be near its property.** A ring whose centroid
  lies more than 25 km from the property's map point (`centroid_lat` /
  `centroid_lon` on the property) is refused with a 422 that names the
  distance — with no imagery under the editor, that is the ring drawn
  over the wrong ground. A property with no map point yet is not
  guarded.

Area is computed by PostGIS as `ST_Area(geography)` in hectares. A
paddock carries no map point of its own — it inherits its property's, and
the point its name is drawn at is the ring's mean.

**Provenance.** `boundary_source` (traced | grid | gps | imported) is
the client's claim about how the ring was captured and travels with it;
`boundary_captured_by` is stamped server-side from the acting user and
is never accepted from the request. Both are cleared with the ring.

**The diary.** When the save changes `area_ha` — a first ring measured
under `drawn`, a redraw that measures differently, or a cleared ring
that takes a measured area with it — one completed observation is filed
on the paddock in the same transaction ("Boundary redrawn — area 70 ha,
was 84 ha"; "Boundary drawn — area 70 ha"; "Boundary removed — was
84 ha"), through the same path a rename or a typed area change uses. A
ring that measures the same, or one saved over a typed area, writes
nothing. Requires organization edit rights.

## Path parameters

- `id` string, required

## Query parameters

- `include` string

## Request body

- PaddockBoundaryModelAttributes: 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.
  - `data` object, required
    - `boundary` union: The outline being written: a GeoJSON `Polygon` or `MultiPolygon` (RFC 7946, longitude first), or the legacy array of rings. Rings are repaired (`ST_MakeValid`) and wound counter-clockwise before the area is measured, so the geometry read back may be reordered or split into parts; the response `boundary` is always the array of rings. Omit the key to leave the stored geometry untouched; send `null` to clear it.
      - GeoJSONPolygon: An RFC 7946 Polygon. Exactly one ring (the exterior); holes are refused.
        - `type` 'Polygon', required
        - `coordinates` GeoJSONLinearRing[], required
          - GeoJSONPosition[]: One closed ring — at least 4 positions, the first repeated last.
            - number[]: [longitude, latitude] — longitude FIRST (RFC 7946).
      - GeoJSONMultiPolygon: An RFC 7946 MultiPolygon — one hole-less polygon per part (a paddock either side of a creek).
        - `type` 'MultiPolygon', required
        - `coordinates` array[], required
          - GeoJSONLinearRing[]
            - GeoJSONPosition[]: One closed ring — at least 4 positions, the first repeated last.
              - …
      - GeoJSONLinearRing[]: The legacy form: an array of exterior rings, each an array of `[longitude, latitude]` pairs with at least 4 positions and the first repeated last. Still accepted everywhere a boundary is written; prefer the GeoJSON form. Deprecated (decision 2026-10-28, Phase 6): a response to a write that sent this form carries `Deprecation` (RFC 9745) and a `Link` with `rel="deprecation"` (`X-Deprecated-Form: boundary_rings`), until the owner sets a `Sunset`. Not `deprecated: true`: the schema is one of the forms `boundary` takes, and the MCP tools' body schema keeps it.
        - GeoJSONPosition[]: One closed ring — at least 4 positions, the first repeated last.
          - number[]: [longitude, latitude] — longitude FIRST (RFC 7946).
    - `area_source` 'typed' | 'drawn', nullable: `drawn` recomputes `area_ha` from the ring; `typed` leaves `area_ha` untouched. Omitted, the paddock's own source stands.
    - `boundary_source` 'traced' | 'grid' | 'gps' | 'imported', nullable: How the ring being saved was captured. Provenance describes THIS ring: omitted alongside a new `boundary`, the stored source is reset to null rather than carried over from the ring it replaced; sent without a `boundary`, it corrects the stored ring's source in place. Cleared with the ring. `boundary_captured_by` is not accepted — the server stamps it from the acting user.

## Response `200`

The paddock with its saved boundary.

- PaddocksShowResponseAttributes
  - `data` PaddockModelAttributes: Single paddock in attributes-adapter shape (Accept application/json).
    - `id` string
    - `type` string
    - `slug` string: 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` string: The address on the web. Present when read directly.
    - `url` string: The whole link; carries the property's `?share=` token when it is shared by link. Present when read directly.
    - `name` string
    - `area_ha` number, nullable: Paddock area in hectares. Measured from the boundary when `area_source` is `drawn`; the farmer's own number when it is `typed`.
    - `area_source` 'typed' | 'drawn', nullable: Where `area_ha` came from. A `typed` area is never overwritten by a drawn one.
    - `land_use` 'cropping' | 'grazing' | 'mixed' | 'fallow', nullable
    - `tenure` 'owned' | 'leased', nullable: Whether the area is owned or leased. Null when unset.
    - `current_crop` string, nullable
    - `boundary` array[], nullable: The paddock outline: an array of rings, each ring an array of `[longitude, latitude]` pairs in RFC 7946 order — **longitude first** — with at least 4 positions and the first repeated last. Not a GeoJSON object (see `PaddockBoundaryModel`).
      - array[]: One closed exterior ring.
        - number[]: [longitude, latitude]
    - `has_boundary` boolean: Whether the paddock has an outline at all — the cheap face of `boundary`.
    - `withholding` object, nullable: 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).
      - `task_id` string
      - `record_id` string: The SprayRecord id.
      - `safe_to_graze_on` string, date, nullable
      - `safe_to_harvest_on` string, date, nullable
      - `products` string[]
    - `boundary_captured_at` string, date-time, nullable
    - `boundary_source` 'traced' | 'grid' | 'gps' | 'imported', nullable: 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` string, nullable: 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` boolean: An archived paddock leaves the map and the lists; its records stay on the property.
    - `archived_at` string, date-time, nullable
    - `block_id` string, uuid, nullable: The block the paddock sits in. Null in the home block.
    - `parcel_id` string, uuid, nullable: The cadastral parcel this paddock sits in. Null until one is attached to the property and chosen here.
    - `record_count` integer: Completed rows on this paddock — its diary.
    - `open_task_count` integer: Pending rows on this paddock.
    - `last_record_at` string, date-time, nullable
    - `created_at` string, date-time
    - `updated_at` string, date-time
    - `property` object: Present when include=property
  - `meta` PaginationMeta
    - `current_page` number
    - `next_page` number, nullable
    - `prev_page` number, nullable

## Other responses

- `400`: The `include` query parameter names a relationship path this endpoint does not take (JSON:API 1.1, "Inclusion of Related Resources"). One error with `code: "unknown_include"` and `source.parameter: "include"`; its `detail` names the refused path(s) and the paths the endpoint allows (the parameter's `x-include-paths`). An endpoint whose serializer whitelists no relationship refuses any path. Rolling out as monitor then enforce: until the API's `JSONAPI_UNKNOWN_INCLUDE` is `reject`, an unknown path is dropped and logged instead.
- `401`: Unauthorized
- `403`: Forbidden
- `404`: Not Found
- `409`: Conflict — the JSON:API request document names a different resource from the URL (JSON:API 1.1 §9.2 and §9.3). `errors[0].code` is `type_mismatch` when `data.type` is not one of this endpoint's resource types (`source.pointer` is `/data/type`), or `id_mismatch` when, on an update, `data.id` is not the id in the URL (`/data/id`). Only a body sent as `application/vnd.api+json` is checked; the flat `application/json` body has no resource object. Nothing was changed. Request validation runs first: a `data.type` outside the operation's documented enum, or missing where the schema requires it, is a `400` before this check, so the 409 is seen where the documented type is a free string, or when validation is off.
- `415`: Unsupported Media Type — the request's `Content-Type` is `application/vnd.api+json` with a media type parameter the JSON:API media type does not allow (anything other than `ext` and `profile`; `; charset=utf-8` is the usual one). Send the bare media type. The error's `code` is `unsupported_media_type` and `source.header` is `Content-Type`. Until the switch is on (`JSONAPI_CONTENT_TYPE_PARAMS=reject`) such a request is let through and logged; the operation documents the response it will have.
- `422`: 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`

## Changes

- **2026-10-30** `e32f34ba218b`: 1 info
  - added the optional property `data/relationships/block` to the response with the `200` status
- **2026-10-30** `143d65e52617`: 3 info
  - added the non-success response with the status `400`
  - added the non-success response with the status `409`
  - added the non-success response with the status `415`
- **2026-10-30** `92c4663a1266`: 12 warning, 12 info
  - removed the optional property `_meta` from the response with the `404` status (media type: application/json)
  - removed the optional property `_meta` from the response with the `404` status (media type: application/vnd.api+json)
  - removed the optional property `_meta` from the response with the `422` status (media type: application/json)
  - removed the optional property `_meta` from the response with the `422` status (media type: application/vnd.api+json)
  - …20 more
- **2026-10-30** `c1bc18b1cdbc`: 1 info
  - endpoint added

[Operation history](https://skmtc.dev/purplegrain/apis/purple-grain-api/changes/paddocks/:id/boundary/patch.md)

---

[API](https://skmtc.dev/purplegrain/apis/purple-grain-api.md) - [All operations](https://skmtc.dev/purplegrain/apis/purple-grain-api/llms.txt) - [OpenAPI document](https://skmtc.dev/purplegrain/apis/purple-grain-api/revisions/370fe2f3ed4f?raw)
