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 …
| Operation | Request | Success |
|---|---|---|
| List | GET /{collection} | 200 with a page |
| Read | GET /{collection}/{id} | 200 with the record and its ETag |
| Create or replace | PUT /{collection}/{id} | 201 (created, with Location and ETag) or 200 (replaced) |
| Change | PATCH /{collection}/{id} | 200 with the changed record |
| Delete | DELETE /{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
}
| Parameter | Meaning |
|---|---|
limit | records per page, 1 – 1000, default 100 |
cursor | the nextCursor of the previous page |
updatedAfter | start a synchronisation: only records with a later updatedAt |
| filters | one 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.
| Header | Effect |
|---|---|
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) |
| neither | create 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
| Header | On | Meaning |
|---|---|---|
ETag | responses | the record's version |
If-Match | PUT, PATCH, DELETE | act only on this version → 412 otherwise |
If-None-Match: * | PUT | create 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.
| Writer | originType | What to send |
|---|---|---|
| a person, through an app | USER | originType: USER in the body; the server records the person and the calling client |
| a processing run — a service, a device, an import | SERVICE, DEVICE or IMPORT | that 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.
| Operation | Permission |
|---|---|
| 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
Filerefers to object storage throughstorageUrl; 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
- Resources — the collections
- Errors — what a refusal looks like
- Getting started — these requests, in order