Skip to main content

Conventions

Every collection of the digital farm API behaves the same way. Learn the pattern once; Resources lists the collections.

The five operations​

For a collection /{collection} — /fields, /tasks, /observations …

OperationRequestSuccess
ListGET /{collection}200 with a page
ReadGET /{collection}/{id}200 with the record and its ETag
Create or replacePUT /{collection}/{id}201 (created, with Location and ETag) or 200 (replaced)
ChangePATCH /{collection}/{id}200 with the changed record
DeleteDELETE /{collection}/{id}204

AuditLog is the exception: it is read-only (list and read).

Representations​

Content negotiation​

Records are JSON, as specified in clause 9 of the Recommendation: camelCase, UTC timestamps, GeoJSON geometries, enumerations in upper case, identifiers as strings. An optional attribute that is not set is omitted, never null.

Ask for application/ld+json and the server adds three JSON-LD keywords in front of the same attributes — @context (https://datamodel.agrifooddata.org/v1/context.jsonld), @id (urn:uuid: and the id) and @type (the entity):

curl -H "Authorization: Bearer $TOKEN" \
-H "Accept: application/ld+json" \
https://<host>/v1/tasks/069a9d2c-0d5c-5eda-b447-1eb9d3791f7e

See Representations.

Identifiers​

A record's id is a UUID in lower case, generated by the client that creates it — so records created while offline never collide, and a request repeated after a lost response cannot create a duplicate. References hold the bare UUID of the other record.

Reading​

Listing and filtering​

A list returns a page in synchronisation order — by updatedAt, then id:

{
"items": [ { "id": "7adcec09-66ff-5ccf-8ef7-e75408287d38", "name": "Nordschlag", "…": "…" } ],
"nextCursor": "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1zZXJ2ZXI",
"hasMore": false
}
ParameterMeaning
limitrecords per page, 1 – 1000, default 100
cursorthe nextCursor of the previous page
updatedAfterstart a synchronisation: only records with a later updatedAt
filtersone query parameter per reference and enumeration attribute of the entity, e.g. farmId, status, tenure, regionId, and originType on every collection

The exact filters of each collection are in the OpenAPI description. The cursor is opaque: store it, send it back, do not interpret it. The full protocol is in Synchronization.

Reading one record​

GET /{collection}/{id} returns the record and an ETag that identifies its version — the value to send back in If-Match.

Changing a record​

Create or replace — PUT​

PUT /{collection}/{id} stores the record under the identifier the client generated. The body is the record: its id (equal to the id in the path), its attributes and its originType — USER for a person. createdAt and updatedAt are read-only; the server sets them, and the provenance attributes (createdByUserId, createdByJobId, clientId) from the authenticated caller.

HeaderEffect
If-None-Match: *create only — a second request after a lost response cannot overwrite anything (412 if the record exists)
If-Match: <ETag>replace only the version you read (412 if it changed since)
neithercreate if absent, replace otherwise
curl -X PUT -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" -H "If-None-Match: *" \
https://<host>/v1/farms/997142c3-14cf-5cb4-b260-048af4b57a76 \
-d '{ "id": "997142c3-14cf-5cb4-b260-048af4b57a76", "name": "Lindenhof",
"organizationId": "7511ba84-72fb-5ebc-b4a2-a75a6dafe0d2", "originType": "USER" }'

Change attributes — PATCH​

PATCH takes a JSON Merge Patch (RFC 7396), Content-Type: application/merge-patch+json. It names only the attributes it changes; null removes one.

curl -X PATCH -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/merge-patch+json" \
https://<host>/v1/tasks/069a9d2c-0d5c-5eda-b447-1eb9d3791f7e \
-d '{ "status": "IN_PROGRESS" }'

Conflicts resolve per attribute, last write wins: changes to different attributes never conflict, and of two changes to the same attribute the later wins. Use If-Match where you would rather fail than overwrite. A change to a status must follow the state model, otherwise it is refused with 409 invalid-transition.

Delete — DELETE​

Operational records are normally withdrawn — by a terminal status or an endsAt — rather than deleted, and the append-only audit log keeps every change. Where deletion is allowed it is recorded as a DELETE entry in /audit-logs, through which synchronising clients learn of it. A record that other records depend on cannot be deleted (409 dependent-records).

Concurrency​

HeaderOnMeaning
ETagresponsesthe record's version
If-MatchPUT, PATCH, DELETEact only on this version → 412 otherwise
If-None-Match: *PUTcreate only → 412 if it exists

These are the standard HTTP validators of RFC 9110.

Provenance​

The server sets createdAt, updatedAt, createdByUserId, createdByJobId and clientId from the authenticated caller. A client does not send them and cannot forge them.

WriteroriginTypeWhat to send
a person, through an appUSERoriginType: USER in the body; the server records the person and the calling client
a processing run — a service, a device, an importSERVICE, DEVICE or IMPORTthat originType, and the header Processing-Job-Id: <id of a ProcessingJob>; the server sets createdByJobId from it

originType is declared on creation and checked against the caller: USER for a user, SERVICE, DEVICE or IMPORT for a run named in Processing-Job-Id. (SYSTEM marks records the system itself creates, such as those of an onboarding.) It cannot be changed afterwards. Every automatic creation of data is a processing run: create a ProcessingJob first, then name it on every write.

The correction path​

Documentation that has already been reported — a record referenced by a RegulatoryReport in status SUBMITTED or later — can only be changed on the correction path: send Change-Reason: <why>, which the server records as AuditLog.reason. Without it the change is refused (422 reason-required). The submitted report itself is immutable (rule V15, 409 immutable-record); a correction there is a new report that references the earlier one.

Permissions​

Each operation requires a permission in the entity:action pattern of Role.permissions, evaluated on the farm concerned, in addition to the identity provider's policies. It is named in the OpenAPI description as x-agfoda-permission.

OperationPermission
list, read<entity>:read
PUT<entity>:create or <entity>:update — whichever the request amounts to
PATCH<entity>:update
DELETE<entity>:delete

<entity> is the entity name in camelCase: field:read, croppingPlanEntry:create, regulatoryReport:update. The OGC features endpoints need field:read, region:read and site:read; /stock-levels needs stockMovement:read.

See Scopes, audiences & permissions.

Derived views​

GET /stock-levels returns stock levels computed from the stock movements and never stored — optionally filtered by productId, lotId and storageLocationId. Levels in different units are not added up.

What the binding deliberately leaves out​

  • How a client obtains its token. That is the identity provider's business — see Authentication.
  • Rate limits.
  • Bulk import.
  • The transfer of file contents. A File refers to object storage through storageUrl; the API holds the record, not the bytes.
  • The interfaces of external services. SensorThings endpoints are referenced (Device.sensorThingsUrl), not described.
  • An export of the provenance graph as PROV-O.

See it in action​