# Overview (/en/docs)
Developer documentation for integrating with agrirouter, the universal data exchange platform for agricultural machinery and applications
agrirouter is a universal data exchange platform for farmers and contractors. It connects agricultural machinery, farm management software, and telemetry systems so they can exchange data regardless of manufacturer.
These docs are for **developers integrating software or hardware** with the agrirouter platform.
Returning developer? Check the [Changelog](/changelog) for the latest platform updates, including the agrirouter 2.0 migration.
# Confirm received messages (/en/docs/api/confirmMessages)
Acknowledge received messages so agrirouter marks them as processed for the confirming endpoint.
Confirm that messages have been received and processed by the application.
Each confirmation carries a message ID and the endpoint ID that received it.
The same message can reach multiple endpoints, and each endpoint confirms it
separately, either in the same request or in different ones.
# Delete endpoint (/en/docs/api/deleteEndpoint)
Remove an endpoint from agrirouter by external ID so it no longer receives messages.
Delete an endpoint by external ID. The endpoint is removed from agrirouter and no longer receives messages.
Deleting an endpoint that is the [owner (parent)](/api/putEndpoint#owner-parent-endpoint) of other endpoints also deletes every endpoint it owns.
# Errors (/en/docs/api/errors)
How the current agrirouter API returns errors, plus reference for legacy error codes
agrirouter has two API surfaces with very different error conventions. The current API returns plain human-readable JSON messages. The legacy API returns numeric codes inside a protobuf envelope. This page covers both.
Do not surface agrirouter error messages one-to-one to end users. Wrap them with application-specific context so the guidance is meaningful in your UI.
Every error response on the current API has the same shape:
```json
{
"message": "A human-readable description of the error."
}
```
There is no numeric code field on the wire. Branch on the HTTP status code; the `message` string carries the human-readable detail. Values like message IDs or endpoint IDs are already substituted into the text, so you can log it directly or wrap it in your own UI copy.
### HTTP status codes [#http-status-codes]
The current API uses the following status codes across its endpoints. Individual endpoint pages list which statuses apply to that endpoint.
| Status | Meaning |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `200` | The request succeeded. |
| `204` | The request succeeded and there is no response body (typical for `DELETE /endpoints/{externalId}`). |
| `400` | The request is invalid (malformed headers, missing required fields, unsupported message type), or, for `POST /messages`, one or more direct recipients are not reachable. See [Routing failures](#routing-failures-on-post-messages). |
| `401` | The access token is missing, expired, or otherwise not valid. |
| `403` | The endpoint ID in the request header does not belong to a tenant that the caller's access token is authorized for. |
| `404` | The requested endpoint, message, or resource does not exist. |
| `413` | The payload exceeds the [256 MB size limit](/concepts/messaging#chunking). |
| `429` | The application has exceeded its per-application request rate. Back off and retry. See [Rate limits](#rate-limits). |
| `5xx` | An internal error in agrirouter. Retry with exponential backoff. |
Check the agrirouter status page at [agrirouter.statuspage.io](https://agrirouter.statuspage.io/) for ongoing incidents or maintenance windows before investigating `5xx` responses in your own integration.
### Rate limits [#rate-limits]
Request limits are enforced per application across all its endpoints. When the limit is exceeded, the gateway responds with HTTP `429` and a plain `{ "message": "rate limit exceeded" }` body. The response does **not** include a `Retry-After` header, so client code should fall back to its own exponential backoff.
Concrete limits vary by environment and application tier and may change over time. For the current limit for your application, write to .
### Routing failures on `POST /messages` [#routing-failures-on-post-messages]
Direct recipients are evaluated individually against routes and capabilities. The message text tells you whether anything was delivered:
| `message` | Meaning | Delivered? |
| --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- |
| `No recipients for this sender and info type` | None of the direct recipients is reachable: no route from the sender, or the recipient lacks the capability. Legacy code `VAL_000004`. | No |
| `Recipient is not allowed from this sender` | At least one, but not all, direct recipients are reachable. Legacy code `VAL_000005`. | Yes, to the reachable recipients |
A partial-delivery `400` must not be retried against the full recipient list. See [Multi-recipient direct sends](/integration/sending-and-receiving#multi-recipient-direct-sends-are-not-atomic).
### Relationship to the legacy codes [#relationship-to-the-legacy-codes]
Internally, agrirouter still runs the same validation logic that produces the legacy `VAL_000XXX` and `SYS_000XXX` codes listed below. On the current API, the numeric code is stripped before the response goes out, and only the human-readable message reaches the wire. That means:
* You cannot distinguish `VAL_000004` (no recipient reachable) from `VAL_000005` (some recipients reachable, partial delivery) from the status line alone. Both surface as HTTP `400`; only the `message` text differs, see [Routing failures](#routing-failures-on-post-messages).
* Your code should rely on the HTTP status and, where disambiguation matters, on matching the message string. It should not attempt to parse numeric codes out of the JSON body.
## Legacy API error codes [#legacy-api-error-codes]
The numeric codes in the tables below are returned only by the [Legacy API](/api/legacy) in its protobuf response envelope. The current API strips these codes and returns only the `{ "message": "..." }` body described above. They are kept here as a reference for partners maintaining legacy integrations.
### Message placeholders [#message-placeholders]
Legacy error messages may contain the following placeholders, which agrirouter replaces with actual values at runtime:
| Placeholder | Description |
| ------------------------ | ------------------------------------------- |
| `%messageId%` | The internal agrirouter message ID |
| `%applicationMessageId%` | The message ID assigned by your application |
| `%technicalMessageType%` | The technical message type of the message |
| `%endpoints%` | The affected endpoint IDs |
| `%chunkContextId%` | The chunk context ID for chunked messages |
| `%chunkNumber%` | The current chunk number |
| `%chunkTotal%` | The total number of chunks |
### Error categories [#error-categories]
System errors point to internal agrirouter issues. If they persist, check the status page or contact support.
| Code | Message | Description |
| ------------ | ------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `SYS_000001` | Unexpected internal error for message %messageId% | An internal system error occurred. Retry the request; if it persists, contact support. |
These errors point to problems with the format or content of your request.
| Code | Message | Description |
| ------------ | ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `VAL_000001` | Malformed request: protobuf/json can not be parsed for message %messageId% | The request body could not be parsed. Check that your protobuf or JSON encoding is correct. |
| `VAL_000002` | Malformed message header for message %messageId% | One or more required header fields are missing or invalid. |
| `VAL_000003` | Unsupported message for message %messageId% of type %technicalMessageType% | The technical message type is not recognized. Verify the type string. |
| `VAL_000004` | No recipients for this sender and info type | None of the addressed recipients is reachable through a route with matching capabilities. Nothing was delivered. |
| `VAL_000005` | Recipient is not allowed from this sender | Some, but not all, addressed recipients are reachable. The message was delivered to the reachable ones. |
| `VAL_000006` | Publish only, no error even if no partner found | The message was published without a specific recipient. No error is raised even if no recipient receives it. |
| `VAL_000007` | No recipients found for message %applicationMessageId% of type %technicalMessageType% | No valid recipients could be resolved. Ensure endpoints are reachable and routes exist. |
| `VAL_000008` | Message %applicationMessageId% could not be delivered to endpoint %endpoints% | Delivery failed for the specified endpoints. The endpoints may be offline or unreachable. |
| `VAL_000009` | Duplicate message %applicationMessageId% | A message with this application message ID was already processed. Use unique IDs. |
| `VAL_000010` | Message %applicationMessageId% exceeds maximum size | The message payload exceeds the maximum request size. Reduce the payload size. |
| `VAL_000011` | Chunk %chunkNumber% of %chunkTotal% for context %chunkContextId% is invalid | A chunk in the sequence is malformed or out of order. Resend the complete chunk sequence. |
| `VAL_000012` | Chunk context %chunkContextId% has expired | The chunked transfer took too long and the context expired. Resend all chunks. |
| `VAL_000013` | Chunk %chunkNumber% of %chunkTotal% for context %chunkContextId% already received | This chunk was already uploaded. Do not resend chunks that have been acknowledged. |
These errors relate to subscription and capability configuration.
| Code | Message | Description |
| ------------ | ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `VAL_000050` | Subscription for %technicalMessageType% not valid | The subscription for this message type is not valid or has not been set up. |
| `VAL_000051` | Capability for %technicalMessageType% not valid | The endpoint does not have the required capability for this message type. Update your capabilities declaration. |
| `VAL_000052` | Sending of %technicalMessageType% not allowed | The endpoint is not permitted to send this message type. Check your capability configuration. |
Onboarding errors occur during the legacy endpoint registration process.
| Code | Message | Description |
| ------------ | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `VAL_000201` | Registration code is invalid | The registration code (TAN) is not valid. It may have expired or already been used. |
| `VAL_000202` | Registration code has expired | The TAN has expired. Request a new one. |
| `VAL_000203` | Endpoint already exists | An endpoint with this external ID already exists for this tenant and application. |
| `VAL_000204` | Application not found | The application ID does not match any registered application. |
| `VAL_000205` | Certification not found | The `certificationVersionId` from your legacy onboarding request could not be resolved. Verify the value issued to your application in the developer portal. |
| `VAL_000206` | Onboarding request is invalid | The onboarding request body is malformed or missing required fields. |
| `VAL_000207` | Gateway type not supported | The requested gateway type (MQTT/HTTP) is not supported for this operation. |
| `VAL_000208` | Reonboarding not possible | The endpoint cannot be reonboarded. It may have been permanently revoked. |
| `VAL_000209` | Revoke not possible | The endpoint cannot be revoked in its current state. |
Errors related to Virtual CUs (VCUs) and device management.
| Code | Message | Description |
| ------------ | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `VAL_000300` | VCU onboarding failed | The Virtual CU could not be onboarded. Check the parent endpoint and permissions. |
| `VAL_000301` | VCU not found | The specified VCU does not exist or is not accessible. |
| `VAL_000302` | VCU limit reached | The maximum number of VCUs for this endpoint has been reached. |
| `VAL_000303` | Not allowed to send message type %technicalMessageType% | The VCU is not allowed to send this message type. This occurs when registering a VCU on an endpoint that is not a telemetry platform endpoint. |
### Troubleshooting legacy responses [#troubleshooting-legacy-responses]
* **SYS errors**: server-side. Retry with exponential backoff. If they persist, check the status page.
* **VAL\_000001–VAL\_000003**: double-check your message encoding and header fields.
* **VAL\_000004 and VAL\_000005**: review your route configuration in the agrirouter UI. On the current API, both surface as HTTP `400`; the message text tells complete and partial failures apart.
* **VAL\_000201–VAL\_000209**: check your legacy onboarding flow. TANs must be fresh and application IDs correct.
* **VAL\_000303**: typically means you are trying to use a VCU capability on an endpoint that is not a telemetry platform endpoint.
## Next steps [#next-steps]
# API Reference (/en/docs/api)
API reference for the agrirouter gateway API: endpoints, authentication, and request patterns
The agrirouter API is a small REST surface for managing endpoints, exchanging messages, and receiving events.
If you are maintaining an existing integration that uses the legacy protocol, see the [Legacy API](/api/legacy) documentation.
## Gateway Endpoints [#gateway-endpoints]
See the sidebar for the full list of API operations and their reference pages.
## Base URLs [#base-urls]
The API is available in two environments. Partner integrations use **Production**; QA is reserved for agrirouter team use and for partners DKE has explicitly directed there. See [Environments](/integration/environments).
| Environment | Base URL |
| -------------------- | -------------------------------------------------------------- |
| Production (default) | |
| QA | |
See the [URLs appendix](/appendix/urls) for the complete list of environment URLs.
## Authentication [#authentication]
Every request requires a `Bearer` token in the `Authorization` header. Applications obtain this token via the OAuth2 client credentials flow (see [Authorization & Security](/concepts/authorization-and-security)).
## Common Headers [#common-headers]
| Header | Value | Description |
| --------------- | ------------------ | --------------------------------------------------- |
| `Content-Type` | `application/json` | All request bodies are JSON |
| `Authorization` | `Bearer ` | JWT from client credentials or endpoint credentials |
## Request and Response Patterns [#request-and-response-patterns]
* **Request bodies** are JSON-encoded.
* **Events** come in over a Server-Sent Events (SSE) stream from `GET /events`. Keep a persistent connection open to receive them in real time.
* **Payloads** arrive inline in events or via a `payload_uri` the client downloads directly.
## OpenAPI Specification [#openapi-specification]
Use the machine-readable OpenAPI document to generate clients, import the API into tools, or keep a local copy of the contract:
| Format | URL |
| ------ | --------------------------------------------------------------------------------------------------------------------------------- |
| YAML | |
| JSON | |
The YAML endpoint is also available at for tools that prefer a shorter URL.
## Credentials Package Schema [#credentials-package-schema]
G4 API credentials packages are JSON files that describe one software version and one OAuth client. Use the schema to validate downloaded packages before importing them into deployment tooling:
| Format | URL |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| JSON Schema | |
The schema endpoint is also available at .
## Additional Resources [#additional-resources]
# Legacy API (/en/docs/api/legacy)
Documentation for the legacy agrirouter API, kept for existing integrations and migration reference
The legacy agrirouter API is still available for integrations that have not yet migrated to the current API. It supports MQTT and REST, with protobuf-encoded messages on both.
New integrations should use the [current API](/api). The legacy API remains available for existing integrations.
Migrating from the Legacy API
## Key Differences from the Current API [#key-differences-from-the-current-api]
| Aspect | Legacy API | API |
| -------------- | ------------------------------------------------------ | --------------------------- |
| Protocols | MQTT and REST | REST only (SSE for events) |
| Message format | Protobuf-encoded with envelope structure | JSON request bodies |
| Onboarding | Separate onboarding/reonboarding/revoke REST endpoints | Unified endpoint management |
| Architecture | Multiple endpoints per protocol | 4 gateway endpoints |
## MQTT Communication [#mqtt-communication]
MQTT gives you two-way, real-time communication with agrirouter. After onboarding, you receive two MQTT topics:
* **Measures topic**: used to send messages to agrirouter
* **Commands topic**: used to receive messages and acknowledgements from agrirouter
Your application maintains a persistent MQTT connection and publishes or subscribes to these topics as needed.
## REST Communication [#rest-communication]
The legacy REST protocol uses a one-way polling model:
1. **Send messages** by posting to the outbox URL provided during onboarding.
2. **Receive messages** by polling the inbox URL at regular intervals.
Your application polls for new messages rather than receiving them in real time.
## Message Format [#message-format]
All legacy messages use Protocol Buffers (protobuf) encoding with the following header fields:
| Field | Description |
| ------------------------- | ---------------------------------------------------------------------------------- |
| `ApplicationMessageId` | Unique ID assigned by your application |
| `ApplicationMessageSeqNo` | Sequence number for ordering |
| `technicalMessageType` | The type of message being sent (e.g., `iso:11783:-10:device_description:protobuf`) |
| `recipients` | Target endpoint IDs |
| `chunkInfo` | Chunking metadata for large messages |
The message envelope wraps the header and the protobuf-encoded payload together for transport.
## Error Responses [#error-responses]
Legacy responses carry numeric error codes (`SYS_*`, `VAL_*`) in the protobuf envelope alongside a human-readable message. These codes remain for partners maintaining legacy integrations. The current API strips them and returns only `{ "message": "..." }`.
Legacy API error codes
## Migration Path [#migration-path]
Switching gateways between MQTT and HTTP is now allowed in agrirouter 2.0, but data loss is possible during the switch. Plan the transition carefully and make sure no critical messages are in transit.
* **New integrations** should use the current API exclusively.
* **Existing integrations** can continue using the legacy API without changes.
* **Switching gateways** (MQTT to HTTP or vice versa) is supported in agrirouter 2.0 but requires caution.
## SDKs [#sdks]
Several SDKs cover the legacy API:
SDKs and Libraries
# List authorized tenants (/en/docs/api/listAuthorizedTenants)
List every tenant for which the current application has an existing authorization, together with each tenant's visible endpoints.
Returns every tenant for which your application has an existing authorization, together with the related endpoints currently visible to the application in each tenant. This is the primary global synchronization operation: call it on application startup, after a crash, or whenever you need to rebuild your complete view of authorized tenants from scratch.
For an ongoing, push-based view of the same data, subscribe to the [`AUTHORIZATION_ADDED`](/api/events/authorization-added), [`AUTHORIZATION_REVOKED`](/api/events/authorization-revoked), and [`ENDPOINTS_LIST_CHANGED`](/api/events/endpoints-list-changed) events on [`GET /events`](/api/receiveEvents).
`tenants[].endpoints` follows the privacy rule documented on the schema: until your application has at least one of its own endpoints in a tenant, that tenant's `endpoints` array is empty even if other endpoints exist there. The tenant itself still appears in the response.
# List tenant endpoints (/en/docs/api/listTenantEndpoints)
List the endpoints currently visible to your application in one tenant, including route information for endpoints owned by your application.
Returns the current list of endpoints visible to your application in one already-known tenant, together with capability information. For endpoints owned by your application, the response also includes route-derived `can_send_to` and `can_receive_from` maps describing which other endpoints they can exchange which message types with.
Use this operation to inspect or refresh endpoint information for a single tenant, for example when you cannot or do not want to persist the data carried by [`ENDPOINTS_LIST_CHANGED`](/api/events/endpoints-list-changed), or when your frontend does not have access to that data. It is not intended as the primary application-startup or global re-sync operation; for that, use [`GET /tenants`](/api/listAuthorizedTenants), which returns all currently authorized tenants together with their related endpoints.
# Create or update endpoint (/en/docs/api/putEndpoint)
Create a new endpoint or replace the configuration of an existing one by external ID.
Create a new endpoint, or update an existing one. The first call for a given `externalId` creates the endpoint. Later calls replace its configuration (capabilities, subscriptions, and endpoint type).
## Owner (parent) endpoint [#owner-parent-endpoint]
An endpoint can be given an **owner** (parent) endpoint by setting the optional `owner_endpoint_external_id` field to the external ID of another endpoint. This models a parent–child relationship between two endpoints — for example a platform endpoint that owns the machine endpoints it manages.
The referenced owner endpoint must belong to the **same tenant and the same application** as the endpoint being created or updated. If it does not — or if the external ID cannot be resolved — the request is rejected with `400`.
Because `PUT` replaces the endpoint's full configuration, the owner is treated like any other attribute: send `owner_endpoint_external_id` on every request that should keep the owner, and omit it to leave the endpoint without an owner (which also clears a previously set owner).
Endpoint state is propagated asynchronously, so an owner endpoint that was just created may not be resolvable immediately. The gateway retries resolution for a few seconds before giving up. If the request still fails with `400`, retry it once the owner endpoint has been created.
Deleting an owner endpoint also deletes all endpoints it owns. See [Delete endpoint](/api/deleteEndpoint) and the [endpoint lifecycle](/concepts/endpoints#endpoint-lifecycle).
# Receive events (/en/docs/api/receiveEvents)
Open a Server-Sent Events stream that delivers message and endpoint events for every endpoint of the authorized application.
Open a stream of events from agrirouter. Events fire for every endpoint of the authorized application and are delivered as Server-Sent Events (SSE): each event is a pair of lines, where `event:` names the event type and `data:` carries the JSON payload. See the [Server-Sent Events specification](https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events) for transport details.
For the catalog of event types and the full field list and sample frame of each one, see the Events page in the integration guide. The per-event payload schemas (named `*EventData`) are also rendered in the playground below.
Events
## Connection behaviour [#connection-behaviour]
* **Filtering**: pass `types` once per event type to limit the stream, for example `GET /events?types=MESSAGE_RECEIVED&types=FILE_RECEIVED`.
* **Keep-alive**: while idle, the gateway writes an SSE comment line (`: keep-alive`) every 5 seconds. Treat a longer silence as a dead connection and reconnect.
* **Payload links**: `payload_uri` values are pre-signed object storage URLs, valid for 15 minutes. See [Payload downloads](/appendix/urls#payload-downloads) for the host to allow.
# Send one or several messages (/en/docs/api/sendMessages)
Send one or several messages through agrirouter to other endpoints, with automatic chunking for large payloads.
Send one or several messages through agrirouter to other endpoints.
The payload goes in the request body as a binary stream.
If it exceeds the transport chunk size, agrirouter splits it for you,
so the client does not need to chunk on its own.
Every delivered message lands in the feed of each recipient that a route
points to.
# Contact (/en/docs/appendix/contact)
How to reach the agrirouter team for support, access, credentials, and integration questions
Use this page when you need to reach the agrirouter team. Email is the primary channel for support and coordination requests.
## When to contact support [#when-to-contact-support]
Write to when you need help with:
* Developer account promotion
* Credential requests
* IO-Tool access
* Implementation review coordination
* Current limits for your application
* Integration questions that are not answered in the documentation
If your question is about an existing business or project relationship and you already have a named contact person at DKE, you can contact that person directly instead of going through general support.
## Related references [#related-references]
Development steps that may require manual coordination with the agrirouter team.
What to prepare before requesting the review for a production integration.
Environment URLs and service references for agrirouter.
# Glossary (/en/docs/appendix/glossary)
Glossary of agrirouter terms and abbreviations
Definitions for the terms and abbreviations used throughout the agrirouter documentation.
| Term | Definition |
| ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **agrirouter** | Universal data exchange platform for agriculture, connecting machines, software, and services across manufacturer boundaries. |
| **App / Application** | A software product registered with agrirouter. Each application receives a unique application ID and is managed through the agrirouter developer portal. |
| **App Provider** | The company that builds and operates an application integrated with agrirouter. Holds a developer account and is responsible for the integration's technical side (authorization, endpoint creation, messaging). See [App Providers](/concepts/ecosystem#app-providers). Previously sometimes called "solution provider". |
| **CAP / Capabilities** | Declaration of which technical message types an endpoint can send and/or receive. Capabilities are included in the endpoint creation request and updated whenever they change. |
| **Cloud Software** | A cloud-hosted application that integrates with agrirouter on behalf of an end user. Registered as the [`cloud_software`](/concepts/ecosystem#cloud_software) endpoint type. Replaces the legacy "Farming Software" and "Telemetry Platform" framings. |
| **CU / Communication Unit** | A physical hardware device (terminal, telemetry box, or ISOBUS gateway) that connected to agrirouter via the legacy API. Not an endpoint type in the current API: new hardware integrations register as a [`virtual_communication_unit`](/concepts/ecosystem#virtual_communication_unit) instead. See [Migrating from Legacy](/appendix/migrating-from-legacy). |
| **DDI / Data Dictionary Identifier** | Standardized numeric identifiers for sensor values and task parameters, defined by the ISOBUS standard (ISO 11783). |
| **DVC / Device Description** | An EFDI message that describes the devices (implements, sensors) attached to a Communication Unit or VCU. |
| **EFDI** | Extended Farm Device Interface, a protobuf-based format used by agrirouter for transmitting device descriptions and live telemetry data. |
| **EP / Endpoint** | The representation of a software or hardware instance within the agrirouter platform. Each application instance connected to agrirouter becomes an endpoint. |
| **Farming Software** | Legacy framing for a cloud or desktop application that exchanges agricultural data with agrirouter. **Deprecated** as an endpoint type: new applications should register as [`cloud_software`](/concepts/ecosystem#cloud_software). The `farming_software` value is still accepted for backward compatibility. See [Migrating from Legacy](/appendix/migrating-from-legacy). |
| **Feed** | Temporary storage within agrirouter for unread messages destined for a specific endpoint. Messages remain in the feed until retrieved or expired. |
| **ISOBUS** | ISO 11783, the international standard for serial data communication between agricultural electronics, including tractors, implements, and farm software. |
| **ISOXML** | An XML-based data format for task data defined by ISO 11783-10. Used for exchanging prescriptions, field boundaries, and recorded task results. |
| **IT / Information Type** | A user-facing grouping of related technical message types. Information types simplify routing configuration for end users by bundling similar data formats together. |
| **MSG / Message** | A unit of data exchanged between endpoints through the agrirouter platform. Each message has a header (metadata) and a body (payload). |
| **MQTT** | Message Queuing Telemetry Transport, a lightweight publish/subscribe protocol used as the legacy transport layer for agrirouter communication. |
| **REST** | Representational State Transfer, an HTTP-based protocol style used as a transport layer for agrirouter communication. |
| **Route** | A connection configured by the account owner in the agrirouter UI that allows messages of a given information type to flow between two endpoints. Previously called "routing rule" in older documentation, and sometimes called "routing" in the UI. See [Migrating from Legacy](/appendix/migrating-from-legacy). |
| **SSE** | Server-Sent Events, a unidirectional protocol used by the current API for receiving real-time events from agrirouter. |
| **TeamSet** | A collection of devices reported by a Communication Unit or VCU. Each TeamSet has a unique TeamSet ID and represents the current configuration of attached implements. See [TeamSet Context ID](/message-types/efdi#teamset-context-id). |
| **TeamSet Context ID** | The identifier for a TeamSet passed on the wire as the `x-agrirouter-teamset-context-id` header. Sometimes also called "TeamSet ID". See [TeamSet Context ID](/message-types/efdi#teamset-context-id). |
| **Telemetry Platform** | Legacy framing for a cloud application that acted as a concentrator for a fleet of machines. The current API has no `telemetry_platform` endpoint type: a cloud application is registered as [`cloud_software`](/concepts/ecosystem#cloud_software), and each machine is registered directly as its own [`virtual_communication_unit`](/concepts/ecosystem#virtual_communication_unit) endpoint. See [Migrating from Legacy](/appendix/migrating-from-legacy). |
| **TMT / Technical Message Type** | A specific data format identifier used by agrirouter to classify messages (e.g., `iso:11783:-10:taskdata:zip`). Capabilities and routes are defined in terms of TMTs. |
| **TP** | Legacy abbreviation for Telemetry Platform. See **Telemetry Platform**. |
| **VCU / Virtual Communication Unit** | An endpoint that represents a physical machine (tractor, implement, or ISOBUS-connected unit) participating in data exchange. Registered as the [`virtual_communication_unit`](/concepts/ecosystem#virtual_communication_unit) endpoint type via its own `PUT /endpoints/{externalId}` call, with its own external ID, capabilities, subscriptions, and feed. |
| **URN** | Uniform Resource Name, a standardized naming format used for external identifiers in agrirouter, such as endpoint IDs. |
# Appendix (/en/docs/appendix)
Quick-reference lookup for agrirouter terms, limits, URLs, and contact options
Quick-reference material for working with agrirouter: terminology, system constraints, environment URLs, and contact options.
Definitions of agrirouter terms, abbreviations, and concepts.
Legacy and obsolete terms mapped to their equivalents in the current API.
System constraints including account limits, message sizes, rate limits, and storage retention.
Complete URL reference for all agrirouter environments and services.
How to reach the agrirouter team for support, access, credentials, and integration questions.
# Limitations (/en/docs/appendix/limitations)
System constraints. Message size, request rate, endpoint quotas, feed retention, payload link validity, and confirmation batches
agrirouter enforces several system constraints you need to know about when developing and operating an integration.
The values on this page are the current defaults and may change over time. If your integration needs more than the defaults, write to .
## Message Size [#message-size]
The maximum payload size accepted by the API is **256 MB** per request. Payloads larger than that are rejected with HTTP 413.
## Request Rate [#request-rate]
Requests to the current API are rate limited **per application**, across all of its endpoints and tenants. The current default is **40 requests per second** sustained, with a **burst of 80**. When the limit is exceeded, the gateway responds with HTTP `429` and no `Retry-After` header, so the client owns the backoff schedule.
The limiter runs locally on each gateway instance behind the load balancer. Connections that land on different instances are counted separately, so the effective allowance for a client that spreads requests over many connections is somewhat higher than the default. Do not design for more than the default.
Rate limits on the Errors page: response shape and backoff guidance
## Endpoints per Account [#endpoints-per-account]
Endpoints created through the current API (`PUT /endpoints/{externalId}`) are not subject to a fixed quota today. The legacy onboarding flow limits an account to 300 endpoints. Contact agrirouter support if you plan to register an unusually large number of endpoints per account, for example thousands of virtual communication units.
## Feed Storage [#feed-storage]
Messages stay in an endpoint's feed until the receiving application confirms them, for at most **28 days** after arrival:
* After **21 days** without confirmation, the account owner is notified by email that messages are about to expire.
* After **28 days**, unconfirmed messages are deleted and the account owner is notified again. The data is no longer available to the receiving application.
An integration that can be offline for longer than the retention period loses data. Confirm messages as soon as they are processed.
## Payload Links [#payload-links]
`payload_uri` links on `MESSAGE_RECEIVED` and `FILE_RECEIVED` events are pre-signed and valid for **15 minutes**. After that, reconnecting to `GET /events` replays the unconfirmed event with a fresh link.
## Confirmations per Request [#confirmations-per-request]
`POST /confirmations` has no fixed maximum batch size. Each confirmation is processed individually, but the whole request is rejected with `403` if any of the endpoint IDs does not belong to your application, and a `5xx` in the middle of a batch leaves the earlier confirmations applied. Keep batches at a few hundred entries so partial results stay easy to reason about.
The operation is idempotent, so trying to confirm messages that have been confirmed already does not break the process.
Partners coming from the legacy API should also review Migrating from Legacy for constraints that no longer apply, in particular client-side chunking and custom Base64 conventions.
# Migrating from Legacy (/en/docs/appendix/migrating-from-legacy)
Map legacy agrirouter terms and concepts to their equivalents in the current API
If you are coming from an earlier integration or older documentation, you will run into vocabulary that no longer appears in the current API. This page lists the legacy terms most likely to cause confusion and points each one at its current counterpart.
This page currently covers terminology only. Deeper guidance on migrating existing integrations to the current API will be added later.
If you are migrating an existing integration from the legacy API to the current API, please write to directly. Some manual changes on our side in the backend are required for the migration to work, so it cannot be completed by the partner alone.
## Renamed concepts [#renamed-concepts]
The concept still exists under a different name.
| Legacy term | Now called | Notes |
| ------------------ | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Onboarding | Endpoint Creation | The flow that registers a new endpoint for an application. See [Endpoints](/concepts/endpoints). |
| Reconfiguration | Endpoint Update | Updating an existing endpoint, for example changing its capabilities or subscriptions. |
| Routing rule(s) | Route(s) | See [Routing](/concepts/messaging#routing) on the Messaging concept page. |
| Data types | [Message types](/message-types) | The technical message-type identifiers (for example, `iso:11783:-10:taskdata:zip`) that classify payloads and drive capability declarations. |
| Farming Software | [`cloud_software`](/concepts/ecosystem#cloud_software) | `farming_software` is still accepted for backward compatibility, but new integrations should register as `cloud_software`. |
| Telemetry Platform | [`virtual_communication_unit`](/concepts/ecosystem#virtual_communication_unit) per machine | The telemetry-platform framing, in which a cloud platform acted as a concentrator for a fleet of machines, is gone. Each machine is now registered directly as its own `virtual_communication_unit` endpoint under the tenant. |
## CU and VCU: not the same thing [#cu-and-vcu-not-the-same-thing]
The abbreviations look similar and both relate to machines, but they refer to different integrations.
| Term | Surface | What it is | Who creates it |
| ------------------------------------------------------------------ | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **CU** (Communication Unit, `communication_unit`) | [Legacy API](/api/legacy) only | A physical hardware device on a machine (terminal, telemetry box, ISOBUS gateway) that connects to agrirouter directly. | Hardware manufacturers, via the legacy onboarding flow. See the [Communication Unit guide](/integration/communication-unit). |
| **VCU** (Virtual Communication Unit, `virtual_communication_unit`) | Current API | A regular endpoint that represents a single physical machine in an end user's account. Not hardware: a cloud application that manages the machine creates and operates the VCU on the user's behalf. | Cloud applications, via `PUT /endpoints/{externalId}` with `endpoint_type: "virtual_communication_unit"`. See the [Virtual Communication Units guide](/integration/virtual-communication-units). |
CUs are not an endpoint type in the current API and cannot be created through it. They remain active in the agrirouter ecosystem on the legacy surface, so a current-API integration may still receive messages from a `communication_unit` sender in its feed.
## Removed, replaced by a current primitive [#removed-replaced-by-a-current-primitive]
The old idea is no longer exposed as such, but the same integration need is covered by a different mechanism in the current API.
| Legacy term | Now handled by |
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Inbox / Outbox | Endpoints exchange messages through [`POST /messages`](/api/sendMessages) for sending and the event stream on [`GET /events`](/api/receiveEvents) for receiving. |
| Push notifications | Server-Sent Events on [`GET /events`](/api/receiveEvents). |
| Chunked messages (1 MB client-side chunking) | Transparent server-side chunking. The payload limit is **256 MB** and chunking is handled by agrirouter, so applications no longer split or assemble messages themselves. |
## Removed, no equivalent [#removed-no-equivalent]
These concepts were specific to the legacy integration model and have no place in the current API.
| Legacy term | Notes |
| ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
| Middleware | Not part of the current integration model. |
| Onion Principle | The current API is built on standard HTTP primitives and no longer uses the layered "onion" framing. |
| ARTS | The ARTS integration is not supported. |
| Certificates (TLS client certificates, mTLS) | Authentication is based on OAuth 2.0 client credentials. See [Authorization and Security](/concepts/authorization-and-security). |
| Router Devices | Legacy-only concept; the current API does not use Router Devices. |
| Base64 line-break rules, 1-based numbering conventions | These legacy encoding conventions do not apply. |
## Next steps [#next-steps]
# URLs (/en/docs/appendix/urls)
URL reference for agrirouter environments and services
All relevant URLs for working with agrirouter across the Production and QA environments. URLs that apply only to the legacy API are grouped at the bottom for reference.
Partner integrations use the **Production** URLs. Only use the QA URLs if DKE has explicitly directed you to the QA environment. See [Environments](/integration/environments).
Mixing Production and QA URLs will cause authentication and communication failures.
## Environment URLs [#environment-urls]
| Service | URL |
| ------------------------- | ------------------------------------------------------------------------ |
| agrirouter UI | |
| Authorization Consent URL | |
| OAuth Token URL | |
| API Base URL | |
| Service | URL |
| ------------------------- | --------------------------------------------------------------------------- |
| agrirouter UI | |
| Authorization Consent URL | |
| OAuth Token URL | |
| API Base URL | |
## Developer Contracts [#developer-contracts]
| Contract | URL |
| -------------------------- | ------------------------------------------------------------------------------------------ |
| OpenAPI JSON | |
| OpenAPI YAML | |
| Credentials Package Schema | |
## Payload downloads [#payload-downloads]
`payload_uri` values on [`MESSAGE_RECEIVED`](/api/events/message-received) and [`FILE_RECEIVED`](/api/events/file-received) events are pre-signed, path-style object storage URLs. They are valid for 15 minutes and must be fetched **without** an `Authorization` header. If outbound traffic from your integration is restricted, allow the following host. The bucket name is the first path segment of the URL.
| Environment | Host | Buckets |
| ----------- | --------------------------------------------------------------------- | ------------------------------------------------------------------- |
| Production | | `prod-agrirouter-message-payloads`, `prod-agrirouter-file-payloads` |
| QA | | `qa-agrirouter-message-payloads`, `qa-agrirouter-file-payloads` |
## Other Services [#other-services]
| Service | URL |
| ---------------------- | --------------------------------------------------- |
| agrirouter Status Page | |
| IO-Tool | |
| Solution Finder | |
| DKE Data GitHub | |
| Contact | [Contact the agrirouter team](/appendix/contact) |
The status page covers the Production environment only. Check it for real-time service availability and incident reports.
## Legacy API URLs [#legacy-api-urls]
The URLs below are **only for the legacy API**. They do not apply to current API integrations and are kept here as a reference for partners maintaining legacy integrations.
| Service | URL |
| ----------------- | --------------------------------------------------------------------------------------------------- |
| Onboarding URL | |
| Re-onboarding URL | |
| Revoking URL | |
| MQTT Broker | |
| Service | URL |
| ----------------- | ------------------------------------------------------------------------------------------------------ |
| Onboarding URL | |
| Re-onboarding URL | |
| Revoking URL | |
| MQTT Broker | |
# agrirouter 2.0 (/en/docs/changelog/agrirouter-2-0)
Complete changelog for the agrirouter 2.0 migration, covering new features, removed features, and migration notes
The agrirouter 2.0 release in September 2024 was a complete rebuild of the platform. This page covers the changes that matter for integration developers: removed features, new capabilities, and migration notes.
## New User Interface [#new-user-interface]
Changes in the 2.0 UI:
* **Interactive routings view**: visual management of message routes between endpoints
* **Clear differentiation** between onFarm-Software and onField-Devices in the UI
## Removed Features [#removed-features]
The following features were removed in agrirouter 2.0:
Account pairing allowed linking two agrirouter accounts. It was removed because it was rarely used and caused confusion.
EFDI no longer creates separate machine endpoints. Device descriptions are still required, but the machine endpoint concept is gone. Endpoints now represent the connected application or CU, not individual machines.
The groups concept for organizing endpoints has been replaced by managed routes, which handle routing configuration more flexibly and automatically.
DDI-based filtering for telemetry data has been replaced by simple telemetry routing. Instead of filtering on individual DDIs, telemetry is routed at the message type level.
Technical message types (TMTs) are no longer bundled into higher-level information types. Routes now operate directly on technical message types.
The MarketPlace has been replaced by the Solution-Finder, which is a more focused way to discover agrirouter-compatible software and services.
The metrics export functionality has been removed.
## New and Changed Features [#new-and-changed-features]
### VCU Updates [#vcu-updates]
Virtual CUs (VCUs) can now be updated through further `PUT /endpoints/{externalId}` calls. You can rename a VCU or change its configuration without creating a new endpoint.
### External ID Uniqueness [#external-id-uniqueness]
External IDs are now scoped per **tenant**, not globally. The same external ID can be reused across different tenants, but within a single tenant an external ID cannot be used twice, even by different applications. See [External IDs](/concepts/endpoints#external-ids) for the details.
### Gateway Switching (MQTT to HTTP) [#gateway-switching-mqtt-to-http]
You can now switch an endpoint between MQTT and HTTP gateways, but data loss is possible during the switch. Messages in transit may be lost. Plan the switch during a maintenance window and make sure no critical messages are in flight.
Endpoints can switch their communication gateway between MQTT and HTTP without being re-created.
### Multiple Redirect URLs [#multiple-redirect-urls]
Applications can configure multiple redirect URLs for the authorization flow, which is handy for multi-environment setups (separate URLs for development, staging, and production).
### Clarified Error Codes [#clarified-error-codes]
Error codes `VAL_000004` and `VAL_000005` have been clarified on the legacy proto response:
* **VAL\_000004**: all routes failed, no recipients received the message
* **VAL\_000005**: some routes failed, partial delivery
Previously, the distinction between these two codes was ambiguous. On the current API, both conditions surface as HTTP `400` with a plain `{ "message": "..." }` body. The numeric code is not returned on the wire, so branch on the HTTP status instead. See the [Errors](/api/errors) reference for full details.
### Simplified Revoke Response [#simplified-revoke-response]
The revoke response now returns an HTTP status code with an empty body instead of a protobuf-encoded response.
### Fast Route Updates [#fast-route-updates]
Route changes now take effect instantly. In agrirouter 1.0, route updates could take 2.5 to 5 minutes to propagate.
### Managed Routes on Endpoint Creation [#managed-routes-on-endpoint-creation]
Managed routes are now created automatically when a new endpoint is created, so endpoints can communicate right away without manual route setup. You can turn this off if you prefer configuring routes by hand.
### Router Device Names [#router-device-names]
Router devices can now have human-readable names, which makes them easier to spot in the UI and in routing configurations.
### Faster Re-creation of Revoked Endpoints [#faster-re-creation-of-revoked-endpoints]
Re-creating a previously revoked endpoint with the same external ID now has little or no waiting time. In agrirouter 1.0, there was a delay before a revoked endpoint's external ID could be reused.
## Next steps [#next-steps]
# Changelog (/en/docs/changelog)
Platform changes, updates, and migration notes for agrirouter
This section tracks major platform changes, feature updates, and migration notes. Review these entries when upgrading your integration or planning for new features.
## Releases [#releases]
# Business, Legal & Marketing (/en/docs/getting-started/business-and-legal)
When you need a contract with the agrirouter operator, which participation option fits your company, what it costs, and how agrirouter helps you promote your solution
Becoming an agrirouter integration partner runs on three parallel streams of work: **Business and Legal**, **Development**, and **Marketing**. This page covers the business and legal stream and the marketing stream. For the development stream, see [Setup Application](/getting-started/setup-application).
## Testing is free of charge [#testing-is-free-of-charge]
You can request a developer account, build your integration, and test it end to end without any contract and without any cost. That includes testing with real customers, since you can invite specific users as [testers](/integration/endpoint-management#inviting-testers) even before your integration is publicly approved.
A contract is only required when you **go live**, meaning when you offer your agrirouter-connected solution on the market to your customers. The signed agreement is not a prerequisite for the [implementation review](/integration/implementation-review) itself, but your application is only set live after the review once a contract is in place. Start this stream in parallel with development so it does not hold up your launch.
## Our business model [#our-business-model]
DKE-Data operates on a non-profit basis. All financial contributions are only as high as necessary to cover the continued operation, maintenance, and further development of agrirouter and its surrounding solutions.
## Which option fits your company [#which-option-fits-your-company]
The deciding number is the yearly **agricultural turnover** of your company, or of the whole group if your company belongs to a group of companies.
| Yearly agricultural turnover | Participation option |
| ---------------------------- | --------------------------------------------------------------- |
| Below 60 million EUR | [Member of DKE-Data agrirouter e.V.](#association-member) |
| Above 60 million EUR | [Business Partner of DKE-Data GmbH & Co. KG](#business-partner) |
Independent of turnover, every company may alternatively become a **shareholder** of DKE-Data GmbH & Co. KG and take direct responsibility for the platform. If that is interesting for you, [get in touch](#how-to-get-started).
## Association member [#association-member]
Companies with an agricultural turnover below 60 million EUR per year join the ecosystem by becoming a member of the association **DKE-Data agrirouter e.V.**
Membership grants production access to agrirouter and lets you take part in the regular meetings of the association, where current proceedings are presented and the feedback and requests of the members are collected and discussed.
The yearly membership fee is:
| Agricultural turnover | Yearly contribution |
| --------------------- | ------------------- |
| 35 to 60 million EUR | 3,000 EUR |
| 10 to 35 million EUR | 2,000 EUR |
| Below 10 million EUR | 1,000 EUR |
To join, fill out the [membership form on the DKE-Data website](https://dke-data.com/association).
## Business Partner [#business-partner]
Companies, or groups of companies, with an agricultural turnover above 60 million EUR per year join as a **Business Partner of DKE-Data GmbH & Co. KG**. The **first year is free of charge**, in return the commitment is **two years**.
The contributions below are derived from the agricultural turnover of the **whole company or group**, so for any given company they are the **maximum** fee. Participating with a single subdivision rather than the entire group lowers the relevant turnover, and with it the yearly contribution, considerably. Talk to us if that sounds like a fit for your company.
| Agricultural turnover | Yearly contribution |
| -------------------------- | ------------------- |
| 1,220 to 1,988 million EUR | 50,000 EUR |
| 745 to 1,220 million EUR | 36,000 EUR |
| 453 to 745 million EUR | 25,000 EUR |
| 273 to 453 million EUR | 18,000 EUR |
| 162 to 273 million EUR | 13,500 EUR |
| 94 to 162 million EUR | 9,000 EUR |
| 60 to 94 million EUR | 7,500 EUR |
| 35 to 60 million EUR | 6,500 EUR |
| 10 to 35 million EUR | 4,500 EUR |
| Below 10 million EUR | 2,500 EUR |
For an agricultural turnover above 1,988 million EUR, please get in touch and we will walk you through the applicable contribution.
The list also covers turnovers below 60 million EUR, because companies of that size may choose to become a Business Partner instead of an association member.
From the third year onwards the contribution is recalculated. We are happy to walk you through the details.
## Contract documents [#contract-documents]
* **Association members** join by signing the membership form of DKE-Data agrirouter e.V. Legal documents can be found as part of the [sign up process](https://dke-data.com/association).
* **Business Partners** and **shareholders** sign the standard Business Partner Agreement or Shareholder Partnership Agreement for their option. The text is the same for every partner and is not negotiated individually, for antitrust reasons. Copies of the documents are available on request before you decide, see below.
## How to get started [#how-to-get-started]
To discuss the options or request a copy of a contract, contact Dr. Johannes Sonnen through the [DKE-Data contacts page](https://dke-data.com/#team).
These contacts are for the business side only. For development or end-user support, see [Contact](/appendix/contact).
## Marketing [#marketing]
Marketing is the third partnership stream. Like business and legal, it can run in parallel with development from the start — talk to us early so everything is ready by the time you launch. DKE-Data helps you reach the agrirouter community, and there are a few things we need from you to make that possible.
### How we promote your solution [#how-we-promote-your-solution]
* **Solution Finder and Solution Guides.** Your solution is listed in the [Solution Finder](https://www.agrirouter.com/solutions), the public directory where farmers, contractors, and agricultural service providers look for approved agrirouter-compatible solutions. The [Solution Guides](https://manual.agrirouter.com/en/solution-guides.html) show users step by step how to connect and use the solutions in the network to ensure a successful first data exchange. Listing is the final publication step after your application version is approved; see [Understand publication](/getting-started/setup-application#understand-publication).
* **Promotion on our channels.** Once your application is on the market including agrirouter, we promote you as a new partner across our own channels.
* **A joint campaign.** We are happy to run a campaign together: you announce that your solution is finally agrirouter ready, and we announce that the agrirouter network has grown by another partner. A short testimonial from your side — for example on how quickly and easily the integration went, or on the support you received — makes the story stronger for both of us.
* **The marketing material box.** We provide a [marketing material box](https://go.dke-data.com/marketing_material) with logos, badges, and ready-to-use assets so you can show your agrirouter integration in your own communication.
### What we need from you [#what-we-need-from-you]
* **Your logo.** We need your company or product logo for the Solution Finder listing and for any joint promotion.
* **A marketing contact.** Name a person we can reach for marketing coordination and general information.
* **A Product Steering Committee contact.** Name a person to represent your company in the Product Steering Committee, where partners help steer the direction of agrirouter.
To set any of this up — or to send us your logo and the two contacts above — write to .
Continue with the development stream: create your developer account, register your application, and prepare it for the implementation review.
# Getting Started (/en/docs/getting-started)
Partner setup through sending your first message
agrirouter is a data exchange platform for agriculture. It connects machines, farm management software, and agricultural services, regardless of manufacturer, so they can exchange data with each other.
This section walks you through becoming a partner and sending your first message through the platform. The walk-through covers the `cloud_software` and `virtual_communication_unit` integration paths, both of which use the current REST API. If you are building firmware for a physical Communication Unit, jump straight to the [Communication Unit guide](/integration/communication-unit), which uses the legacy API.
The current API is available as a hosted [OpenAPI specification](/api#openapi-specification). Use the YAML or JSON document with OpenAPI generators, API clients, and contract validation tools, or keep it open alongside the tutorials while you implement the examples below.
Some steps along the way need involvement from the agrirouter team. Plan for them up front so they do not block your build:
* **Agreement (business and legal).** Testing is free of charge and needs no contract, but your application is only set live after the [implementation review](/integration/implementation-review) once a signed agreement with the agrirouter operator is in place. Start this track in parallel with development rather than after it. See [Business, Legal & Marketing](/getting-started/business-and-legal).
* **Developer-account promotion.** Sign up for a regular end-user account self-service, then ask the agrirouter team to promote it to a developer account. Promotion is reviewed and approved manually. See [Create a developer account](/getting-started/setup-application#create-a-developer-account).
* **Application version approval.** You create each application version yourself in the developer UI, but submitting it sends it to the agrirouter team for review; the version becomes usable once they mark it **Approved**. See [Submit an application version for approval](/getting-started/setup-application#submit-an-application-version-for-approval).
* **Implementation review.** Production traffic is gated on a review with the agrirouter team once your implementation is working. See [Implementation Review](/integration/implementation-review).
OAuth client credentials are self-service after your application exists. Create them in the Developer Portal under your application's **Auth clients** area. See [Obtain credentials](/getting-started/setup-application#obtain-credentials).
Write to early on the reviewed items so they are not in the critical path.
## What you will learn [#what-you-will-learn]
## Before you begin [#before-you-begin]
If you are new to agrirouter, read the pages in order. Each one builds on the previous:
1. **Business, Legal & Marketing** explains when a contract is required, which participation options exist and what they cost, and how agrirouter helps you promote your solution. Testing is free of charge.
2. **Setup Application** covers the technical steps to become an agrirouter integration partner.
3. **Environments** explains why you work in Production throughout development and what the QA environment is reserved for.
4. **Your First Endpoint** walks through creating an endpoint that can send and receive data.
5. **Send Your First Message** shows you how to send data through agrirouter.
6. **Receive Your First Message** closes the loop by showing you how to receive and verify messages.
Already a partner with a developer account? Skip ahead to [Environments](/integration/environments) or [Your First Endpoint](/getting-started/your-first-endpoint).
For agrirouter's architecture and data model, see the concepts section.
Concepts
# Receive Your First Message (/en/docs/getting-started/receive-your-first-message)
Step-by-step guide to receiving messages, downloading payloads, and verifying delivery through agrirouter
After [sending your first message](/getting-started/send-your-first-message), the next step is receiving it on the other side. This page walks through listening for events, downloading the payload, and verifying delivery.
## Prerequisites [#prerequisites]
Before you begin, make sure you have:
* An **active endpoint** with capabilities configured (see [Your First Endpoint](/getting-started/your-first-endpoint))
* A valid **access token** from the client credentials flow
* A **message already sent** to your endpoint (see [Send Your First Message](/getting-started/send-your-first-message))
* Your **environment URLs** (see [Environment URLs](/appendix/urls))
* Optional: the hosted [OpenAPI specification](/api#openapi-specification) for generated clients and API tooling.
## Receiving a message [#receiving-a-message]
### Listen for events via SSE [#listen-for-events-via-sse]
Events about message activity are delivered through **Server-Sent Events (SSE)**. To receive events, open a long-lived connection:
In generated clients, this is the `receiveEvents` operation from the [OpenAPI specification](/api#openapi-specification). Make sure the generated client you choose supports streaming `text/event-stream` responses, or handle this operation with your platform's SSE client.
Each event tells you that a message has arrived in your endpoint's **feed**, the per-endpoint queue where agrirouter stores received messages until you confirm them (see [Feed Management](/concepts/messaging#feed-management)). The `payload_uri` field contains the URL to download the payload.
For the full field list on every event type, see [SSE Events](/integration/events).
### Download the payload [#download-the-payload]
Once you receive a message event, download the actual payload by making a `GET` request to the `payload_uri` from the event:
```http
GET https://s3.eu-central-1.amazonaws.com/prod-agrirouter-message-payloads/inbox/2026/03/20/10/30/e4f5a6b7-c8d9-0123-4567-89abcdef0123?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=900&X-Amz-Signature=...
```
The response contains the message payload as a binary stream (`application/octet-stream`) in its original format. For the example above, this would be a ZIP file containing ISO 11783 TaskData.
`payload_uri` links expire after at most **15 minutes**. Download the payload as soon as you receive the event. If the link expires, the only recovery path is to reconnect to the SSE stream so agrirouter replays the unconfirmed event with a fresh URL.
The `payload_uri` is pre-signed, so the download request does not need an `Authorization` header. Do not add one either: the object store rejects requests that carry both a signed URL and an `Authorization` header. Payloads are served from `s3.eu-central-1.amazonaws.com`; see [Payload downloads](/appendix/urls#payload-downloads) if outbound traffic from your integration is restricted by an allowlist.
For small payloads, the event may include the data directly in the `payload` field (base64-encoded) instead of `payload_uri`. Only one of the two will be present. When `payload` is included inline, no separate download is needed.
### Verify receipt [#verify-receipt]
Confirm that the complete flow worked:
1. **Sender side**: the `POST /messages` request returned `200`, confirming agrirouter accepted and routed the message. This is the only feedback the sender gets; there is no delivery confirmation event.
2. **Receiver side**: you received a `MESSAGE_RECEIVED` event with the correct metadata.
3. **Payload**: the downloaded content matches what was sent.
If you are using the IO-Tool as the receiving endpoint, you can also verify receipt on the IO-Tool side by checking its received messages.
{/* TODO: SCREENSHOT: IO-Tool received messages view
Shows: The IO-Tool interface displaying a received message with its metadata (message type, sender, timestamp) and the option to download the payload
*/}
## What you have accomplished [#what-you-have-accomplished]
You have completed the full agrirouter message lifecycle:
1. Sent a message from your endpoint
2. Received events via SSE
3. Downloaded the message payload
4. Verified successful delivery
This is the pattern every data exchange through agrirouter follows, whether the payload is task data, machine descriptions, images, or telemetry.
## Handling intermittent connections [#handling-intermittent-connections]
Server-Sent Events are the only way to receive messages and events from agrirouter. There is no polling API, no webhook callback, and no push-notification fallback, so your application has to consume the SSE stream, either by keeping it open continuously or by reconnecting periodically.
An always-open stream is simplest, but many integrations cannot hold a long-lived connection: batch jobs, mobile clients, and processes that cycle through restarts. For those cases, the gateway performs a **feed stream replay** on reconnect. When you open a fresh SSE connection to `GET /events`, the gateway first replays every unconfirmed event that arrived on your endpoints while you were disconnected, then continues with live events. You do not need to track a cursor or query a separate endpoint; reconnecting is enough to catch up.
Confirm each event after your application has processed it by calling [`POST /confirmations`](/api/confirmMessages). Confirmed events drop out of the replay, so the next reconnect only replays genuinely missed events instead of everything since the endpoint was created.
## Next steps [#next-steps]
For advanced messaging patterns and error handling, see the integration guide.
Sending and Receiving Messages
For a conceptual overview of how messaging, routing, and subscriptions work together, see the messaging concepts page.
Messaging Concepts
## Choose your integration path [#choose-your-integration-path]
Now that you have completed the Getting Started pages, continue with the integration guide for your endpoint type:
# Send Your First Message (/en/docs/getting-started/send-your-first-message)
Step-by-step guide to sending your first message through agrirouter
With an active endpoint in place, you can send data through agrirouter. This page covers composing and sending a message.
## Prerequisites [#prerequisites]
Before you begin, make sure you have:
* An **active endpoint** with capabilities configured (see [Your First Endpoint](/getting-started/your-first-endpoint))
* A valid **access token** from the client credentials flow (see [Your First Endpoint](/getting-started/your-first-endpoint#obtain-an-access-token))
* The **endpoint ID** (agrirouter-generated UUID) and **tenant ID** from the endpoint creation response
* A **second endpoint** to receive the message. The [IO-Tool](/tools/io-tool) is a good fit.
* Your **environment URLs** (see [Environment URLs](/appendix/urls))
* Optional: the hosted [OpenAPI specification](/api#openapi-specification) for generated clients and API tooling.
Use the **[IO-Tool](/tools/io-tool)** as your test receiver. It acts as a receiving endpoint, so you can verify delivery without standing up a second fully integrated application.
## Sending a message [#sending-a-message]
### Compose and send [#compose-and-send]
The API uses **HTTP headers** for message metadata and a **binary body** for the payload. Send a `POST` request to the messages endpoint:
In generated clients, this is the `sendMessages` operation from the [OpenAPI specification](/api#openapi-specification).
The message headers consist of:
* **x-agrirouter-endpoint-id**: the agrirouter-generated UUID of your sending endpoint (from the [endpoint creation response](/getting-started/your-first-endpoint#create-the-endpoint))
* **x-agrirouter-tenant-id**: the tenant UUID (also from the endpoint creation response)
* **x-agrirouter-message-type**: the [message type](/message-types) being sent, which must match a capability declared by your endpoint
* **x-agrirouter-is-publish**: `false` for direct addressing, `true` for publishing
* **x-agrirouter-direct-recipients**: comma-separated agrirouter endpoint UUIDs of the recipients. Typically set for direct addressing, usually omitted when publishing. See [Choose an addressing mode](#choose-an-addressing-mode).
* **x-agrirouter-sent-timestamp**: client-side timestamp in ISO 8601 format. For records collected from in-field devices, this is the timestamp when the record was captured on the machine, not the time the message reaches agrirouter. Devices that buffer data for later transmission should preserve the original capture timestamp.
* **x-agrirouter-teamset-context-id** *(optional)*: identifies the set of physically connected machines (typically a tractor and its connected implements) the payload belongs to. Required in practice for EFDI device descriptions and time logs, optional and usually omitted for everything else. The same identifier is also called the *TeamSet ID* or *context ID* in EFDI documentation. See [TeamSet Context ID](/message-types/efdi#teamset-context-id) for the full reference.
### Choose an addressing mode [#choose-an-addressing-mode]
There are two ways to address messages:
* **Direct addressing**: set `x-agrirouter-is-publish: false` and provide `x-agrirouter-direct-recipients` with one or more endpoint UUIDs. The message is delivered only to the named endpoints.
* **Publishing**: set `x-agrirouter-is-publish: true` and usually omit `x-agrirouter-direct-recipients`. The message is delivered to every endpoint that has subscribed to the message type and is permitted to receive it by the account's routes.
Pick the mode based on who triggers the send:
* **Interactive processes**, where a user initiates the transfer to a specific target (for example, sending an application map from an FMIS to a specific machine), are a good fit for **direct addressing**.
* **Automated processes** with an open-ended set of receivers (for example, telemetry data streamed from machines to any subscribed platform), are a good fit for **publishing**.
A publishing send can also carry a list of recipients in `x-agrirouter-direct-recipients`, in which case those recipients receive the message in addition to any subscribers. This is uncommon and is not a separate addressing mode, just a quirk of the two headers interacting: the conceptual model is still direct *or* publish.
Message delivery requires three conditions to all be true:
1. The sender has a **capability** to send the message type
2. The recipient has a **capability** to receive the message type
3. A **route** exists from sender to recipient for that message type
Routes are configured by the **account owner** in the agrirouter UI. Your application cannot create them. If messages are not being delivered, check the routing configuration before debugging your code.
### Understand the response [#understand-the-response]
A `200` response means agrirouter has accepted the message and validated the addressing and routing. This is the **only feedback the sender gets**: there is no asynchronous delivery confirmation or delivery status event.
If the request fails, common causes include:
* **400**: invalid request (malformed headers, missing required fields, unsupported message type), or no route exists from your endpoint to a specified direct recipient
* **403**: the endpoint ID in the request header does not belong to a tenant your access token is authorized for
* **413**: payload exceeds the [256 MB size limit](/concepts/messaging#chunking)
The response body is always a JSON object with a single `message` field. See the [Errors](/api/errors) reference for the complete status code list.
There is no client-supplied message ID and no server-side deduplication on `POST /messages`. Every accepted request creates a new message in the recipients' feeds, so a request that already returned `200` must not be sent again. Retry only when no response was received (connection error, timeout) or after `429` and `5xx`. See [Retries](/integration/sending-and-receiving#retries) for the details.
agrirouter does not notify the sender whether recipients have actually received or confirmed the message. To verify delivery during development, check the receiving side: for example, use the [IO-Tool](/tools/io-tool), or listen for `MESSAGE_RECEIVED` events on the recipient endpoint.
## Next steps [#next-steps]
Now that you have sent a message, receive one on the other side.
Receive Your First Message
# Setup Application (/en/docs/getting-started/setup-application)
Become an agrirouter partner. Create a developer account, register your application, prepare software versions, and submit for review.
Becoming an agrirouter integration partner runs on three parallel streams of work: **Business and Legal**, **Development**, and **Marketing**. This page covers the development stream, from creating your developer account to preparing the application workspace for review and publication. For the business, legal, and marketing streams, see [Business, Legal & Marketing](/getting-started/business-and-legal).
## How the application workspace is organized [#how-the-application-workspace-is-organized]
The Developer Portal application workspace separates information by responsibility:
| Area | Purpose |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Application Rail** | Lists the applications in the workspace. Search and filters help find an application by name, company, brand, application type, tenant, visibility, or software-version status. |
| **Application profile** | Holds the user-facing application name, brand, logo, application type, and current visibility. |
| **Setup guide** | Shows the next unfinished setup step for the selected application and version. It is a checklist, not a separate configuration store. |
| **Software versions** | Holds the reviewable versions of the application. Each version has its own release description, capability declaration, review status, and version ID. |
| **Technical details** | Holds support and Deep URL values, stable identifiers, OAuth clients for every application, and legacy auth fields for legacy application types. |
The workspace is intentionally revealed in this order: create the application profile first, then prepare a software version, then add credentials, then submit the version for review. Publication is shown as **Visibility** on the application because public availability belongs to the application as a whole.
## Integration steps [#integration-steps]
### Create a developer account [#create-a-developer-account]
A developer account is built on top of a regular agrirouter end-user account. It includes everything an end-user account does, plus tools for managing applications, software versions, and developer-specific settings.
To get one:
1. Sign up for a regular end-user account on the agrirouter platform if you do not already have one.
2. Request that the account be promoted to a developer account from within your account's settings and then write to . Promotion is reviewed and approved manually.
Two practical conventions:
* Use a **generic company email address** such as `dev@yourcompany.com` rather than a personal one. This keeps the account usable when team members change.
* Plan on **one developer account per app provider**. All developers in your organization share the same account.
### Register your application profile [#register-your-application-profile]
Once your developer account is active, open **Developer** > **Applications** and use **Create application**. The application profile is the stable product record that users and reviewers see. Complete these fields first:
* **Application name**, the name displayed to end users.
* **Brand**, the commercial product or company label shown with the application.
* **Application type**, the portal category for the application record. New current-API integrations should use **Default application type**. The other values are legacy compatibility types; see [Application Types in the Developer Portal](/concepts/ecosystem#application-types-in-the-developer-portal).
* **Support URL**, where users can get help with your integration.
* **Deep URL**, the HTTPS URL where users are sent when they start connecting from agrirouter. See [Discovery via Deep URL](/concepts/authorization-and-security#discovery-via-deep-url).
* **Logo**, the visual identifier for your application in the agrirouter ecosystem.
The Developer Portal saves the application profile automatically. There is no separate save button: while required fields are still missing, the status indicator shows that autosave is pending. After the required fields are valid, autosave creates the application, assigns its Application ID, and opens the application workspace. For current application types, the required fields are application name, brand, application type, Support URL, and Deep URL.
The setup guide reads the saved application state. As soon as autosave creates the application and the required profile fields are saved, the guide moves on to the next unfinished setup item.
#### Support URL [#support-url]
The Support URL is the public help address for your application. agrirouter stores it on the application record and can show it to users when they need help from the application provider, for example when a connection cannot be managed or deleted directly from the agrirouter UI.
When registering a new application, the Developer Portal requires a valid URL here. Use a stable page that explains how users can contact your support team, find integration-specific help, or continue connection management in your own system.
Prefer an HTTPS page that is reachable without signing in to agrirouter. If the page requires a vendor account, include enough public context that users know they reached the right support destination.
After the application exists, the profile can still be maintained in the workspace:
* Edit the application name, logo, brand, support URL, and Deep URL directly from the profile and **Technical details** areas.
* Application metadata and software-version drafts autosave independently.
* Delete an application only while all of its versions are still drafts. Once a version enters the review lifecycle, keep the application and create a new version for later changes.
* Manage OAuth clients from **Technical details**. OAuth clients belong to the application as a whole, not to a single software version. For legacy compatibility types, also complete **Legacy Auth** because that is the setup requirement until the integration is migrated.
### Work with software versions [#work-with-software-versions]
A software version is the reviewable unit of an application. It records what a specific release does, which message types it sends or receives, and where it is in the review lifecycle.
Use **Create version** in **Software versions** to start a draft. The portal suggests the next version number from the existing versions. A version draft begins empty so the number can be reserved first, then the release description and capabilities can be filled in continuously.
The version row shows:
* **Version number**, the release number assigned in the portal.
* **Lifecycle status**, such as **Draft**, **In review**, **Testing approved**, or **Approved**.
* **Latest**, shown on the highest version number in the list.
* **Actions**, including the version ID and draft deletion while the version is still editable.
Expand the version row to edit the release description and capabilities. Draft changes autosave after each edit. **Submit for review** becomes available after the version has a saved release description and at least one saved capability.
### Declare capabilities [#declare-capabilities]
Capabilities declare the message types the software version can **send** and **receive**. They should match the integration behavior that will be demonstrated during the implementation review.
Use **Add capability** to choose message types from the catalog. Capability rows are grouped by message-type family and can be toggled for send, receive, or both directions. Declare only what the version actually supports; the review uses this declaration as the scope for message exchange tests.
Use the [Message Types](/message-types) reference before adding capabilities. It lists the supported technical message type identifiers, and the category pages explain payload formats, common use cases, and important constraints such as deprecated GPS messages or EFDI TeamSet context.
For details about what reviewers check against the declared capabilities, see [Review Scope](/integration/implementation-review#review-scope).
### Submit an application version for approval [#submit-an-application-version-for-approval]
When the draft is complete and the autosave status is clear, click **Submit for review** on the version card. Submission locks that version for editing. The agrirouter team then moves the version through the review statuses:
| Status | Meaning |
| -------------------- | --------------------------------------------------------------------------------------------------- |
| **Draft** | The version can be edited by the developer. |
| **In review** | The version was submitted and editing is locked while the agrirouter team reviews it. |
| **Testing approved** | The version can be exercised with tester accounts before production approval. |
| **Approved** | The version passed the implementation review. |
| **Needs changes** | The submitted version cannot continue as-is. Prepare a new draft version for the corrected release. |
| **Blocked** | Review is stopped until the blocking issue is resolved with the agrirouter team. |
The **Application ID** is shown in **Technical details**. The **Version ID** is available from the software version's actions menu. You will need both when [creating an endpoint](/getting-started/your-first-endpoint).
### Obtain credentials [#obtain-credentials]
Default application type integrations authenticate against agrirouter using OAuth 2.0 client credentials, a `client_id` and `client_secret`.
These credentials are tied to the application, not to a single software version, and they are used for every customer that authorizes your application.
See [Authorization and Security](/concepts/authorization-and-security) for the full flow.
For legacy compatibility application types, the setup guide asks for **Legacy Auth** instead: at least one redirect URL and the public key used by the legacy authorization flow. The **Auth clients** area is still available on those applications so credentials can be prepared for a later migration, but creating an OAuth client is not part of the legacy setup checklist.
OAuth credentials are created self-service in the Developer Portal. Open your application, find **Technical details**, and use **Auth clients** to create one or more OAuth clients for the application.
### Create an OAuth client [#create-an-oauth-client]
An application can have multiple OAuth clients. Use separate clients for your own deployment stages, for example an internal test deployment and your live deployment, or for credential rotation where old and new deployments need to overlap briefly. Your deployment stages are your own concern and are unrelated to the agrirouter [environments](/integration/environments); they all connect to Production.
To create one, click **Create** in the **Auth clients** area, enter a recognizable name, and register the exact redirect URL your application will use during the authorization flow.
### Store the one-time client secret [#store-the-one-time-client-secret]
After the client is created, agrirouter shows the `client_id` and `client_secret`. The secret is shown only once. Copy it immediately into your secrets manager or deployment environment before closing the dialog.
Treat the `client_secret` like a password: never commit it to version control, never log it in plain text, and never expose it in client-side code. Store it in a secrets manager or an environment variable.
### Download a credentials package [#download-a-credentials-package]
For current G4 API integrations, the **Technical details** area can also download a JSON credentials package. The package is a convenience file, not an SDK. It contains the stable values for exactly one software version and one OAuth client:
* The application ID, application type, tenant ID, Deep URL, and support URL.
* The selected software version ID, version number, lifecycle status, and release description.
* The selected OAuth client name, `client_id`, `client_secret` or placeholder, and registered redirect URI.
* The authorization endpoint, token endpoint, supported scopes, and API base URL for the Developer Portal environment where the client was created, plus the public OpenAPI URLs and package schema URL.
The version selector defaults to the latest package-eligible version by version number. Package-eligible versions are **Testing approved** and **Approved** versions; draft and submitted versions are intentionally omitted.
Existing OAuth client secrets cannot be retrieved again. If you open the package dialog for an existing client without a one-time secret still available in the current session, the Developer Portal downloads the package with a `client_secret` placeholder that you replace yourself. To include the live one-time secret in the JSON, download the package immediately after creating the OAuth client or after rotating its secret.
Production and QA credentials are isolated. The Developer Portal automatically writes the matching Production or QA authorization, token, and API URLs into the package; do not replace them with URLs from the other environment.
The downloaded JSON file can contain a live `client_secret` when it is generated from the one-time secret dialog. Move live credentials directly into a secrets manager, restrict access to the file, and delete local copies after your deployment has been updated.
The package format is described by the JSON Schema at .
### Rotate or revoke OAuth clients [#rotate-or-revoke-oauth-clients]
Existing OAuth clients are listed under **Auth clients** with their name and `client_id`. The `client_secret` is not shown again after creation. Use **Rotate** when a new secret is needed, then update your deployments before retiring the old value. Use **Revoke client** only after every environment using that client has been migrated.
### Develop your integration [#develop-your-integration]
With your application registered and credentials in hand, you can start building the integration. Two notes on how we expect the integration to be built:
**There are no official SDKs for the agrirouter API.** Generate a client from the OpenAPI specification using a code generator for your language of choice, for example [openapi-generator](https://openapi-generator.tech/), [NSwag](https://github.com/RicoSuter/NSwag), or [Prism](https://stoplight.io/open-source/prism). The full specification is published on the [API Reference](/api), and you can render or browse it directly from there.
We plan to provide thin SDKs that wrap generated clients and/or AI agent instructions to build an SDK.
**Use the IO-Tool as your test receiver and sender.** The IO-Tool acts as a receiving and/or sending endpoint, so you can send and receive messages without running a second integrated application.
IO-Tool
### Request the implementation review [#request-the-implementation-review]
After the software version is submitted and the integration is ready to demonstrate, write to and request the implementation review. For the full checklist, see [Implementation Review](/integration/implementation-review).
Implementation Review
## Understand publication [#understand-publication]
Approval and publication are separate.
An **Approved** software version means the reviewed version passed the implementation review. **Visibility** controls whether the application itself is public. **Private** applications are not listed for general end users in the Solution Finder. **Public** applications are listed in the **Solution Finder**, the public directory on the agrirouter website where farmers, contractors, and agricultural service providers look for approved solutions that integrate with agrirouter.
Developers see **Visibility** as a read-only field in the application profile. The agrirouter team manages publication separately. Publishing requires at least one approved software version; the portal prevents publication before that condition is met.
Publication belongs to the application record. Software-version status only reflects review state, so use application visibility to determine whether the application is public.
Visit the [agrirouter Solution Finder](https://www.agrirouter.com/solutions) to browse currently listed solutions.
## Getting support [#getting-support]
If you need help at any stage of setting up your application, see the contact overview.
Contact
## Next steps [#next-steps]
For agrirouter's architecture, endpoints, and messaging model, see the concepts section.
Concepts
# Your First Endpoint (/en/docs/getting-started/your-first-endpoint)
Step-by-step guide to authenticating your application, obtaining user authorization, and creating your first agrirouter endpoint
An endpoint is your application's representation within agrirouter. It represents a single user account of your application connected to a specific agrirouter account.
Before you can send or receive any data, you need to authenticate your application, obtain user authorization, and create an endpoint.
This page covers the full setup, from your first access token to a working endpoint.
This page covers the authorization and endpoint-creation flow for `cloud_software` and `virtual_communication_unit` endpoints, which use OAuth2 client credentials for
application authentication and a browser-based consent flow for user authorization. If you are building a physical **Communication Unit (CU)** such as a terminal or
telemetry box, use the legacy API instead. See the [CU integration guide](/integration/communication-unit).
## Prerequisites [#prerequisites]
Before you begin, make sure you have:
* A **developer account** with an application version that is at least in **Approved for Testing** state (see [Setup Application](/getting-started/setup-application))
* Your **application credentials**: `client_id` and `client_secret` from a self-service OAuth client in the Developer Portal (see [Obtain credentials](/getting-started/setup-application#obtain-credentials))
* A registered **redirect URI** for your OAuth client
* The **environment URLs** for your target environment (see [Environment URLs](/appendix/urls))
* A tool for making HTTP requests (e.g., cURL, Postman, or your application code). You can also use the built-in API testing feature in these docs.
* Optional: the hosted [OpenAPI specification](/api#openapi-specification) if you want to generate a client or import the contract into an API tool.
## How the pieces fit together [#how-the-pieces-fit-together]
Creating an endpoint requires three independent steps, which map to the [authorization flow](/concepts/authorization-and-security):
1. **Authenticate your application**: obtain an access token using your client credentials. This can happen at any time (for example, at application startup).
2. **Get user authorization**: redirect the user to agrirouter's consent page so they grant your application permission to manage endpoints in their account. This is done *once* per user.
3. **Create the endpoint**: call the gateway API with your access token. The gateway checks that the user has authorized your application before processing the request.
## Setting up authentication and authorization [#setting-up-authentication-and-authorization]
### Obtain an access token [#obtain-an-access-token]
Your application authenticates itself using the **OAuth2 Client Credentials** flow. This is independent of any user interaction: you can request a token at any time, for example at application startup.
```bash
curl -X POST https://api-oauth.agrirouter.com/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&client_id={yourClientId}&client_secret={yourClientSecret}"
```
The URL above is for the **production environment**, which is the one partner integrations use. Only if DKE has directed you to the QA environment, use the corresponding QA URL instead. See [Environment URLs](/appendix/urls) for the complete list.
The response contains a signed JWT (JSON Web Token):
```json
{
"access_token": "eyJhbGciOiJFUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600
}
```
Only the value of the `access_token` field is the Bearer token. Do not include the full JSON object or the surrounding quotes when you set the `Authorization` header:
```http
Authorization: Bearer eyJhbGciOiJFUzI1NiIs...
```
Store this token and include it on all subsequent API requests. Request a new token before it expires.
You should only use the information in `expires_in` to schedule the next refresh and you *should not* try to parse the token itself. We do not guarantee the structure
or content of the token, you need to treat it as opaque (pass it as-is, without any transformation or introspection).
The access token only proves your application's identity. It does not grant permission to act on any user's behalf. That requires user authorization (next step).
### Set up the user consent redirect [#set-up-the-user-consent-redirect]
When a user wants to connect your application to their agrirouter account, redirect their browser to agrirouter's consent URL:
```text
https://app.agrirouter.com/api/authorize?client_id={yourClientId}&redirect_uri={yourRedirectUri}&scope=endpoints:manage&state={randomStateValue}
```
| Parameter | Required | Description |
| -------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `client_id` | Yes | Your OAuth client ID |
| `redirect_uri` | Yes | Must exactly match (string equality) the URI registered for your application. If it is missing or does not match, the consent page shows an error and the user cannot continue. |
| `scope` | No | Defaults to `endpoints:manage`, the permission to create and manage endpoints. |
| `state` | Recommended | A cryptographically random string echoed back on the redirect, and on the [`AUTHORIZATION_ADDED`](/api/events/authorization-added) event. Use it to prevent CSRF attacks, to correlate the response with the originating request, and to let your backend map the authorization to the customer that started the flow. |
The user sees a consent page where they can **Connect** or **Reject** your application. If `client_id` or `redirect_uri` is missing, malformed, or does not match the registered values, the consent page renders an error instead of the consent prompt — the user has no way to grant authorization in that case, so make sure both parameters are present and correct before redirecting.
### Handle the redirect back [#handle-the-redirect-back]
After the user makes a choice, the browser is redirected back to your `redirect_uri`:
**On approval:**
```text
https://yourapp.com/callback?state={randomStateValue}&tenant_id={userTenantId}
```
**On rejection:**
```text
https://yourapp.com/callback?error=access_denied&state={randomStateValue}
```
Your application should:
1. **Verify the `state` parameter** matches the value you originally sent (to prevent CSRF attacks)
2. **Check for the `error` parameter**. If present, the user denied the request.
3. **Store the `tenant_id` parameter** (approval only). This is the tenant ID of the end user account the user selected on the consent page. Use it as the `x-agrirouter-tenant-id` header on every subsequent API call you make on this user's behalf.
No authorization code or token is returned in the redirect. The authorization is recorded server-side by agrirouter. Your application only needs to know the user
approved, and the tenant ID of the account they selected. After that, your existing client credentials token is all you need to call the API.
## Creating your endpoint [#creating-your-endpoint]
With a valid access token and user authorization in place, you can now create an endpoint.
### Get your tenant ID [#get-your-tenant-id]
The **tenant ID** identifies the end user account your endpoint will belong to. Use it as the `x-agrirouter-tenant-id` request header on every call to `PUT /endpoints/{externalId}` and the other endpoint-management operations for this user.
You can pick up the tenant ID from either of two sources, depending on which side of your application is driving the next step:
* **From the redirect query parameter** — the `tenant_id` value on the authorization callback shown above. This is the natural choice when the frontend continues straight into endpoint creation, for example when the same browser session calls back to your backend with the parsed query.
* **From the [`AUTHORIZATION_ADDED`](/api/events/authorization-added) SSE event** — delivered on the [`GET /events`](/api/receiveEvents) stream when the user grants the authorization, carrying the same `tenant_id` together with the granted scope. This is the natural choice when a backend keeps a long-lived SSE connection and reacts to authorizations there, without taking a dependency on the redirect URL.
Both sources surface the same authorization. Pick the one that fits your architecture, or use both — for example, the frontend stores the tenant ID for the immediate next call and the backend independently picks it up from the event to update its own state.
To list every tenant your application is currently authorized for (for example, on application startup or after a crash), call [`GET /tenants`](/api/listAuthorizedTenants).
Your application's own **developer-account tenant ID** (used to sign into the developer portal) is a separate value issued with your OAuth credentials. That is not the tenant ID you pass on per-user API calls.
Treat the access token as opaque. Do not decode the JWT to look up a tenant ID or any other account information, since the token format is an implementation detail and may change without notice.
Tenant IDs
### Choose an external ID [#choose-an-external-id]
Every endpoint needs an **external ID**, a unique identifier your application uses to reference this endpoint instance. Use the URN format:
```text
urn:::
```
For example:
```text
urn:mycompany.com:farmapp:user-12345
```
External IDs are **unique within the scope of a tenant** (account). This means the same external ID cannot be used twice in the same account, even by different applications.
### Create the endpoint [#create-the-endpoint]
Send a `PUT` request to the endpoints API with your access token and chosen external ID. The example below focuses on the fields you will edit most often; open the API reference for the full schema and to try it against your own data.
If you prefer to work from generated code instead of raw HTTP examples, download the [OpenAPI specification](/api#openapi-specification) before implementing this call. The `putEndpoint` operation maps to `PUT /endpoints/{externalId}`.
The request body includes:
* **name** (optional): a human-readable label shown in the agrirouter web interface, 1-200 characters of letters, digits, spaces, and `-`, `_`, `.`, `,`, `:`. If you omit it, agrirouter generates one for you. Names do not have to be unique, and the user can rename the endpoint in the web interface; once they do, later PUT calls will not overwrite their choice.
* **application\_id** and **software\_version\_id**: the UUIDs of your registered application and its version (received during application registration in [Setup Application](/getting-started/setup-application))
* **endpoint\_type**: the type of endpoint, either `cloud_software` or `virtual_communication_unit` (see [Endpoint Types](/concepts/endpoints#endpoint-types))
* **capabilities**: the [message types](/message-types) your endpoint can handle, each with a direction (`SEND`, `RECEIVE`, or `SEND_RECEIVE`)
* **subscriptions**: the message types your endpoint wants to receive through the publish and subscribe model
For the full conceptual model, see [Capabilities](/concepts/messaging#capabilities) and [Subscriptions](/concepts/messaging#subscriptions) on the Messaging concepts page.
A successful response returns the endpoint details, including the **agrirouter-generated endpoint ID**, a UUID you will need when sending messages.
Store the `id` (agrirouter endpoint UUID) and `tenant_id` from this response. You will need both when sending messages: the API uses these agrirouter-generated UUIDs, not the external ID.
If the user has not authorized your application, this request will fail. The gateway checks that a valid authorization record exists for your application and the user's account before creating the endpoint.
### Verify in the agrirouter UI [#verify-in-the-agrirouter-ui]
Log in to the agrirouter UI and navigate to your account's endpoint list. Your newly created endpoint should appear with:
* The name of your application
* The capabilities you configured
* An active status
{/* TODO: SCREENSHOT: Endpoint list in agrirouter UI
Shows: The agrirouter account dashboard with the newly created endpoint visible in the endpoint list, displaying application name, status, and capability summary
*/}
## Capabilities and subscriptions in depth [#capabilities-and-subscriptions-in-depth]
The `capabilities` and `subscriptions` arrays you sent above decide which message types your endpoint can handle and which published messages it wants to receive. Both are required on every `PUT /endpoints/{externalId}` request. Each call replaces the previous configuration with the values you send, so always include the complete set.
Declaring a `SEND` or `RECEIVE` capability and a subscription only tells agrirouter what your endpoint is *willing* to handle. For any message to actually flow between two endpoints, the **account owner** has to configure a [route](/concepts/messaging#routing) for the matching information type in the agrirouter UI. Your `PUT /endpoints/{externalId}` call does not create routes, and **direct addressing with `x-agrirouter-direct-recipients` does not bypass this**: a direct send to a recipient with no route comes back as a `400`.
For a self-test, log in to a test account where you control routing and configure routes between your endpoints in the agrirouter UI before exercising send and receive flows. Plan for this before you start coding the send path; if the routes are not in place, your first send will fail or silently no-op (publish to nobody returns a `200` with no delivery).
Messaging Concepts
## What you have accomplished [#what-you-have-accomplished]
You now have:
* An **access token** that authenticates your application with agrirouter
* **User authorization** granting your application permission to manage endpoints
* An **active endpoint** with declared capabilities and subscriptions
For the authorization model in depth, see the authorization and security concepts page.
Authorization & Security
For the full endpoint lifecycle, including updates and revocation, see the endpoints concepts page.
Endpoint Concepts
## Next steps [#next-steps]
Your endpoint is ready to exchange data. Continue to the next page to send your first message.
Send Your First Message
# Cloud Software (/en/docs/integration/cloud-software)
How to integrate a cloud-hosted application with agrirouter as a cloud_software endpoint
This is the integration path for cloud-hosted applications that exchange data with agrirouter on behalf of end users, registered as the `cloud_software` endpoint type. For the conceptual overview, see [`cloud_software`](/concepts/ecosystem#cloud_software).
If your application also represents physical machines (tractors, implements, ISOBUS units), each machine is registered as a separate [`virtual_communication_unit`](/concepts/ecosystem#virtual_communication_unit) endpoint. See the [Virtual Communication Units guide](/integration/virtual-communication-units) for that flow. Any `cloud_software` application can do this; there is no separate endpoint type for fleet management.
## Typical Capabilities [#typical-capabilities]
A `cloud_software` endpoint typically supports these message types:
| Direction | Message Types |
| ----------- | --------------------------------------------------------------------------- |
| **Receive** | TaskData (ISO 11783), EFDI Device Description, EFDI Time Log, GPS positions |
| **Send** | TaskData (ISO 11783), Shape files |
Your actual capabilities depend on your application's functionality. Declare only the message types you genuinely support.
## Integration Steps [#integration-steps]
### Register as an App Provider [#register-as-an-app-provider]
Sign up for an agrirouter developer account and register your application. You receive an `application_id` and a `software_version_id` that identify your application and its current version on the platform.
### Implement the Authorization Flow [#implement-the-authorization-flow]
Your endpoint requires user authorization via the OAuth2-based consent flow. Users typically find your application through the agrirouter Solution Finder and arrive at your site via the [Deep URL](/concepts/authorization-and-security#discovery-via-deep-url) you provided during registration. Once the user lands on your site, redirect them to the agrirouter authorization URL. After the user grants access, the browser is redirected back to your `redirect_uri` with the original `state` parameter. No authorization code or token is returned; the authorization is recorded server-side.
Authorization and Security
### Create the Endpoint [#create-the-endpoint]
With a valid access token from the client credentials flow (see [Access Tokens](/concepts/authorization-and-security#access-tokens)), create an endpoint in the user's agrirouter account.
Send a single `PUT /endpoints/{externalId}` request with:
* A unique `externalId` for this endpoint instance (in the URL path)
* Your client credentials JWT in the `Authorization: Bearer` header
* **Capabilities**: the message types your endpoint can send and receive, with directions
* **Subscriptions**: the message types your endpoint wants to receive via publish/subscribe
* The `endpoint_type` field set to `cloud_software`
This single request creates the endpoint with its full configuration. Call the same endpoint again at any time to update capabilities and subscriptions.
Each `PUT` request **replaces all existing capabilities and subscriptions**. Always include the complete set of both in every update.
Existing integrations that register endpoints as `farming_software` continue to work using the steps on this page, but new integrations should use `cloud_software`. `farming_software` is a legacy value kept for backward compatibility and may be removed in a future version. See [Migrating from the Legacy API](/appendix/migrating-from-legacy) for the full mapping.
### Configure Routes [#configure-routes]
The farmer or contractor configures routes in the agrirouter UI to connect your endpoint with their other endpoints.
Routes between two cloud software applications are **not** auto-created: the end user must create them manually. **Managed routes** automatically bridge machinery (`virtual_communication_unit`) and cloud software (`cloud_software`, or legacy `farming_software`) endpoints when both declare compatible capabilities. See [Data Flow Control](/concepts/ecosystem#data-flow-control) for the conceptual model.
### Exchange Messages [#exchange-messages]
With capabilities, subscriptions, and routes in place, your application can send and receive messages through agrirouter.
* **Receiving**: open a Server-Sent Events connection on `GET /events` to receive incoming messages.
* **Sending**: POST messages to `/messages`, addressing them to specific endpoints or publishing them to all subscribers.
Sending and Receiving Messages
## Managing a Fleet of Machines [#managing-a-fleet-of-machines]
If your application represents physical machines (tractors, implements, ISOBUS units), register each machine as its own `virtual_communication_unit` endpoint via its own `PUT /endpoints/{externalId}` call. Each VCU has its own external ID, capabilities, subscriptions, feed, and events stream. Any `cloud_software` application can do this; there is no separate endpoint type for fleet management.
Continue with the Virtual Communication Units guide.
## Testing with IO-Tool [#testing-with-io-tool]
During development, use the IO-Tool to simulate message exchange. It acts as a counterpart endpoint, so you can send test messages to your application and verify that your application sends correctly.
IO-Tool
## Message Exchange Patterns [#message-exchange-patterns]
### Receiving Telemetry from Machines [#receiving-telemetry-from-machines]
A common pattern for cloud software:
1. A machine sends EFDI time logs and GPS data to agrirouter, either through a legacy CU or through a VCU endpoint your application registered for that machine.
2. The data is routed to your endpoint based on the configured routes.
3. Your application receives events on the SSE connection to `GET /events` and downloads the payloads.
4. Confirm receipt to remove messages from the feed.
### Sending Task Plans to Machines [#sending-task-plans-to-machines]
1. Your application creates a TaskData file (ISO 11783 XML).
2. Send the TaskData to agrirouter, addressing specific endpoints or publishing to subscribers.
3. The CU or VCU receives the task and loads it on the machine.
## API Reference [#api-reference]
Open the operations a `cloud_software` integration uses in the API playground:
Create or update endpoint
Send one or several messages
Receive events
Confirm received messages
Delete endpoint
## Next Steps [#next-steps]
# Communication Unit (Legacy API) (/en/docs/integration/communication-unit)
How to integrate a Communication Unit (CU) or hardware device with agrirouter using the legacy API
This guide is for **hardware device manufacturers** building physical Communication Units that use the [legacy API](/api/legacy) with MQTT or REST transport. CUs cannot use the current REST API with SSE.
If you are building a **cloud application**, see the [Cloud Software guide](/integration/cloud-software) instead.
A Communication Unit (CU) is a physical hardware device installed on an agricultural machine. CUs include terminals, telemetry boxes, and ISOBUS gateways. They collect machine data and exchange it with farming software and telemetry platform endpoints through agrirouter.
For the conceptual overview of endpoint types, see the [ecosystem documentation](/concepts/ecosystem).
## Key Characteristics [#key-characteristics]
* CUs are **hardware devices** running firmware, not cloud applications.
* CUs connect to machines via **ISOBUS** (ISO 11783) and report device descriptions for attached implements.
* Onboarding uses a **registration code (TAN)** entered by the user. No OAuth flow required.
* CUs typically **send** telemetry data (EFDI, GPS) and **receive** task data (TaskData).
* CUs use the **legacy API**. See the [Legacy API documentation](/api/legacy) for protocol details.
## Typical Capabilities [#typical-capabilities]
| Direction | Message Types |
| ----------- | ----------------------------------------------------- |
| **Send** | EFDI Device Description, EFDI Time Log, GPS positions |
| **Receive** | TaskData (ISO 11783) |
## Integration Steps [#integration-steps]
### Register Your CU Application [#register-your-cu-application]
Register your CU application in the agrirouter developer portal. You receive an `applicationId` and `certificationVersionId`.
### Generate a Registration Code [#generate-a-registration-code]
The end user generates a registration code (TAN) in the agrirouter UI. This code authorizes your CU to onboard to their agrirouter account.
Since agrirouter 2.0, the registration code (TAN) is valid for any CU application. The user no longer needs to select a specific CU type when generating the code.
### Onboard Using the Registration Code [#onboard-using-the-registration-code]
Send an onboarding request with:
* Your `applicationId` and `certificationVersionId`
* The registration code (TAN)
* A unique `externalId` for this CU instance
* Your preferred gateway type (`REST` or `MQTT`)
The response contains connection credentials (certificate and keys) for all subsequent communication.
### Declare Capabilities [#declare-capabilities]
Send a capabilities message declaring the message types your CU supports. This is what lets routing work between your CU and other endpoints.
Sending capabilities clears existing subscriptions. Always re-send subscriptions immediately after capabilities.
### Set Up Subscriptions [#set-up-subscriptions]
Subscribe to message types your CU wants to receive (typically TaskData).
### Send Device Descriptions [#send-device-descriptions]
Send an EFDI Device Description to agrirouter describing the machine and any attached implements. Re-send the description whenever the ISOBUS configuration changes (new implement connected, implement removed).
### Exchange Messages [#exchange-messages]
Your CU can now send telemetry and receive task instructions through agrirouter.
Sending and Receiving Messages
## ISOBUS Connection [#isobus-connection]
CUs that connect to machines via ISOBUS should:
1. **Discover implements**: read the ISOBUS network to identify attached implements and their Device Description (DD).
2. **Report device descriptions**: send EFDI Device Descriptions to agrirouter reflecting the current machine configuration.
3. **Collect time logs**: record EFDI Time Logs during field operations and send them to agrirouter.
4. **Receive tasks**: accept incoming TaskData files and load them onto the machine's task controller.
## Router Devices [#router-devices]
A router device is a CU that manages multiple sub-CUs under a single connection. This is useful for hardware that aggregates data from several machines.
Since agrirouter 2.0, router devices can have display names, making them easier to identify in the agrirouter UI.
## Re-Onboarding [#re-onboarding]
To refresh credentials or recover from a lost certificate, re-onboard using the same `externalId`. Re-onboarding returns new credentials and preserves the endpoint's identity, routes, and feed history.
## Certificate Management [#certificate-management]
The onboarding response carries the client certificate and private key in the format requested with `certificateType`: `PEM` (certificate and encrypted key as PEM text) or `P12` (a password-protected PKCS#12 bundle). Certificates issued today are valid for **10 years** from onboarding.
CU certificates have an expiration date. The expiration is visible to the user in the agrirouter UI. Plan for certificate renewal by implementing re-onboarding before certificates expire.
If a certificate expires before re-onboarding, the CU loses connectivity. Renew certificates well before they expire.
## Next Steps [#next-steps]
# Endpoint Management (/en/docs/integration/endpoint-management)
Managing endpoints throughout their lifecycle, from creation to update to revocation
Every application instance connected to agrirouter is represented as an **endpoint**. This page covers the full endpoint lifecycle, from creation through ongoing management to revocation.
For the conceptual model, see the [endpoints documentation](/concepts/endpoints).
## Endpoint Lifecycle [#endpoint-lifecycle]
An endpoint moves through these stages:
| Stage | Description |
| --------------- | --------------------------------------------------------------------------------------------------------------- |
| **Create** | Create the endpoint with a single `PUT` request that provides the external ID, capabilities, and subscriptions. |
| **Communicate** | Send and receive messages through the platform. |
| **Update** | Reconfigure capabilities and subscriptions with another `PUT /endpoints/{externalId}` request. |
| **Revoke** | Remove the endpoint when it is no longer needed. |
## Creating Endpoints [#creating-endpoints]
Create an endpoint by sending a `PUT /endpoints/{externalId}` request with the endpoint's `externalId`. The response is the created endpoint record.
Every current-API endpoint type (`cloud_software`, `virtual_communication_unit`, and the deprecated `farming_software`) follows the same authorization path before an endpoint can be created: the OAuth2 client-credentials + user-consent flow described in [Your First Endpoint](/getting-started/your-first-endpoint). Each VCU is registered directly with its own `PUT /endpoints/{externalId}` call on the same authorization, with no parent or concentrator endpoint involved.
For physical Communication Units, see the [legacy Communication Unit guide](/integration/communication-unit). CUs use the legacy API and its own authorization mechanism.
## Discovering and syncing tenant state [#discovering-and-syncing-tenant-state]
A long-running integration needs an up-to-date view of two things: which tenants have authorized your application, and which endpoints are currently visible in each of those tenants. The gateway exposes both a pull-based bootstrap and a push-based update path; production integrations typically combine the two.
### At startup [#at-startup]
Call [`GET /tenants`](/api/listAuthorizedTenants) once your application is up. The response lists every tenant for which an authorization currently exists, together with the endpoints visible in each tenant. Use this as the seed for whatever local state your application keeps about tenants, authorizations, and endpoints.
A tenant may appear in the response with an empty `endpoints` array. That is the privacy rule documented on the schema: until your application has created at least one of its own endpoints in a tenant, the visible-endpoints list is empty there, even if the tenant contains other endpoints. Once you create that first own endpoint, the full visible set appears.
### At runtime [#at-runtime]
Open the [`GET /events`](/api/receiveEvents) SSE stream and react to four event types alongside the data-flow events:
| Event | What changed | Typical reaction |
| -------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| [`AUTHORIZATION_ADDED`](/api/events/authorization-added) | A user just granted a new authorization for one of your application's scopes. | Add the tenant to your local list and start operating on it. |
| [`AUTHORIZATION_REVOKED`](/api/events/authorization-revoked) | A user just revoked an authorization. Access to the tenant for the given scope is already gone. | Drop locally cached state for the tenant and stop calling APIs scoped to it. |
| [`ENDPOINTS_LIST_CHANGED`](/api/events/endpoints-list-changed) | The set of endpoints visible to your application in a tenant changed, or a visible endpoint's capabilities or routes changed. | Replace the cached endpoint list for the tenant with the one carried by the event. |
| [`ENDPOINT_DELETED`](/api/events/endpoint-deleted) | One specific endpoint was deleted (by your application, by the user, or as a side effect of an authorization revocation). | Drop local state for that endpoint. |
`ENDPOINTS_LIST_CHANGED` only fires once your application has at least one endpoint of its own in the target tenant. Until then, changes to other endpoints in the tenant are not reported.
### Out-of-band refresh [#out-of-band-refresh]
If your application loses confidence in the cached list for a single tenant, [`GET /tenants/{tenantId}/endpoints`](/api/listTenantEndpoints) returns the full current list for that one tenant without a global resync. Use it when a frontend component wants endpoint information without subscribing to the event stream, or when you want to verify state after a reconnect.
## Updating Capabilities and Subscriptions [#updating-capabilities-and-subscriptions]
To update an endpoint's capabilities or subscriptions, send another `PUT /endpoints/{externalId}` request with the complete new configuration. This is the same endpoint used for creation: the first call creates the endpoint, and subsequent calls update it.
Each `PUT` request **replaces all existing capabilities and subscriptions**. Always include the complete set of both in every request. Omitting subscriptions leaves your endpoint without any, meaning it will not receive published messages.
A successful update returns an HTTP `200` response with the updated endpoint object. If the request fails, inspect the HTTP status code and error details. Common causes: invalid message types, or a mismatch with the software version's allowed capabilities.
## About Subscriptions [#about-subscriptions]
Subscriptions tell agrirouter which message types to route to your endpoint's feed when other endpoints publish them. Subscriptions only affect the publish/subscribe model. Directly addressed messages are delivered regardless of subscriptions, as long as a valid route and matching capabilities exist.
## Revoking Endpoints [#revoking-endpoints]
To permanently remove an endpoint, send a delete request. The endpoint loses access to agrirouter immediately.
Since agrirouter 2.0, the revocation response is simplified: the API returns an HTTP status code with an empty body instead of a detailed response payload.
After revocation:
* The endpoint can no longer send or receive messages.
* Routes involving the endpoint are removed.
* Unread messages in the feed are discarded.
* The same `externalId` can be reused to create a new endpoint later.
Since agrirouter 2.0, recreating a revoked endpoint has reduced or no waiting time. Previously, a mandatory delay applied before a revoked endpoint's external ID could be reused.
## Blocking Endpoint Instances [#blocking-endpoint-instances]
As an app provider, you can block specific endpoint instances of your application. Use cases:
* Revoking access from a user who has violated your terms of service.
* Disabling endpoints running outdated or unsupported versions.
* Managing license compliance.
Blocked endpoints cannot communicate through agrirouter until unblocked.
## Software Updates and Capability Changes [#software-updates-and-capability-changes]
When you release a new version of your application with changed capabilities (new message types, removed message types, or changed directions):
### Register a New Application Version [#register-a-new-application-version]
Register the new version in the agrirouter developer portal to get a fresh `software_version_id`. If the capability changes affect how your application communicates with agrirouter, the new version also goes through an [implementation review](/integration/implementation-review#re-review) before it can be used in production.
### Update Each Endpoint [#update-each-endpoint]
Update each active endpoint via `PUT /endpoints/{externalId}` with the new `software_version_id` and updated capabilities and subscriptions. Until an endpoint is updated, it keeps operating with the old capability set.
## Inviting Testers [#inviting-testers]
Before submitting an application version for [implementation review](/integration/implementation-review), you can validate it against real agrirouter accounts by inviting testers:
1. Open **Settings** > **Testers** in the agrirouter developer portal.
2. Add tester accounts by their agrirouter email address.
3. Invited testers can connect your pre-approval application to their agrirouter account and create endpoints under it.
Tester management is account-level, not part of a single application workspace. A tester invited by your developer account can test applications owned by that developer account.
Testers are only needed while the software version is not yet approved for production, that is while it is in **Testing approved**: in that state only tester accounts (and your own developer account) can connect to the version. Once the version is **Approved**, any agrirouter user can connect to your application and tester invitations are no longer required.
## API Reference [#api-reference]
Try the endpoint-lifecycle operations against your tenant in the API playground:
Create or update endpoint
Delete endpoint
List authorized tenants
List tenant endpoints
## Next Steps [#next-steps]
# Environments (/en/docs/integration/environments)
Production and QA environments for agrirouter integrations
agrirouter operates two independent environments: **Production** and **QA** (Quality Assurance). Accounts, endpoints, and data in one environment are fully isolated from the other. **Production is the default environment for partner integrations.** Use it throughout development, testing, and live operation.
## Production environment [#production-environment]
Production is where all partner integrations run, from the first line of code through live operation with end users. Your integration connects to real agrirouter accounts and exchanges real agricultural data from day one, and your credentials stay valid for the full lifecycle of your application.
When you need a counterpart to exchange messages with during development, the IO-Tool can act as a test receiver on Production.
## QA environment [#qa-environment]
The QA environment is reserved for agrirouter team use and for partners DKE has explicitly directed to it, typically to validate a new platform feature before it ships to Production. Data in QA may be reset periodically.
QA is **not** a general development environment. Use Production for day-to-day integration work. Do not distribute QA credentials or QA URLs to end users.
Endpoints in one environment cannot communicate with endpoints in the other. If you ever test against QA, make sure every component (including any IO-Tool instance) is configured for the same environment.
## Environment URLs [#environment-urls]
Each environment has its own set of API endpoints and UI URLs. For the complete list, see the appendix.
Environment URLs
## Availability area [#availability-area]
agrirouter currently operates in the **EU** area. Both the Production and QA environments are hosted within the EU region.
## Next steps [#next-steps]
With your developer account set up and the environments clear, create your first endpoint.
Your First Endpoint
# Events (/en/docs/integration/events)
Server-Sent Events emitted on GET /events, and how to subscribe to individual event types
Every event consumed from agrirouter flows through the [`GET /events`](/api/receiveEvents) Server-Sent Events stream. This page catalogs each event type, its `data:` payload fields, and a sample frame as it appears on the wire.
Each event is a pair of lines:
```text
event:
data:
```
The `data` value is a JSON object. Every payload carries an `event_type` discriminator field whose value matches the preceding `event:` line. See the [SSE specification](https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events) for the wire format.
Subscribe to a subset of event types with the `types` query parameter on `GET /events`, repeated once per type, for example `?types=MESSAGE_RECEIVED&types=FILE_RECEIVED`. If omitted, all event types stream on the connection.
## Two families of events [#two-families-of-events]
The events on the stream fall into two groups:
* **Data-flow events** describe messages and files arriving on your endpoints' feeds: `MESSAGE_RECEIVED` and `FILE_RECEIVED`.
* **Tenant-state events** describe changes to the access and visibility your application has across tenants: `AUTHORIZATION_ADDED`, `AUTHORIZATION_REVOKED`, `ENDPOINT_DELETED`, and `ENDPOINTS_LIST_CHANGED`. Use them to keep your local view of authorized tenants and visible endpoints in sync without polling.
## Event types [#event-types]
## Next steps [#next-steps]
# Implementation Review (/en/docs/integration/implementation-review)
How the agrirouter team reviews your application before it can connect to production. What we look for, prerequisites, and the status flow.
Before your application can communicate with production agrirouter, the agrirouter team runs through an **implementation review** with you. The review is attached to a submitted software version in the Developer Portal. It confirms that the version's declared capabilities match the behavior of the integration and that the application behaves reliably for end users. The review is free of charge. It replaces the older external certification process.
## When a review is required [#when-a-review-is-required]
A review is required when:
* You are connecting a **new application** to agrirouter for the first time.
* You make changes to an existing application that affect how it communicates with agrirouter (see [Re-review](#re-review) below).
## Prerequisites [#prerequisites]
Before requesting a review, make sure you have:
* Your **company information** and **support contact details** registered.
* A **developer account** with the application registered, metadata completed, OAuth client credentials created, and the application version you want reviewed prepared in the Developer Portal. See [Setup Application](/getting-started/setup-application) for the steps to get there.
* For the review session, **two distinct test accounts** on your platform that can both be onboarded to agrirouter, plus an [IO-Tool](/tools/io-tool) endpoint onboarded in one of the test agrirouter accounts to act as the counterpart for message exchange tests.
## Version Status Flow [#version-status-flow]
The review status belongs to the software version, not to the application profile.
| Status | Who changes it | Meaning |
| -------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------- |
| **Draft** | Developer | The version can still be edited. Add a release description and declare capabilities before submitting it. |
| **In review** | Developer submission | The version was submitted with **Submit for review**. Editing is locked while the agrirouter team reviews it. |
| **Testing approved** | agrirouter team | The version can be tested with reviewer or tester accounts before final approval. |
| **Approved** | agrirouter team | The version passed the implementation review. |
| **Needs changes** | agrirouter team | The submitted version cannot continue as-is. Create a new draft version for the corrected release. |
| **Blocked** | agrirouter team | Review is paused until the blocking issue is resolved with the agrirouter team. |
Publication is separate from this status flow. A version can be approved while the application is still private. The application becomes publicly visible only when the agrirouter team publishes the application after an approved version exists.
Going live also requires a signed agreement with the agrirouter operator. The agreement is not a prerequisite for the review itself, but the application is only set live once it is in place. See [Business and Legal](/getting-started/business-and-legal) for the participation options.
See how approval and publication relate in the application workspace.
## Review Scope [#review-scope]
The review scope is defined by the software version you submit:
* The **release description** explains what the version is meant to cover.
* The **capabilities** declare the message types the version can send and receive.
* The **application profile** supplies the name, brand, logo, support URL, Deep URL, and credentials used during review.
The review walks through your integration end-to-end and runs the explicit checks below. Sections marked **VCU only** apply to integrations that register `virtual_communication_unit` endpoints; skip them otherwise. Each item is phrased so it can be verified independently; the reviewer goes through them one by one and records any deviation.
### Onboarding [#onboarding]
#### Path 1: Initiated from your application [#path-1-initiated-from-your-application]
The user starts on your platform and is sent to agrirouter to grant consent.
* The user is redirected from your application to the agrirouter authorization screen.
* **If the user rejects:** they return to your application, and your UI shows a visible error message explaining the rejection.
* **If the user accepts:** they return to your application, and a new endpoint is visible in agrirouter. Run the [Endpoint configuration checks](#endpoint-configuration-checks) below against that endpoint.
After the happy-path test, leave the endpoint in place for later checks.
#### Path 2: Initiated from agrirouter (Deep URL) [#path-2-initiated-from-agrirouter-deep-url]
The user discovers your application from inside agrirouter and starts the connection there. To exercise this path without a published Solution Finder entry, manually open the [Deep URL](/concepts/authorization-and-security#discovery-via-deep-url) in the browser.
* Your application presents the user with a confirmation screen asking whether to onboard to agrirouter.
* After confirmation, the flow proceeds exactly as Path 1 from the redirect to the authorization screen onward, and the same [Endpoint configuration checks](#endpoint-configuration-checks) apply.
#### Endpoint configuration checks [#endpoint-configuration-checks]
Run these against every endpoint your application creates (`cloud_software` and, where applicable, each `virtual_communication_unit`):
* **Capabilities** match the message types and directions your application actually sends and receives.
* **Subscriptions** are declared, or a documented reason is given for declaring none.
* **Name** identifies the source account in a way an end user will recognize — for example, email address, organization name, or person name.
* **External ID** follows the [recommended format](/concepts/endpoints#external-ids) and is unique across the application.
* **Type** is set correctly (`cloud_software` or `virtual_communication_unit`).
* **`allow_delete_by_user`** is set to the value your platform expects (see [Offboarding](#offboarding) for the implications).
* The agrirouter control center shows the correct **application name, brand, and logo** for the connected endpoint.
#### Account-combination matrix [#account-combination-matrix]
The reviewer exercises these combinations to confirm that one account on your platform can map to multiple agrirouter accounts and vice versa:
| # | Your platform account | agrirouter account | Required |
| - | --------------------- | ------------------ | ------------------------------------------------------------------------------------------ |
| 1 | A | X | Yes — baseline onboarding from Path 1 or Path 2. |
| 2 | B | X | Yes — a *second* user from your platform onboards into the *same* agrirouter account. |
| 3 | A | Y | Optional — a *single* user from your platform onboards into a *second* agrirouter account. |
Each onboarding in the matrix must independently pass the endpoint configuration checks above.
### Managing Virtual Communication Units (VCU only) [#managing-virtual-communication-units-vcu-only]
If your application registers a `virtual_communication_unit` per machine, additionally verify:
* A VCU endpoint exists for every eligible machine exposed by your platform.
* Each VCU passes the [Endpoint configuration checks](#endpoint-configuration-checks) on its own.
* Renaming a machine on your platform is reflected as a name change on the corresponding VCU in agrirouter.
If your platform supports per-machine exposure controls, also verify:
* Removing one machine from agrirouter exposure on your platform causes its VCU to disappear in agrirouter.
* Re-adding the same machine causes its VCU to re-appear.
* When `allow_delete_by_user == true` for the VCU, deleting the VCU in agrirouter is reflected in your platform's machine state. `allow_delete_by_user` should not be set to `true` if your platform does not support per-machine management.
### Message sending [#message-sending]
These checks require **two distinct agrirouter accounts** connected by your application, with an [IO-Tool](/tools/io-tool) endpoint onboarded in one of them to act as the receiving counterpart.
For **every endpoint type in your application that declares a send capability**, the reviewer runs the full set of checks below. If both a `cloud_software` and a `virtual_communication_unit` endpoint can send, both are exercised end-to-end.
* The user can either pick one or more target endpoints from your UI (preferred for interactive flows) **or** your application publishes the message automatically (preferred for background flows).
* The **sending endpoint** on the outbound request is set correctly. When a message is sent on behalf of a VCU, the request carries that VCU's endpoint ID, not the parent `cloud_software` endpoint's.
* The file arrives at the target endpoint, verified by reading the IO-Tool inbox.
* The HTTP headers on the outbound message are valid.
* A **filename** is provided, even though the header is technically optional.
#### Edge case: connector outage [#edge-case-connector-outage]
The reviewer simulates an outage of the connection between your application and agrirouter using one of:
1. Configuring your connector to point at invalid agrirouter API endpoints.
2. Disabling the connector on your platform.
3. Temporarily revoking the OAuth credentials used by the connector.
With the connector disconnected:
* Triggering a send from your application produces either a user-visible error (interactive flows) **or** the message is queued for later delivery (background flows; queueing is also acceptable for interactive flows).
After reconnecting the connector:
* Queued or retried messages are delivered correctly, and the IO-Tool inbox receives them.
### Message receiving [#message-receiving]
These checks also require **two distinct agrirouter accounts** connected by your application and an [IO-Tool](/tools/io-tool) endpoint onboarded in one of them to act as the sender.
The reviewer runs a payload-type × target matrix from IO-Tool. Delivery mode (directly addressed vs. published) is irrelevant on the receiving side — the same event arrives either way — so it is exercised on the [sending side](#message-sending) only. Chunked payloads surface as [`FILE_RECEIVED`](/api/events/file-received) SSE events after reassembly; non-chunked payloads surface as [`MESSAGE_RECEIVED`](/api/events/message-received) events.
| Payload type | Target endpoint |
| ----------------------------------------------------------------- | --------------------------------------- |
| Chunked payload (e.g. TaskData, Shape, documents, images, videos) | `cloud_software` |
| Non-chunked payload (e.g. EFDI TimeLog, GPS positions) | `cloud_software` |
| Chunked payload | `virtual_communication_unit` (VCU only) |
| Non-chunked payload | `virtual_communication_unit` (VCU only) |
For every row in the matrix, verify:
* The payload is received in the correct account on your platform and surfaces in the correct context for the addressed endpoint.
* For VCU targets, the payload is forwarded to the machine without further user interaction wherever possible.
* After processing, receipt is confirmed so the feed count for that endpoint returns to 0 in the agrirouter control center's Endpoint Details view.
#### Edge case: connector outage [#edge-case-connector-outage-1]
Using the same disconnection methods as for sending, with the connector disconnected:
* IO-Tool addresses messages to any endpoint of your application; the feed count for those endpoints grows and stays non-zero — your application does not confirm what it has not processed.
After reconnecting the connector:
* The queued messages are processed and confirmed, and the feed count returns to 0.
### Offboarding [#offboarding]
The reviewer exercises every offboarding path your application supports:
* **Delete from agrirouter** (only when `allow_delete_by_user == true`): the user deletes the endpoint inside agrirouter; your platform reflects the disconnected state for the corresponding account or machine.
* **Disconnect from your platform:** when the user removes the agrirouter connection on your platform, all of that user's endpoints in agrirouter are deleted.
* **Revoke from agrirouter:** when the user revokes the authorization to your application in agrirouter, your platform reflects the disconnected state.
* **Re-onboarding after offboarding** succeeds — the same user can connect again and a fresh endpoint is created with the expected configuration.
### Cross-cutting requirements [#cross-cutting-requirements]
These apply to every endpoint type, regardless of which sections above are in scope:
* **Retry safety**: a `POST /messages` request that returned `200` is never sent again, since agrirouter does not deduplicate. Retries are limited to requests without a response and to `429` and `5xx` responses, and a `400` on a multi-recipient send is treated as a possible partial delivery. See [Retries](/integration/sending-and-receiving#retries).
* **Buffering**: when the connection to agrirouter is lost, the application buffers outbound messages and sends them once the connection is restored.
* **Error handling**: the application reacts correctly to HTTP error responses and to network errors, with retry logic for transient failures.
## How to request a review [#how-to-request-a-review]
Once your integration is feature-complete and you have a good feeling you will pass the requirements above, write to with:
* The **application ID** and **application version ID** of the version you want reviewed.
* A short summary of what the application does and which capabilities it uses.
* provide a **fully functional account to your platform** so the agrirouter team can make their own testing before and after the live-session.
* if you agree, this account may also be used to showcase the integration to other partners or in agrirouter materials.
### How the review session runs [#how-the-review-session-runs]
The review is a **live session led by your developer**. You drive each scenario from the checks above; the agrirouter team observes, asks follow-up questions, and records the outcome of every item.
The team walks through the areas above with you and updates the reviewed software-version status when the review concludes. Passing the review approves the version; public availability in the Solution Finder is handled as a separate publication step. See [Understand publication](/getting-started/setup-application#understand-publication) for how approval and publication relate to each other.
## Re-review [#re-review]
A re-review is required when an existing application changes in ways that affect how it communicates with agrirouter:
| Change | Re-review required? |
| ------------------------------------------------------------------------ | ------------------- |
| Adding new message types or directions | Yes |
| Changing communication patterns in a way that affects protocol behaviour | Yes |
| Adding or removing API operations the application uses | Yes |
| Bug fixes that do not change protocol behaviour | No |
| UI changes with no protocol impact | No |
When a re-review is required, create a new application version if your capabilities change, declare the changes, and submit the new version through the same review flow.
Then, write to with your request for re-review.
## Next steps [#next-steps]
# Integration Guide (/en/docs/integration)
Task-oriented guides for integrating farming software, telemetry platforms, and communication units with agrirouter
These guides cover integrating your application or device with agrirouter. Each one targets a specific developer persona and walks from first endpoint creation through production approval.
For implementation, the current REST API is also published as a hosted [OpenAPI specification](/api#openapi-specification). Use it to generate clients, import the contract into API tooling, or validate requests while you follow the integration guides.
## Which Integration Path Is Right for You? [#which-integration-path-is-right-for-you]
Answer these two questions to find the guide that matches your use case:
### Are you building a cloud application or a hardware device? [#are-you-building-a-cloud-application-or-a-hardware-device]
If you are building firmware for a **physical device** (terminal, telemetry box, ISOBUS gateway) that sits on a machine, follow the **Communication Unit** guide. That path uses the legacy API. If you are building a cloud or desktop application, continue to the next step.
### Does your application manage a fleet of machines? [#does-your-application-manage-a-fleet-of-machines]
If your application is a cloud backend that manages physical machines, where each machine should appear as a separate endpoint, follow the **Cloud Software** guide and then the **Virtual Communication Units** guide. If your application is a standalone application that exchanges data with machines but does not manage them directly, follow the **Cloud Software** guide only.
## Integration Guides [#integration-guides]
## Cross-Cutting Guides [#cross-cutting-guides]
These guides apply to all endpoint types. Read them after the type-specific setup.
## Understanding the Platform [#understanding-the-platform]
Before starting integration, make sure you understand the agrirouter ecosystem and the roles of the different endpoint types.
Ecosystem: Endpoint Types and Roles
# Remote Application Connection (/en/docs/integration/remote-application-connection)
Let your users easily connect your application to another one
Remote Application Connection (RAC) lets a user start from your solution, pick another agrirouter application, and end up with working routes between the two — without being asked to find their way through agrirouter on their own. Your application redirects the browser once, agrirouter drives login, onboarding of both sides and route creation, and the user is handed back to you with a result.
The application that starts the flow is the **initiating application**. The application being connected to is the **target application**. Both are identified by their catalog `application_id`.
## Prerequisites [#prerequisites]
### Set up Single Sign-On [#set-up-single-sign-on]
RAC is entered through your SSO integration: the start URL requires the `idp_alias` slug assigned to your solution, and the user is authenticated against your Identity Provider before anything else happens. Without SSO in place the flow cannot be started.
Single Sign-On
### Register your application and its capabilities [#register-your-application-and-its-capabilities]
Compatibility with other applications is computed from the message types declared on your application's software version. An application that declares no capabilities matches nothing and cannot be connected.
Cloud Software
### Configure the redirect URLs [#configure-the-redirect-urls]
The URL agrirouter returns to at the end of the flow must be registered in advance. In the agrirouter UI, open **Developer → Applications**, select your application, and add the URL under **Technical details → Remote Application Connection → Redirect URLs**.
Matching is an **exact string comparison** — no wildcards, no prefix matching, no normalisation of trailing slashes. A `redirect_uri` that is not on the list causes the connection request to be rejected.
The list is re-checked when the connection is completed, not only when it is started. Removing a URL while connections are still in progress causes those connections to fail at the final hand-back.
## Discovering compatible applications [#discovering-compatible-applications]
Your application can present the agrirouter applications it is compatible with — in a picker, a marketplace page, or anywhere that fits your UI — and offer your users a Connect action that starts the RAC flow for the one they choose. The list can be retrieved from the G4 API:
```
GET /compatible-applications?initiating_application_id=
```
The request is authenticated with your own client-credentials access token, and `initiating_application_id` must match the application that token belongs to — a mismatch is answered with `403`.
Compatibility is derived from declared capabilities alone, so the list is available before any endpoint exists for either side. An application is returned when messages could flow in at least one direction **and** it is visible to you — that is, published in the agrirouter catalog, or owned by the same tenant as your own application. The second half of that rule lets a vendor wire up their own applications before publishing them.
```json
{
"compatible_applications": [
{
"application_id": "1f9a8c62-3c1e-4a5b-9c4b-0f2f1a7d8e11",
"name": "IO-Tool",
"brand": "DKE-Data",
"description": "Test and diagnosis tool for agrirouter",
"logo_url": "https://images.agrirouter.com/logos/io-tool.png",
"type": "FARMING_SOFTWARE",
"compatible_capabilities": {
"can_send": ["iso:11783:-10:taskdata:zip"],
"can_receive": ["iso:11783:-10:taskdata:zip", "iso:11783:-10:time_log:protobuf"]
}
},
{
"application_id": "8d3b7e51-2a4c-4f6d-8e1b-7c9d0a2b3f44",
"name": "CCI A3",
"brand": "CCI",
"type": "COMMUNICATION_UNIT",
"compatible_capabilities": {
"can_send": ["iso:11783:-10:time_log:protobuf"],
"can_receive": ["iso:11783:-10:taskdata:zip"]
}
}
]
}
```
`compatible_capabilities` is expressed from the **target's** perspective: `can_send` lists what the target could send to your application, `can_receive` what it could receive from it.
`type` is the catalog type of the target, and it is the field that decides how the connection is made:
| `type` | How the target is connected |
| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `COMMUNICATION_UNIT` | A registration code shown by agrirouter and typed into the machine's terminal by the user. |
| `FARMING_SOFTWARE`, `TELEMETRY_PLATFORM`, `G4_APPLICATION` | The target's own Deep URL, where the user authorizes it. |
Machines are legitimate, connectable targets, but connecting requires the user enter the registration code on a terminal. Consider using `type` to label or group them in your picker.
`brand`, `description` and `logo_url` are optional and are omitted when the catalog entry does not provide them. Build the picker so a missing logo does not break the layout.
## Starting the flow [#starting-the-flow]
The flow is started by redirecting the user's browser to:
```
https://app.agrirouter.com/api/remote-app-connection/start
```
The following query parameters can / must be set:
| Parameter | Required | Description |
| --------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `initiating_application_id` | Yes | Your application's catalog ID, as a UUID. |
| `target_application_id` | Yes | The catalog ID of the application to connect to, as a UUID, taken from the discovery response. |
| `redirect_uri` | Yes | Where agrirouter returns the user at the end of the flow. Must match one of the registered redirect URLs exactly. |
| `state` | Yes | An opaque value echoed back unchanged on the return redirect. 1–512 characters from `A-Z a-z 0-9 _ . ~ -`. |
| `idp_alias` | Yes | The SSO slug assigned to your solution. Determines which Identity Provider the user is authenticated against. |
| `locale` | No | Interface language for the agrirouter screens. One of `de`, `en`, `es`, `fr`, `it`, `nl`, `pl`, `pt-BR`, `ru`. An unrecognised value is ignored and a language that's determined by the user's browser is used instead. |
| `company_name` | No | Company name for a user who has no agrirouter account yet. 1–256 characters. |
| `country_code` | No | Country for a user who has no agrirouter account yet, as an ISO 3166-1 alpha-2 code (for example `DE`). |
A complete start URL looks like this:
```
https://app.agrirouter.com/api/remote-app-connection/start
?initiating_application_id=8f2c1b40-6f7a-4c1e-9a3d-5e6f7a8b9c0d
&target_application_id=1f9a8c62-3c1e-4a5b-9c4b-0f2f1a7d8e11
&redirect_uri=https%3A%2F%2Fpartner.example.com%2Frac-callback
&state=7c9f1d2e4b6a8c0e
&idp_alias=iotool
&locale=de
```
Both parameters only apply to users who do not have an agrirouter account yet, and are ignored for everyone else. When **both** are supplied, the account is created from them silently and the user is never asked. When only **one** is supplied, the account creation dialog is shown with that field pre-filled. Send whichever details you have — a single value still saves the user a step.
A missing required parameter, or any parameter present with a malformed value, is answered with `400` and a plain-text message naming it, before any redirect happens. This includes the optional `company_name` and `country_code`: omitting them is fine, but sending an empty, over-long or unrecognised value is not. `locale` is the exception — an unrecognised value is ignored rather than rejected. Validate the values you build the URL from rather than relying on the user seeing that response.
### Native and mobile applications [#native-and-mobile-applications]
The flow is a browser flow from beginning to end, so a native application starts it by handing the start URL to the device's browser instead of displaying it itself. Most mobile platforms provide a browser component for exactly this purpose, and using it keeps the user's existing agrirouter session available. The flow cannot be shown inside your own app: the sign-in screen refuses to be embedded in another page, and a private web view would ask the user to sign in again on every launch.
Set `redirect_uri` to your app's own URL scheme, for example `myapp://rac-callback`, rather than to a web address. Both are accepted, but the hand-back is an ordinary browser redirect, and a web address does not reliably reach a native app.
Connecting the target application takes the user out of agrirouter, and the connection is only finished once they come back. A browser session that is discarded in between loses the link back to your application: the connection is left pending, is finished later inside agrirouter, and the user is not redirected to your `redirect_uri`.
## What the user experiences [#what-the-user-experiences]
### Authentication [#authentication]
The user is signed in through your Identity Provider, using the SSO integration identified by `idp_alias`. Users who already have an active agrirouter session skip this.
### Session creation [#session-creation]
agrirouter records a resumable connection session for the tuple of user, tenant, initiating application and target application. Re-firing the same start URL resumes that session rather than creating a duplicate.
### Initiator check [#initiator-check]
agrirouter checks whether an endpoint exists for your application in the user's account. If none does, the flow stops immediately and the user is sent back to your `redirect_uri` with `result=initiating_app_onboarding_required`.
### Target hop [#target-hop]
What happens here depends on the target's `type`.
#### Software Application targets [#software-application-targets]
The target application is opened at its registered Deep URL so the user can authorize it there, and agrirouter waits for the target's endpoint(s) to appear. How the target is opened depends on the kind of integration it uses:
* **Integrations handled inside agrirouter**: these targets (e.g. John Deere Operation Center and FarmENGAGE) are connected by agrirouter itself rather than by sending the user to an external site. These take over the **same tab**, and the user is brought back automatically once the target is connected. The user is then shown the pending connection and should select "Resume".
* **All other targets**: the Deep URL is opened in a **new tab**. agrirouter cannot observe what happens there, so the user has to return to the agrirouter tab themselves after connecting. A screen naming the target is shown in the original tab for exactly that reason, and it stays there until the target's endpoint is detected.
Opening that new tab is attempted automatically, but browsers block windows opened without a user gesture. When the automatic attempt is blocked, the waiting screen's **Continue** button opens the target from a real click instead, so the user is never stranded.
The wait is bounded at three minutes. If the target's endpoint has not appeared by then, the connection is kept as a pending connection in agrirouter and the user is left on the agrirouter home page, where it can be resumed later, rather than being redirected back to your application.
#### Communication Unit targets [#communication-unit-targets]
agrirouter creates a pending endpoint for the communication unit and shows the [registration code](/integration/communication-unit) that has to be entered on the machine's terminal. The code is valid for 24 hours and can be refreshed in place when it expires.
A **Continue later** action leaves the connection pending and sends the user to the agrirouter home page, where the machine is shown on the canvas with a *Pending* badge and the connection can be resumed. Resuming re-uses the code already issued for that session rather than minting a second one.
The step completes when the code is redeemed on a machine. A registration code is valid for **any** Communication Unit application, so redemption is what is waited for, not a match against the target's `application_id`: a user who walks to a different terminal than the one they picked still ends up connected.
Discarding a connection whose code has not been redeemed does not delete the pending endpoint. It stays on the user's canvas, where it can be cancelled separately.
### Route creation [#route-creation]
Once both sides have endpoints, routes between them are created automatically from the overlapping capabilities, and the user is shown a scoped canvas where those routes can be adjusted. Confirming with **Done** finalises the session and returns the user to your `redirect_uri`.
## Interpreting the result [#interpreting-the-result]
Every hand-back is a redirect to your `redirect_uri` with the `state` you supplied plus a `result` parameter:
| `result` | Meaning | What to do |
| ------------------------------------ | ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `connection_successful` | The routes were created and the user confirmed the connection. | Treat the connection as established and continue in your UI. |
| `initiating_app_onboarding_required` | The user has no endpoint for **your** application, so there is nothing to route to. | Onboard the user (authorization flow plus `PUT /endpoints/{externalId}`), then start the flow again. |
| `user_rejected` | The user cancelled the connection. | Return the user to where they started. The connection was not completed. |
Always compare the returned `state` against the value you generated before acting on the result, and treat any unrecognised `result` value as "not connected".
A `result` redirect is only sent when the user finishes in direct continuity with a launch from your application — in the same browser, within a short window of that launch. Someone who steps away and picks the pending connection up much later, or in a different browser, completes it inside agrirouter, and your `redirect_uri` is not called. Do not treat the absence of a callback as failure: a connection may still exist.
### Handling `initiating_app_onboarding_required` [#handling-initiating_app_onboarding_required]
This is the one result that is expected during normal operation. It says only that no endpoint for your application exists in the user's agrirouter account, which has two possible causes: the user has never authorized your application with agrirouter, or the authorization exists but no endpoint has been created against it yet.
Handle both by walking the same two steps and then re-entering the flow:
### Obtain authorization [#obtain-authorization]
Redirect the user through the agrirouter authorization flow to obtain a `tenant_id` for their account. This requires a real user interaction and cannot be done in the background.
### Create the endpoint [#create-the-endpoint]
Call `PUT /endpoints/{externalId}` with that `tenant_id`, your `application_id` and `software_version_id`, and the capabilities you declared.
### Retry the connection [#retry-the-connection]
Redirect to the start URL again with the same `target_application_id` and a freshly generated `state`.
Bound the number of retries. If a stale `tenant_id` is reused (for example one cached from a different login), the endpoint is created against an account that does not match the current session, agrirouter keeps reporting `initiating_app_onboarding_required`, and an unbounded retry loop bounces the user between the two applications indefinitely.
## Session lifetime [#session-lifetime]
An unfinished connection stays resumable: it is listed as a pending connection in the agrirouter UI, where the user can either finish or discard it. Sessions are removed once they are completed or discarded, and expire after 7 days.
Because a session is keyed on the user, tenant and the two applications, restarting the flow for the same pair is safe: it refreshes the existing session instead of accumulating duplicates.
## Next Steps [#next-steps]
# Sending & Receiving (/en/docs/integration/sending-and-receiving)
How to send and receive messages through agrirouter in production integrations, with addressing, retries, chunk reassembly, and feed confirmation
This page covers the integration-level concerns around sending and receiving messages: addressing modes, retries, chunk reassembly on the receive side, and feed confirmation. The walkthrough-style introductions live in the getting-started tutorials and are the right starting point the first time through.
For the conceptual messaging model, see [Messaging](/concepts/messaging).
## Sending a payload end to end [#sending-a-payload-end-to-end]
Every outbound message is a single HTTPS request to `POST /messages`. The envelope is carried in HTTP headers, and the payload is the raw request body with `Content-Type: application/octet-stream`. See [Send Your First Message](/getting-started/send-your-first-message#compose-and-send) for the full header reference.
If your integration uses generated clients, start from the hosted [OpenAPI specification](/api#openapi-specification) and check how your generator represents binary request bodies, repeated headers, and SSE streams.
A successful `200` response means agrirouter has accepted the message for routing. It does **not** confirm delivery to any recipient. There is no asynchronous delivery acknowledgement event for the sender.
POST /messages — API playground
### Addressing modes [#addressing-modes]
Choose between direct addressing and publish on a per-message basis using two headers.
Set `x-agrirouter-is-publish: false` and provide `x-agrirouter-direct-recipients` with a comma-separated list of recipient endpoint UUIDs. The message is delivered only to those endpoints, provided a route exists from the sender to each recipient for this message type.
Use direct addressing for interactive flows where the user picks the target, for example sending an application map from an FMIS to a specific machine.
Set `x-agrirouter-is-publish: true` and usually omit `x-agrirouter-direct-recipients`. The message is delivered to every endpoint that has a capability for the message type and is reachable through a route the account owner has configured.
Use publish for automated flows with an open-ended set of receivers, for example telemetry streamed from machines to any subscribed platform.
A publish send can still carry a list in `x-agrirouter-direct-recipients`, in which case those recipients receive the message in addition to any subscribers. This is an edge case, not a separate mode.
### Retries [#retries]
`POST /messages` has no idempotency key, and agrirouter does not deduplicate. Every request that is accepted creates a new message in the feed of each routed recipient, and the delivered message carries no application-side identifier that the recipient could use to detect a repeat. Retry decisions are therefore made on the HTTP outcome alone:
| Outcome | Retry? | Notes |
| --------------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `200` | No | The message is in the recipients' feeds. Sending it again delivers a duplicate. |
| No response (connection error, timeout) | Yes | The outcome is unknown. A retry may produce a duplicate on the receiving side, which the recipient has to tolerate. |
| `429`, `5xx` | Yes, with backoff | The message was not accepted. Back off exponentially between attempts. |
| `400`, `403`, `413` | No | The request is invalid as-is. Fix it before sending again, and read the `400` message text first: a partial-delivery `400` means some recipients already received the message (see below). |
The gateway does not return a `Retry-After` header on `429`, so the backoff schedule is up to the client. See [Rate limits](/api/errors#rate-limits) for how the limits are scoped and [Limitations](/appendix/limitations#request-rate) for the current values.
#### Multi-recipient direct sends are not atomic [#multi-recipient-direct-sends-are-not-atomic]
When `x-agrirouter-direct-recipients` names several endpoints, agrirouter evaluates routes and capabilities per recipient, delivers to every recipient that passes, and only then reports the failures:
* **No recipient is routable**: nothing is delivered. The response is `400` with the message `No recipients for this sender and info type`.
* **Some recipients are routable**: the message **is delivered** to those recipients. The response is still `400`, with the message `Recipient is not allowed from this sender`.
A `400` on a multi-recipient send therefore does not mean that nothing was delivered. Resending the same payload to the full recipient list after a partial failure delivers a duplicate to every recipient that already received it. Either send to each recipient in its own request, or resend only to the rejected recipients once the account owner has fixed the routes. See [Routing failures](/api/errors#routing-failures-on-post-messages) for the exact messages.
### Chunking [#chunking]
Payloads larger than the transport chunk size are split by agrirouter on the way out. The sender does not implement chunking. The `content-length` header must carry the total payload size in bytes; the API uses it to decide whether and how to split. agrirouter generates the chunk context that ties the chunks together, and the recipient receives the reassembled file as a single `FILE_RECEIVED` event.
The maximum payload size accepted by the API is **256 MB**. Payloads exceeding that limit are rejected with `413`.
## Receiving through the SSE stream [#receiving-through-the-sse-stream]
Events are delivered through a single Server-Sent Events stream. Open `GET /events` with a valid access token and keep the connection open; events stream as they occur. There is no polling API, no webhook callback, and no push-notification fallback.
The stream covers two families of events. The data-flow events tell you about messages and files arriving on your feeds; the tenant-state events tell you about authorizations and endpoint visibility:
| Event | Meaning |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
| `MESSAGE_RECEIVED` | A new message has arrived in the feed of one of your endpoints. |
| `FILE_RECEIVED` | A chunked file payload (TaskData, Shape, PDF, image, video) has been fully reassembled by agrirouter and is ready to download. |
| `ENDPOINT_DELETED` | One of your endpoints was deleted, either by your application or by the account owner. |
| `ENDPOINTS_LIST_CHANGED` | The set of endpoints visible to your application in a tenant changed, or a visible endpoint's capabilities or routes changed. |
| `AUTHORIZATION_ADDED` | A user granted your application a new authorization for a tenant. |
| `AUTHORIZATION_REVOKED` | A user revoked an authorization. Access to the tenant for the given scope is already gone when the event is delivered. |
See [Receive Your First Message](/getting-started/receive-your-first-message#listen-for-events-via-sse) for the wire format and a sample `MESSAGE_RECEIVED` event, and the [Events catalog](/integration/events) for the per-event payloads.
GET /events — API playground
### Keeping the connection open [#keeping-the-connection-open]
While the stream is idle, the gateway writes an SSE comment line (`: keep-alive`) every **5 seconds**. Comment lines carry no event and are ignored by SSE clients, but they let you detect a dead connection: if nothing at all arrives for well over 5 seconds (for example 30 seconds), close the connection and reconnect. Configure idle timeouts on HTTP clients and proxies accordingly, and disable response buffering on any proxy between you and agrirouter.
### Replay on reconnect [#replay-on-reconnect]
Long-lived SSE connections are the simplest model, but many integrations cannot hold one open: batch jobs, mobile clients, processes that cycle through restarts. When you open a fresh SSE connection, the gateway replays every unconfirmed message in your endpoints' feeds, then continues with live events. The replay starts a few seconds after the connection is established and walks the feeds in pages, oldest first, until it has caught up with the live stream. You do not need a cursor; reconnecting is enough to catch up.
Confirming events promptly matters: confirmed events drop out of the replay window, so the next reconnect only replays genuinely missed events.
### Downloading the payload [#downloading-the-payload]
Both `MESSAGE_RECEIVED` and `FILE_RECEIVED` events may include the payload directly as `payload` (base64 inline, for small payloads) or as a `payload_uri` link to download (for larger payloads). Exactly one of the two is present.
`payload_uri` links are time-limited and expire after at most **15 minutes**. Download the payload as soon as you receive the event.
The `payload_uri` is pre-signed and does not need an `Authorization` header. The response body is the raw binary (`application/octet-stream`) in the original format.
### Chunked file reassembly [#chunked-file-reassembly]
For chunked message types (TaskData, Shape, PDF, images, videos), agrirouter reassembles the chunks server-side and emits a single `FILE_RECEIVED` event when the whole payload has arrived. The event carries a `message_ids` array listing the agrirouter message IDs of the individual chunks, plus one `payload_uri` (or inline `payload`) for the reassembled file.
Your application does not see the intermediate chunks as separate events. You download the reassembled payload once and confirm every ID in `message_ids` so the chunks drop out of the feed.
## Confirming messages [#confirming-messages]
Every event you handle must be confirmed with `POST /confirmations`, otherwise it keeps replaying on reconnect and accumulates in your endpoint's feed. A confirmation is a `(endpoint_id, message_id)` pair; a single request can carry many.
A successful `202` means the confirmation was accepted for processing. The feed is updated asynchronously, so a just-confirmed message may still appear in the replay window for a brief interval.
POST /confirmations — API playground
Unconfirmed events accumulate in your feed and will replay on every reconnect. Confirm events after your application has processed them.
## Error handling [#error-handling]
| Error | Cause | Resolution |
| ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `400` on send | Malformed headers, unsupported message type, or missing required field. | Validate the request against the [API spec](/api/sendMessages) before retrying. |
| `400` on send, `No recipients for this sender and info type` | No direct recipient is reachable: no route exists from the sender, or the recipient lacks the capability. Nothing was delivered. | Ask the account owner to configure a route for the sender, recipient, and message type. |
| `400` on send, `Recipient is not allowed from this sender` | Some, but not all, direct recipients are reachable. The message **was delivered** to the reachable ones. | Do not resend to the full list. See [Multi-recipient direct sends](#multi-recipient-direct-sends-are-not-atomic). |
| `429` on send | The application exceeded its request rate. | Back off exponentially and retry. No `Retry-After` header is sent. See [Rate limits](/api/errors#rate-limits). |
| `403` on send | The endpoint ID in the request does not belong to a tenant the access token is authorized for. | Confirm the access token matches the endpoint's tenant; confirm the endpoint has not been deleted. |
| `413` on send | Payload exceeds the 256 MB limit. | Split the data at the application level before sending. |
| Event not arriving | The recipient's capabilities or the account's routes do not cover the message type. | Verify the recipient's capabilities and confirm the account owner has configured a route. |
| `payload_uri` returns `403` or `404` | The 15-minute expiry window has passed. | Trigger a fresh event, or re-request via the SSE stream on reconnect. |
See [Errors](/api/errors) for the full HTTP status code list and response body shape.
## Next steps [#next-steps]
# Single Sign-On (/en/docs/integration/sso)
Let your users reach agrirouter with the identity they already have in your solution
Single Sign-On (SSO) lets your users arrive at agrirouter carrying the identity they already hold in your solution, instead of maintaining a separate agrirouter password. Your Identity Provider (IdP) is registered as an upstream IdP in agrirouter's Keycloak, and agrirouter brokers the login.
Each partner solution is identified by a **slug**: a short, stable name assigned by the agrirouter team (for example, `iotool`). The slug appears in your redirect URLs and in the URL that starts the flow, so it is fixed once assigned.
Setting up SSO is a one-time exchange between you and the agrirouter team. Once it is in place, your solution starts the flow with a single redirect.
## Setup [#setup]
Setup has two halves: what is configured on your side, and what is requested from the agrirouter team. The slug is assigned by the agrirouter team, so the values below can only be finalised once the request has been processed.
### Create an OAuth/OIDC client in your IdP [#create-an-oauthoidc-client-in-your-idp]
An OAuth/OIDC client is created in your IdP for agrirouter to broker against, configured with the following parameters. The `` placeholder is replaced with the slug assigned to your solution.
| Parameter | Value |
| ------------------------ | ------------------------------------------------------------------------------------------ |
| Redirect URL | `https://auth.agrirouter.com/realms/agrirouter/broker//endpoint` |
| Post Logout Redirect URL | `https://auth.agrirouter.com/realms/agrirouter/broker//endpoint/logout_response` |
**PKCE** (Proof Key for Code Exchange) should be enabled where your IdP supports it, using either the `S256` method (preferred) or `plain`. Clients with PKCE disabled are also supported, so this does not block the integration.
Prefer `S256` over `plain`. The `plain` method transmits the code verifier unhashed, which offers no protection if the authorization request is intercepted.
### Send a request to the agrirouter team [#send-a-request-to-the-agrirouter-team]
The client cannot be brokered until it is registered on the agrirouter side. Write to including:
* **Display name** of your solution, as it should be shown to users — for example, `DKE-Data IO-Tool`.
* **Desired slug** (short name) of your solution — for example, `iotool`. The slug must be precise and must not conflict with other solutions.
* **Whether PKCE is required** on the configured client, and with which method (`S256` or `plain`).
* **How customer email addresses are confirmed** in your solution, described in enough detail to be assessed. Based on this description, the agrirouter team determines whether email addresses delivered by your IdP are fully trusted, or whether they are re-verified on the agrirouter side.
The email verification decision is what determines whether your users are asked for an emailed authorization code when a new agrirouter account is created. If delivered email addresses are fully trusted, that step is skipped on the create path. Linking to an **existing** agrirouter account is always verified by an emailed code, regardless of this decision.
### Wait for confirmation [#wait-for-confirmation]
Once the request has been processed, your IdP is registered as an upstream IdP and the slug is confirmed. The flow can then be started.
## Usage [#usage]
After the request has been processed, the SSO flow is initiated by calling or redirecting the user's browser to the start URL, with your assigned slug:
```
https://app.agrirouter.com/api/sso/start/
```
From there, the user is sent to your IdP to authenticate, and agrirouter takes over once the identity comes back.
### What the user experiences [#what-the-user-experiences]
What the user is asked next depends on whether their partner identity is already linked to an agrirouter account, and whether they are already signed in. The decision tree below covers every path.
The paths reduce to three outcomes:
* **Already linked.** The identity is recognised and the user is logged in. If they are signed in as a different account, they are asked whether to switch to the linked one; declining leaves the existing session untouched.
* **Linked to an existing account.** The user confirms which agrirouter account to link, then proves ownership of that address with an emailed authorization code.
* **A new account is created.** The account is created from the identity delivered by your IdP. An emailed authorization code is requested first unless email addresses from your IdP are fully trusted.
In every case the user ends up logged in with their partner identity linked, and later logins skip the linking questions entirely.
### Errors [#errors]
On failure, the user is redirected to `/sso/error?reason=...`. The `reason` is the error reported by Keycloak, `session_expired` when the SSO session is unknown or has expired, or `unknown_error`. SSO sessions expire 10 minutes after the flow is started.
# Virtual Communication Units (/en/docs/integration/virtual-communication-units)
Register physical machines as virtual_communication_unit endpoints and report their TeamSets
A **Virtual Communication Unit (VCU)** is an agrirouter endpoint that represents a physical machine: a tractor, an implement, or an ISOBUS-connected unit. In the current API each machine is its own full endpoint of type `virtual_communication_unit`, registered in an end user's agrirouter account via its own `PUT /endpoints/{externalId}` call. Every VCU has its own external ID, capabilities, subscriptions, and feed, and is addressable as a distinct source and destination when the end user configures routes.
For where VCUs fit in the ecosystem, see [Ecosystem: `virtual_communication_unit`](/concepts/ecosystem#virtual_communication_unit).
## Before you start [#before-you-start]
VCUs follow the same lifecycle as every other endpoint in the current API. Before you register a VCU on behalf of an end user, the application must already be authorized for that user's agrirouter account, and the external ID you are about to use must not yet exist in that tenant.
* [Authorization and Security](/concepts/authorization-and-security) for the one-time authorization flow.
* [Endpoint Management](/integration/endpoint-management) for the create, update, and delete lifecycle that VCUs share with software endpoints.
* [Tenant IDs](/concepts/tenants) for what a tenant is and how the tenant ID is passed on each call.
In earlier versions of agrirouter, a telemetry platform registered a single "concentrator" endpoint and attached VCUs underneath it through a shared connection. That model is gone. In the current API each VCU is its own endpoint, created directly, with no parent platform endpoint required. A `cloud_software` endpoint for the platform itself is only needed if the platform participates in data exchange as a distinct party, for example by receiving aggregated fleet data.
## Register a VCU [#register-a-vcu]
Create a VCU with the standard endpoint creation call:
```http
PUT /endpoints/{externalId}
```
Set `endpoint_type` to `virtual_communication_unit` in the request body. The `externalId` is your own stable identifier for the machine, for example a serial number, a local fleet ID, or a URN such as `urn:yourcompany:vcu:tractor-1234`. Every VCU needs its own external ID, unique within the tenant.
The request body has the same shape as for any other endpoint. See [`PUT /endpoints/{externalId}`](/api/putEndpoint) for the full reference and [Endpoint Management](/integration/endpoint-management) for the full lifecycle.
### Capabilities and subscriptions [#capabilities-and-subscriptions]
Each VCU declares its own capabilities and subscriptions, independent of any other endpoint in the account. For most VCUs the declaration covers EFDI telemetry and ISOBUS task data:
| Direction | Typical message types |
| --------- | ------------------------------------------------------------------------------ |
| Send | `iso:11783:-10:device_description:protobuf`, `iso:11783:-10:time_log:protobuf` |
| Receive | `iso:11783:-10:taskdata:zip` |
See [Message Types](/message-types) for the full catalogue, and the individual message-type pages for payload details.
Every `PUT /endpoints/{externalId}` replaces the endpoint's entire configuration. Always send the complete set of capabilities and subscriptions on each update to avoid silently losing parts of the declaration.
## Report TeamSets with Device Descriptions [#report-teamsets-with-device-descriptions]
Every VCU should publish an [EFDI Device Description](/message-types/efdi#device-description-teamset) reporting the machines and implements currently attached to it. A Device Description is identified by a **TeamSet context ID**, which receivers use to correlate device descriptions, time logs, and task data that belong to the same on-machine setup.
Carry the TeamSet context ID in the `x-agrirouter-teamset-context-id` request header on `POST /messages`. See [TeamSet Context ID](/message-types/efdi#teamset-context-id) for the full contract, including how the identifier relates to `x-agrirouter-context-id` and when to generate a new one.
Send a Device Description:
* Immediately after the VCU endpoint is created.
* Whenever the machine configuration changes, for example when an implement is attached or a device is replaced.
* Periodically to confirm the machine is still active, typically at an interval between once per hour and once per day.
## Send and receive as a VCU [#send-and-receive-as-a-vcu]
Messages sent on behalf of a VCU use that VCU's own endpoint ID in the `x-agrirouter-endpoint-id` header. Each VCU has its own feed and its own events stream on `GET /events`, and there is no shared connection between a VCU and any other endpoint. A cloud-hosted application managing a fleet acts as each VCU in turn, setting the appropriate `x-agrirouter-endpoint-id` per request.
See [Sending & Receiving](/integration/sending-and-receiving) for the full messaging flow, and [Messaging](/concepts/messaging) for the conceptual model.
## VCU Lifecycle [#vcu-lifecycle]
| Action | How |
| ------ | --------------------------------------------------------------------------------------------------------------------------- |
| Create | `PUT /endpoints/{externalId}` with `endpoint_type: "virtual_communication_unit"`. |
| Update | Another `PUT /endpoints/{externalId}` to the same external ID, with the new complete set of capabilities and subscriptions. |
| Delete | `DELETE /endpoints/{externalId}`, or the end user removes the endpoint from the agrirouter UI. |
Communication Units (CUs) are physical hardware devices, such as ISOBUS terminals or telemetry boxes, that connect to agrirouter through the [legacy API](/api/legacy). They are not the same as VCUs and are not an endpoint type in the current API. Hardware manufacturers building a physical device should follow the [Communication Unit integration guide](/integration/communication-unit). For a mapping between legacy and current terminology, see [Migrating from the Legacy API](/appendix/migrating-from-legacy).
## API Reference [#api-reference]
VCUs share the current API with every other endpoint type. Try the operations a fleet platform exercises per machine in the API playground:
Create or update endpoint
Send one or several messages
Receive events
Delete endpoint
## Next Steps [#next-steps]
# Documents (/en/docs/message-types/documents)
PDF document exchange via agrirouter
| Property | Value |
| -------------------------- | -------------------- |
| **Technical Message Type** | `doc:pdf` |
| **Information Type** | Document |
| **Format** | Binary (PDF) |
| **Protobuf Schema** | None (binary format) |
## Overview [#overview]
The Document message type carries PDF files between endpoints. Typical payloads are delivery notes, invoices, field reports, and compliance records.
## Data Format [#data-format]
Send the raw PDF bytes as the request body with `Content-Type: application/octet-stream`. Do not Base64-encode the payload yourself; agrirouter applies transport encoding internally for chunked message types.
## Chunking [#chunking]
The API handles chunking for you. If a PDF exceeds the internal chunk size, the API splits it on the way out and the recipient gets the reassembled file as a single `FILE_RECEIVED` event. No manual chunking or reassembly on your side.
## Use Cases [#use-cases]
* **Delivery notes**: documentation accompanying seed, fertilizer, or crop deliveries
* **Invoices**: billing documents for agricultural services or products
* **Field reports**: scouting reports, soil analysis results, or advisory documents
* **Compliance records**: regulatory documentation such as spray logs or organic certifications
* **Contracts**: agreements between farmers, contractors, and suppliers
## Next steps [#next-steps]
# EFDI (/en/docs/message-types/efdi)
EFDI telemetry data, Device Descriptions (TeamSets) and TimeLogs for live machine telemetry
EFDI (Extended Farm Device Interface) covers two related technical message types for machine telemetry: **Device Descriptions** that report the configuration of machines and devices, and **TimeLogs** that stream live sensor and task data.
Both types use Protobuf and are defined in the [agrirouter TMT Protobuf Definitions](https://github.com/DKE-Data/agrirouter-tmt-protobuf-definitions) repository.
## Device Description (TeamSet) [#device-description-teamset]
| Property | Value |
| -------------------------- | --------------------------------------------------------------- |
| **Technical Message Type** | `iso:11783:-10:device_description:protobuf` |
| **Information Type** | Telemetry |
| **Format** | Protobuf |
| **Protobuf Schema** | `efdi.ISO11783_TaskData` (only the `device` property is filled) |
| **TypeURL** | `types.agrirouter.com/efdi.ISO11783_TaskData` |
### Overview [#overview]
A Device Description reports the devices attached to a Virtual CU (VCU), or to a legacy Communication Unit (CU) on the legacy API. This collection of devices is called a **TeamSet** and is identified by a unique **TeamSet ID** (a GUID is a good default).
### TeamSet ID Rules [#teamset-id-rules]
The TeamSet ID drives telemetry routing on the receiving side, so:
* The ID **must change** whenever the device description changes, even for small edits
* The ID **should be deterministic**: the same device description should always produce the same TeamSet ID
* The ID **must change** when:
* A different sender (VCU, or a legacy CU) is used
* A machine is added or removed from the configuration
* The DDOP (Device Descriptor Object Pool) changes
* The order of machines in the configuration changes
If you keep the same TeamSet ID after a configuration change, telemetry will be routed and associated incorrectly on the receiving side.
### When to Send [#when-to-send]
Send Device Descriptions:
* Whenever the device configuration changes
* On power-on of the VCU (or of a legacy CU on the legacy API)
* Periodically, somewhere between once per hour and once per day
Since agrirouter Machine 2.0, EFDI is no longer used to create machine endpoints. You still need to send the device description so the receiver can associate telemetry data with the right machines in a TeamSet.
### Protobuf Definition [#protobuf-definition]
The Protobuf definitions for Device Descriptions are available at:
[https://github.com/DKE-Data/agrirouter-tmt-protobuf-definitions](https://github.com/DKE-Data/agrirouter-tmt-protobuf-definitions)
***
## TimeLog [#timelog]
| Property | Value |
| -------------------------- | ----------------------------------- |
| **Technical Message Type** | `iso:11783:-10:time_log:protobuf` |
| **Information Type** | Telemetry |
| **Format** | Protobuf |
| **Protobuf Schema** | `efdi.TimeLog` |
| **TypeURL** | `types.agrirouter.com/efdi.TimeLog` |
### Overview [#overview-1]
TimeLog messages carry live telemetry from machines: sensor readings and task values expressed as DDIs (Data Dictionary Identifiers). agrirouter can filter TimeLog data by specific DDIs based on the routing configuration, so receivers only get the data elements they subscribed to.
Each TimeLog message must not exceed **1 MB**. If your telemetry data grows beyond that, split it across multiple messages.
Send a **Device Description** before you start sending TimeLog messages. Without it, the receiver cannot interpret the telemetry data or associate it with the right machines.
### Data Format [#data-format]
TimeLog messages use Protobuf directly: they are **not** Base64-encoded. Put the Protobuf binary data straight into the message payload.
### DDIs [#ddis]
DDIs (Data Dictionary Identifiers) are standardized identifiers for sensor and task values defined by the AEF (Agricultural Industry Electronics Foundation). Each DDI represents one measurement or parameter, for example ground speed, fuel consumption, or application rate.
The full list of standardized DDIs is at [https://isobus.net](https://isobus.net).
***
## TeamSet Context ID [#teamset-context-id]
The TeamSet context ID is the value that ties EFDI messages together so a receiver can correlate device descriptions and time logs from the same on-machine setup. The same identifier is also called the **TeamSet ID** in EFDI: both names refer to the same concept and the same value.
Senders carry it in the `x-agrirouter-teamset-context-id` request header on `POST /messages` (max 100 characters, free format, GUIDs work well). Receivers find it on the `teamset_context_id` field of `MESSAGE_RECEIVED` and `FILE_RECEIVED` events when the sender included it.
The TeamSet context ID is **not** the same as the [`x-agrirouter-context-id`](/api/sendMessages) header. That header is a per-payload UUID generated by the application and required on every send. The TeamSet context ID is a separate, optional header (`x-agrirouter-teamset-context-id`) that identifies the machine set the payload belongs to.
### When to set it [#when-to-set-it]
The header is optional at the wire level, but receivers cannot interpret EFDI payloads without it. In practice, set it on every message that carries:
* `iso:11783:-10:device_description:protobuf` (Device Description, see above)
* `iso:11783:-10:time_log:protobuf` (TimeLog, see above)
The same value-change rules apply as for the [TeamSet ID inside the device description](#teamset-id-rules): change the ID whenever anything in the machine set configuration changes.
For non-EFDI message types, the header is optional. The value is passed through to the receiver unchanged, but it only carries meaning if the same sender has anchored a device description with the matching ID.
### Worked example [#worked-example]
A contractor mounts a sprayer onto a tractor and starts an application job:
1. The VCU determines the new machine configuration (Tractor A + Sprayer B) and generates a TeamSet context ID, for example `a3f1c2d4-b6e7-4f08-9c1a-2b3d4e5f6a7b`.
2. The VCU sends a Device Description message with `x-agrirouter-teamset-context-id: a3f1c2d4-...` describing both machines and their devices.
3. As the job runs, the VCU sends TimeLog messages with the same `x-agrirouter-teamset-context-id`, carrying live telemetry from the sprayer's sensors.
4. The receiving FMIS sees both messages under the same TeamSet context ID, looks up the device description it received earlier, and decodes each TimeLog DDI against the right device.
5. After the job, the contractor swaps the sprayer for a fertilizer spreader. The VCU detects the change and generates a new TeamSet context ID, for example `7c2e9b18-3d54-4a91-b07f-e8c6d2a14b3e`. From this point on, all device descriptions and time logs from this VCU carry the new ID, and the receiving FMIS treats them as a separate machine set.
### Reference [#reference]
Try the request and event payloads that carry the TeamSet context ID in the API playground:
Send one or several messages
Receive events
For the conceptual framing of TeamSets within agrirouter, see [Ecosystem: Machine Identification](/concepts/ecosystem#machine-identification).
# GPS (/en/docs/message-types/gps)
GPS position data format, gps:info message type
| Property | Value |
| -------------------------- | -------------------------------------------------------------- |
| **Technical Message Type** | `gps:info` |
| **Information Type** | Telemetry |
| **Format** | Protobuf |
| **Protobuf Schema** | `agrirouter.technicalmessagetype.GPSList` |
| **TypeURL** | `types.agrirouter.com/agrirouter.technicalmessagetype.GPSList` |
The `gps:info` message type is **deprecated**. For new integrations, send GPS position data in EFDI TimeLog messages (`iso:11783:-10:time_log:protobuf`) instead.
EFDI (Device Description & TimeLog)
## Overview [#overview]
The GPS message type carries a list of GPS position records. Each record includes coordinates, altitude, accuracy metrics, and status information.
GPS messages use Protobuf directly: they are **not** Base64-encoded.
## Message Structure [#message-structure]
A `GPSList` message contains a list of GPS position entries, each with the following fields:
| Field | Type | Description |
| ---------------------- | ----------- | -------------------------------------- |
| `position_north` | `double` | Latitude in WGS84 degrees |
| `position_east` | `double` | Longitude in WGS84 degrees |
| `position_up` | `sint64` | Altitude in millimeters |
| `position_status` | `enum` | GPS fix quality (see table below) |
| `pdop` | `double` | Position Dilution of Precision |
| `hdop` | `double` | Horizontal Dilution of Precision |
| `number_of_satellites` | `int32` | Number of satellites used for the fix |
| `gps_utc_timestamp` | `timestamp` | UTC timestamp of the GPS reading |
| `field_status` | `enum` | Field context status (see table below) |
## Position Status [#position-status]
The `position_status` enum indicates the quality of the GPS fix.
| Value | Name | Description |
| ----- | ----------------- | ------------------------------- |
| 0 | `D_NO_GPS` | No GPS signal available |
| 1 | `D_GNSS` | Standard GNSS fix |
| 2 | `D_DGNSS` | Differential GNSS fix |
| 3 | `D_PRECISE_GNSS` | Precise GNSS fix |
| 4 | `D_RTK_FINTEGER` | RTK fixed integer solution |
| 5 | `D_RTK_FLOAT` | RTK float solution |
| 6 | `D_EST_DR_MODE` | Estimated (dead reckoning) mode |
| 7 | `D_MANUAL_INPUT` | Manually entered position |
| 8 | `D_SIMULATE_MODE` | Simulated position |
| 9–13 | `RESERVED` | Reserved for future use |
| 14 | `D_ERROR` | GPS error state |
| 15 | `D_NOT_AVAILABLE` | GPS status not available |
## Field Status [#field-status]
The `field_status` enum provides context about the machine's location relative to a field.
| Value | Name | Description |
| ----- | ------------ | -------------------------------------- |
| 0 | `FS_UNKNOWN` | Unknown (default) |
| 1 | `FS_INFIELD` | Machine is working in a field |
| 2 | `FS_ONROAD` | Machine is traveling on a road |
| 3 | `FS_OFFROAD` | Machine is off-road but not in a field |
# Images (/en/docs/message-types/images)
Image file exchange via agrirouter. BMP, JPEG, and PNG formats
Three image formats are supported for exchanging visual data between endpoints. All of them use binary encoding and follow the same transfer pattern.
| Property | Value |
| -------------------------- | -------------------- |
| **Technical Message Type** | `img:bmp` |
| **Information Type** | Image |
| **Format** | Binary (BMP) |
| **Protobuf Schema** | None (binary format) |
Bitmap images in the standard BMP format. Uncompressed, so quality is preserved but file sizes are large.
| Property | Value |
| -------------------------- | -------------------- |
| **Technical Message Type** | `img:jpeg` |
| **Information Type** | Image |
| **Format** | Binary (JPEG) |
| **Protobuf Schema** | None (binary format) |
JPEG images with lossy compression. Good for photographs and complex imagery when smaller file sizes are worth some quality loss.
| Property | Value |
| -------------------------- | -------------------- |
| **Technical Message Type** | `img:png` |
| **Information Type** | Image |
| **Format** | Binary (PNG) |
| **Protobuf Schema** | None (binary format) |
PNG images with lossless compression. Good for screenshots, diagrams, and images that need transparency.
## Data Format [#data-format]
Send the raw image bytes as the request body with `Content-Type: application/octet-stream`. Do not Base64-encode the payload yourself; agrirouter applies transport encoding internally for chunked message types.
## Chunking [#chunking]
The API handles chunking for you. If an image exceeds the internal chunk size, the API splits it on the way out and the recipient gets the reassembled file as a single `FILE_RECEIVED` event. No manual chunking or reassembly on your side.
## Use Cases [#use-cases]
* **Field photos**: documenting crop conditions, pest infestations, or weed pressure
* **Crop documentation**: growth stage records and visual monitoring
* **Machine diagnostics**: screenshots from machine terminals or diagnostic displays
* **Scouting imagery**: visual records from field scouting operations
## Next steps [#next-steps]
# Message Types (/en/docs/message-types)
Overview of all technical message types supported by agrirouter
Technical message types are the standardized payload formats that endpoints exchange through agrirouter. Every message carries a technical message type identifier that defines the structure and semantics of its payload.
Technical message types are grouped into broader **information types**, such as TaskData, Telemetry, or Images. The two levels play different roles in routing:
* **Endpoints** declare capabilities and subscriptions at the **technical message type** level. Each entry in `PUT /endpoints/{externalId}` names a specific TMT such as `iso:11783:-10:taskdata:zip`.
* **Routes**, configured by the account owner in the agrirouter UI, operate at the **information type** level. A single route permits every TMT that belongs to the selected information type to flow between source and destination.
Messaging Concepts
## Supported Message Types [#supported-message-types]
The following table lists all technical message types currently supported.
| Technical Message Type | Information Type | Format | Description |
| ------------------------------------------- | ---------------- | ------------ | -------------------------------------------------- |
| `iso:11783:-10:taskdata:zip` | TaskData | ZIP (binary) | A zip file containing an ISO 11783-10 TaskData set |
| `iso:11783:-10:device_description:protobuf` | Telemetry | Protobuf | EFDI device descriptions (TeamSets) |
| `iso:11783:-10:time_log:protobuf` | Telemetry | Protobuf | Live telemetry data (TimeLogs) |
| `gps:info` | Telemetry | Protobuf | GPS position information (deprecated) |
| `img:bmp` | Image | Binary | Bitmap image |
| `img:jpeg` | Image | Binary | JPEG image |
| `img:png` | Image | Binary | PNG image |
| `shp:shape:zip` | Shape | ZIP (binary) | ESRI Shapefile dataset |
| `doc:pdf` | Document | Binary | PDF document |
| `vid:avi` | Video | Binary | AVI video |
| `vid:mp4` | Video | Binary | MPEG4 video |
| `vid:wmv` | Video | Binary | WMV video |
## Message Type Categories [#message-type-categories]
## Requesting New Message Types [#requesting-new-message-types]
The set of supported message types can be extended. If your application needs a message type that is not listed above, write to .
# TaskData (/en/docs/message-types/taskdata)
ISO 11783-10 TaskData exchange. Zip format, ISOXML structure, and use cases
| Property | Value |
| -------------------------- | ---------------------------- |
| **Technical Message Type** | `iso:11783:-10:taskdata:zip` |
| **Information Type** | TaskData |
| **Format** | ZIP (binary) |
| **Protobuf Schema** | None (binary zip format) |
## Overview [#overview]
TaskData is the main format for exchanging agricultural task information through agrirouter. It follows the ISO 11783-10 standard (ISOXML) and is used to move work orders, application maps, as-applied data, and yield maps between farm management systems and machines.
## Data Format [#data-format]
A TaskData message contains a **zip file** holding an ISOXML dataset. The entry point inside the zip is `TASKDATA.XML`, which references every other resource in the dataset.
Package the ISOXML dataset into a zip archive and send the raw zip bytes as the request body with `Content-Type: application/octet-stream`. Do not Base64-encode the payload yourself; agrirouter applies transport encoding internally for chunked message types.
### Archive layout [#archive-layout]
agrirouter treats the archive as an opaque binary. It does not open, validate, or rewrite it, and the [implementation review](/integration/implementation-review) does not check the archive layout either. What matters is that the receiving side can read it, and terminals are the strictest readers. Follow the ISO 11783-10 conventions for the best interoperability:
## Chunking [#chunking]
The API handles chunking for you. If a TaskData zip exceeds the internal chunk size, the API splits it on the way out and the recipient gets the reassembled file as a single `FILE_RECEIVED` event. No manual chunking or reassembly on your side.
## Use Cases [#use-cases]
* **Task planning**: sending work orders and job definitions from FMIS to machines
* **Application maps**: variable-rate prescriptions for seeding, fertilizing, or spraying
* **As-applied data**: actual application records collected during field operations
* **Yield maps**: harvest data collected by combine harvesters or other machines
## ISOXML Structure [#isoxml-structure]
The ISOXML dataset in the zip follows the ISO 11783-10 standard. `TASKDATA.XML` is the main descriptor and references any additional data files (binary time logs, grid files) in the archive.
For the full specification, refer to the ISO 11783-10 standard documentation.
## Next steps [#next-steps]
# Videos (/en/docs/message-types/videos)
Video file exchange via agrirouter. AVI, WMV, and MP4 formats
Three video formats are supported for exchanging recorded video between endpoints. All of them use binary encoding and follow the same transfer pattern.
| Property | Value |
| -------------------------- | -------------------- |
| **Technical Message Type** | `vid:avi` |
| **Information Type** | Video |
| **Format** | Binary (AVI) |
| **Protobuf Schema** | None (binary format) |
AVI (Audio Video Interleave) container format. A widely-supported legacy format for uncompressed or lightly compressed video.
| Property | Value |
| -------------------------- | -------------------- |
| **Technical Message Type** | `vid:mp4` |
| **Information Type** | Video |
| **Format** | Binary (MP4) |
| **Protobuf Schema** | None (binary format) |
MPEG4 container format. The common modern choice: efficient compression at good quality. Recommended for most use cases.
| Property | Value |
| -------------------------- | -------------------- |
| **Technical Message Type** | `vid:wmv` |
| **Information Type** | Video |
| **Format** | Binary (WMV) |
| **Protobuf Schema** | None (binary format) |
Windows Media Video format. Occasionally seen on agricultural equipment with Windows-based terminals.
## Data Format [#data-format]
Send the raw video bytes as the request body with `Content-Type: application/octet-stream`. Do not Base64-encode the payload yourself; agrirouter applies transport encoding internally for chunked message types.
## Chunking [#chunking]
Video files are typically large, so chunking is common. The API handles it for you: if a video exceeds the internal chunk size, the API splits it on the way out and the recipient gets the reassembled file as a single `FILE_RECEIVED` event. No manual chunking or reassembly on your side.
## Use Cases [#use-cases]
* **Field operation recordings**: video documentation of planting, spraying, or harvesting operations
* **Machine operation documentation**: recording machine behavior for maintenance or training
* **Quality assurance**: visual records of crop handling and processing
* **Incident documentation**: recording equipment issues or field conditions for later review
## Next steps [#next-steps]
# Accounts & Tenants (/en/docs/concepts/accounts-and-tenants)
Account types, tenant model, and how applications relate to agrirouter accounts
Every person and company interacting with agrirouter does so through an **account**. Accounts determine what you can do on the platform, whether you are a farmer connecting your machines or a developer building an integration. Getting the account and tenant model right is essential for building a correct integration.
## Account Types [#account-types]
agrirouter has two distinct account types that serve different purposes:
### End User Accounts [#end-user-accounts]
End user accounts are for **farmers and contractors**: the people who own agricultural data and want to exchange it between their software and machines. An end user account:
* Is created by a farmer or contractor through the agrirouter registration process
* Owns all endpoints connected to it
* Controls data flow through routes configured in the agrirouter UI
* Can connect any number of applications (subject to [account limits](/appendix/limitations))
Each agrirouter account is completely isolated, data does not flow between accounts.
### Developer Accounts [#developer-accounts]
Developer accounts are for **app providers**, the companies that build software or hardware integrations with agrirouter. A developer account sits on top of a regular end-user account: you first register as an end user, then request that the account be promoted through the developer portal. Promotion does not replace the underlying end-user account, it layers developer-specific capabilities on top so the same account can both use agrirouter like any other end user and manage registered applications.
Once promoted, a developer account adds tools for:
* Registering applications and submitting application versions for approval
* Managing OAuth clients and credentials for your application
* Configuring integration settings and application metadata
Two practical conventions for developer accounts:
* Use a **generic company email address**, not a personal one, so the account stays usable when team members change.
* Plan on **one developer account per app provider**. All developers in your organisation share the same account.
Application, application-version, and OAuth-client management is self-service inside the Developer Portal. Create credentials from the application's **Auth clients** area, then store the one-time `client_secret` outside agrirouter because it is not shown again.
See Setup Application for the step-by-step walkthrough from end-user signup through account promotion, application registration, and obtaining credentials.
## Tenant Model [#tenant-model]
In agrirouter's architecture, each account is a **tenant**. The tenant is the fundamental unit of data isolation:
* Every endpoint belongs to exactly one tenant
* Data exchange happens between endpoints within a tenant
* Routes are scoped to a tenant
* Resource limits (endpoint counts, message quotas) are applied per tenant
The tenant model keeps each user's data isolated from every other user's data. The platform enforces this boundary at every level, from message routing to API access.
See Tenant IDs for how tenants are identified in API calls, how the ID relates to the other IDs in an integration, and how to obtain one.
## Authorizations [#authorizations]
An end user grants your application access to their tenant by completing the OAuth consent flow. The result is an **authorization**, the record that lets your application act under that user's tenant.
An authorization is uniquely identified by the triple `(tenant_id, application_id, scope)`:
* **`tenant_id`** — the user's tenant.
* **`application_id`** — your application; implicit in the OAuth credentials you are calling with.
* **`scope`** — the permission granted. Today the only scope in use is `endpoints:manage`, which is the permission your application requests during the consent redirect. Additional scopes may be introduced in future revisions of the API; if so, authorizations with different scopes for the same tenant are treated as distinct authorizations.
A single application typically holds many authorizations at the same time, one per tenant that has connected it. Your application discovers and tracks them through two complementary mechanisms:
* **Pull**: [`GET /tenants`](/api/listAuthorizedTenants) returns every tenant for which your application currently holds an authorization, together with the related endpoints for each tenant. Use it at startup, after a crash, or whenever you need to rebuild your view from scratch. To refresh a single tenant, [`GET /tenants/{tenantId}/endpoints`](/api/listTenantEndpoints) returns the endpoints currently visible there.
* **Push**: the SSE stream emits [`AUTHORIZATION_ADDED`](/api/events/authorization-added) when a user grants a new authorization and [`AUTHORIZATION_REVOKED`](/api/events/authorization-revoked) when one is taken away. When `AUTHORIZATION_REVOKED` arrives, your access to that tenant for the given scope is already gone — drop locally cached state for it.
A small privacy rule applies to the endpoints surfaced for an authorized tenant: until your application has at least one of its own endpoints in the tenant, the visible endpoints list is empty even if other endpoints exist there. Once your application creates its first endpoint, the full visible list appears and changes are reported via [`ENDPOINTS_LIST_CHANGED`](/api/events/endpoints-list-changed).
## Applications vs. Endpoints [#applications-vs-endpoints]
Two concepts that are important to distinguish:
| Concept | What it is | Who creates it | How many |
| --------------- | ------------------------------------------------------- | ---------------------------------------- | ------------------------------------- |
| **Application** | A software product registered with agrirouter | App provider (developer account) | One per product |
| **Endpoint** | An instance of that application within a user's account | End user (by connecting the application) | One per user per application instance |
For example, consider a company called "FarmPlan" that builds a cloud-based FMIS:
1. FarmPlan registers an **application** called "FarmPlan Pro" in their developer account
2. Farmer Alice connects FarmPlan Pro to her agrirouter account, which creates an **endpoint** in Alice's account
3. Farmer Bob also connects FarmPlan Pro to his account, which creates a separate **endpoint** in Bob's account
4. Alice and Bob's endpoints are completely independent, even though they represent the same application
### External ID Uniqueness [#external-id-uniqueness]
External IDs for endpoints are scoped to the **tenant**. This means:
* The same external ID can exist for the same application in different accounts
* The same external ID **cannot** be used twice within the same account, even by different applications
## Account Limits [#account-limits]
Each agrirouter account has a cap on the number of endpoints it can hold. The cap depends on the account type and subscription tier. Once you hit it, no new applications can be connected until existing endpoints are removed.
See the Limitations appendix for specific account and endpoint limits.
## Key Relationships [#key-relationships]
How accounts, applications, and endpoints fit together:
### App provider creates a developer account [#app-provider-creates-a-developer-account]
One developer account per company, built on top of a regular end-user account and then promoted through the developer portal.
### App provider registers applications [#app-provider-registers-applications]
Each software product is registered as a separate application in the developer account and receives an `applicationId`. OAuth clients for that application are created separately in the Developer Portal.
### End user creates an account [#end-user-creates-an-account]
Each farmer or contractor has their own end user account, which is an isolated tenant.
### End user connects an application [#end-user-connects-an-application]
The end user authorizes the application, and the application creates an endpoint in their account. The endpoint represents that specific instance of the application within the user's tenant.
### Endpoint communicates [#endpoint-communicates]
The endpoint sends and receives messages through agrirouter, subject to the routes configured by the account owner.
Learn more about endpoints, their lifecycle, and how they are managed.
# Authorization & Security (/en/docs/concepts/authorization-and-security)
The authorization flow, scopes, managing authorizations, and the security model for agrirouter integrations
Before an application can exchange data through agrirouter, the account owner must authorize the connection. The user grants the application permission to act on their behalf via a consent page; subsequent API calls are authenticated with the application's own client credentials token and authorized against the stored consent record. The flow uses OAuth 2.0 primitives, but it diverges from the textbook consent flow in one important way: no user-specific token is returned to the application.
## Discovery via Deep URL [#discovery-via-deep-url]
Before the authorization flow begins, the user needs a way to find your application and initiate the connection. This is where the **Deep URL** comes in.
When you register your application with agrirouter, you provide a **Deep URL**: an HTTPS URL pointing to your application's connection page where end users sign up or start a new agrirouter connection. This URL is stored on your application record and displayed in the agrirouter Solution Finder (the catalog of available integrations). The Developer Portal requires it for application types that can be connected from agrirouter; legacy Communication Unit records do not use a Deep URL.
The typical flow is:
1. An end user browses the agrirouter Solution Finder and finds your application
2. They click **Connect**
3. Their browser opens your Deep URL in a new tab
4. Your application takes over, typically starting the OAuth authorization flow described below
The Deep URL should point as close as possible to the connection entry point of your application and must be a stable, publicly accessible HTTPS URL.
Before kicking off the authorization flow, confirm that the user intends to start an agrirouter connection. Auto-redirecting on page load leads to dropped connections and confused users; always require an explicit action (a button click or confirmation dialog) before starting the flow.
The full path from discovery to authorization: the user finds your app in the Solution Finder, clicks Connect, lands on your site, confirms they want to connect, and your application redirects them into the authorization flow.
All applications require user authorization before they can create endpoints.
## Authorization Flow [#authorization-flow]
The flow uses OAuth 2.0 redirect mechanics and a consent page, but no authorization code or user token is returned to the application. The authorization is recorded server-side and validated on every subsequent API call.
### Application constructs the authorization URL [#application-constructs-the-authorization-url]
The application redirects the user's browser to agrirouter's authorization endpoint:
```http
https://app.agrirouter.com/api/authorize?client_id={clientId}&scope={scope}&state={state}&redirect_uri={uri}
```
The URL parameters are:
| Parameter | Required | Description |
| -------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `client_id` | Yes | The OAuth client ID assigned to your application during registration |
| `scope` | No | The permissions being requested (for example, `endpoints:manage`). Defaults to `endpoints:manage` if omitted. |
| `state` | No | An opaque string that agrirouter passes back unchanged in the redirect. Use it to prevent CSRF attacks and correlate the response with the request. |
| `redirect_uri` | No | The URL where agrirouter redirects the user after authorization. Must match the URI registered for your OAuth client. |
### User reviews and approves the connection [#user-reviews-and-approves-the-connection]
If the user is not already logged in, they are prompted to authenticate first. Once authenticated, the `client_id` is validated, the application details are resolved, and a consent page is displayed. The user sees the application's name and the requested scope.
The user has two choices:
* **Connect**: grants the application the requested permissions
* **Reject**: denies the request and returns the user to the application
### The authorization is created and the user is redirected back [#the-authorization-is-created-and-the-user-is-redirected-back]
When the user clicks **Connect**, an authorization record is created linking the application to the user's account with the requested scope. The browser is then redirected back to the application's `redirect_uri`.
**On approval:**
| Parameter | Description |
| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `state` | The same state value the application sent in the authorization URL |
| `tenant_id` | The tenant ID of the end user account the user selected on the consent page. Use this value as the `x-agrirouter-tenant-id` header on subsequent API calls acting on this user's behalf. |
The application should verify the `state` parameter matches the original value to prevent CSRF attacks, then store `tenant_id` for later API calls.
**On rejection:**
| Parameter | Description |
| --------- | ------------------------------------------------------------------ |
| `error` | `access_denied`: the user denied the authorization request |
| `state` | The same state value the application sent in the authorization URL |
No `tenant_id` is returned on rejection, since no tenant was selected.
### Application can now manage endpoints [#application-can-now-manage-endpoints]
After verifying the `state` parameter, the application knows the user has granted permission. The application uses its client credentials token, which it can obtain independently at any time (see [Access Tokens](#access-tokens)), to call the gateway API. The gateway verifies that a valid authorization record exists before processing requests like endpoint creation (see [Endpoint Creation](#endpoint-creation)).
The legacy authorization flow uses a different URL pattern (`/application/{applicationId}/authorize`) and returns a registration code with a cryptographic signature that must be verified before the legacy onboarding request. If you are working with the legacy API, see [Legacy API](/api/legacy) for details.
## Scopes [#scopes]
Scopes define the permissions an application requests from the user. Each authorization is tied to a specific scope.
| Scope | Description |
| ------------------ | -------------------------------------------------------------------------------- |
| `endpoints:manage` | Allows the application to create, configure, and delete endpoints on your behalf |
Additional scopes will be introduced in future versions of the API as more granular permissions become available.
## Managing Authorizations [#managing-authorizations]
Users can view and manage all active authorizations in the agrirouter UI under **Settings > Connections**.
The management page shows:
* **Authorized applications**: each application's name, logo, and brand
* **Endpoint count**: how many endpoints the application has created in your account
* **Scopes**: the permissions granted to each application (toggle visibility via the display options)
* **Sort options**: sort by application name or by endpoint count
### Revoking an Authorization [#revoking-an-authorization]
To revoke an application's access, click the **Revoke** button next to the application. A confirmation dialog will appear asking you to confirm the revocation.
Revoking an authorization removes the application's permission to manage endpoints in your account and deletes all endpoints the application created. The application will no longer be able to interact with your account.
## Access Tokens [#access-tokens]
The application authenticates itself using the **Client Credentials** flow to obtain an access token. This is independent of user authorization; the application can request tokens at any time, for example at startup. The application sends its `client_id` and `client_secret` to the token endpoint:
Create these credentials self-service in the Developer Portal under your application's **Auth clients** area. The `client_secret` is shown only once when the client is created or rotated, so it must be copied into a secrets manager before the dialog is closed. See [Obtain credentials](/getting-started/setup-application#obtain-credentials) for the UI walkthrough.
For G4 API integrations, the Developer Portal can download a JSON credentials package. The package is configuration only: it is not an SDK, not an access token, and not a substitute for the client credentials flow. It records one selected software version, one OAuth client, the redirect URI, endpoint URLs matching the Developer Portal's Production or QA environment, OpenAPI URLs, and the schema URL so your deployment can be configured consistently.
```http
POST https://api-oauth.agrirouter.com/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&client_id={clientId}&client_secret={clientSecret}
```
Alternatively, credentials can be sent via HTTP Basic authentication:
```http
POST https://api-oauth.agrirouter.com/token
Authorization: Basic base64({clientId}:{clientSecret})
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
```
The response contains an access token:
| Field | Description |
| -------------- | -------------------------------------------- |
| `access_token` | Bearer token used to authenticate API calls. |
| `token_type` | Always `Bearer` |
| `expires_in` | Token lifetime in seconds (default: 3600) |
The access token only proves the application's identity. When the application calls the gateway API (for example, to create an endpoint), the gateway separately verifies that a valid authorization record exists for this application and the user's account.
### Renewal and refresh tokens [#renewal-and-refresh-tokens]
Access tokens expire (default lifetime: one hour). Request a new token before expiration; once expired, API calls will fail with `401` until a new token is obtained.
agrirouter does not issue refresh tokens. Because the application authenticates with its own client credentials, it can mint a new access token at any time by repeating the token endpoint call. This is functionally equivalent to a refresh token and offers the same security guarantees.
Treat the access token as opaque. Do not attempt to verify, decode, or introspect it; the token format is not part of the public API and may change without notice.
## Endpoint Creation [#endpoint-creation]
After the user grants authorization and the application has obtained an access token, it can create an endpoint within the user's agrirouter account. The application sends a `PUT /endpoints/{externalId}` request with:
* An **external ID** that uniquely identifies this particular application instance
* The **access token** obtained from the token endpoint
The endpoint is created and its endpoint identifier is returned. The application uses its existing client credentials token for all subsequent API calls against this endpoint.
The external ID should follow the URN format: `urn:{company}:{application}:{instance-identifier}` (for example, `urn:farmplan:fmis:user-12345`).
Each endpoint belongs to exactly one **tenant** (the end user's agrirouter account), and every endpoint-management call must carry that tenant ID in the `x-agrirouter-tenant-id` request header so the gateway can scope the operation correctly.
See Tenant IDs for what a tenant is, how the tenant ID is delivered to your application, and how to pass it on API calls.
Learn about the full endpoint lifecycle after the endpoint is created.
Create or update endpoint
## Deleting Endpoints [#deleting-endpoints]
When an endpoint is no longer needed, it can be deleted from the agrirouter account:
* **Via API**: the application sends a `DELETE /endpoints/{externalId}` request.
* **Via agrirouter UI**: the end user deletes the endpoint manually from their account settings. When this happens, the creating application is notified via an [`ENDPOINT_DELETED`](/api/events/endpoint-deleted) SSE event; applications should listen for this event and clean up their local state when it arrives.
Deleting an endpoint removes it from all routes, deletes every message in its feed, and permanently removes it from the account.
Delete endpoint
## Conventions and Standards [#conventions-and-standards]
A few conventions shape how your integration handles data:
### Timestamps [#timestamps]
All timestamps must be in **UTC** using **ISO 8601 format** (for example, `2024-03-15T14:30:00.000Z`). agrirouter does not accept timestamps in local time zones.
### External IDs [#external-ids]
The recommended format for external IDs is a **URN** (Uniform Resource Name):
```text
urn:{company}:{application}:{instance-identifier}
```
For example: `urn:farmplan:fmis:user-12345` or `urn:acme:telemetry:device-serial-ABC`.
URNs provide a structured, globally unique identifier format that is human-readable and easy to debug.
## Differences from the Legacy API [#differences-from-the-legacy-api]
If you previously integrated with the legacy agrirouter API, several concepts no longer apply:
* **Mutual TLS (mTLS) and TLS client certificates** are not used in the current API. Applications authenticate exclusively via the client credentials OAuth 2.0 flow described above.
* **Router Devices** are legacy only. You may still see "Router Devices" in the developer portal's settings; new integrations can ignore this feature.
* **Time synchronization tolerance** is no longer enforced. The legacy API rejected requests whose timestamps drifted by more than one minute; the current API has no such requirement.
* **Base64 line-break and 1-based chunk-numbering conventions** are no longer relevant. Binary payloads and sequence numbers follow standard conventions.
## Security Best Practices [#security-best-practices]
Store your **client credentials** (`client_id` and `client_secret`), access tokens, and endpoint IDs securely. Use encrypted storage, environment variables, or a secrets management system. Never commit credentials to version control or log them in plain text.
Always use a cryptographically random state parameter in authorization requests and verify it in the redirect. This prevents cross-site request forgery (CSRF) attacks.
The `redirect_uri` parameter is validated with an exact string match against the URI registered for your OAuth client. Query strings, trailing slashes, and case differences all count, so register the exact URI your application will send. Never include sensitive data in redirect URI query parameters, and always use HTTPS for your callback endpoints.
Your `client_secret` is used to authenticate your application when requesting access tokens. Treat it like a password: never expose it in client-side code, commit it to version control, or log it. Use server-side token exchange and store the secret in a secrets manager or environment variable.
The credentials package JSON file includes a one-time `client_secret` when it is downloaded from the secret dialog after client creation or secret rotation. Packages downloaded later for an existing client contain a `client_secret` placeholder that you replace yourself. Store live values server-side, then remove local copies of the file. Existing stored secrets are never retrieved for package downloads.
# Ecosystem (/en/docs/concepts/ecosystem)
The agrirouter ecosystem: farmers, contractors, software, and machines
The agrirouter ecosystem covers everyone involved in agricultural data exchange: the farmers and contractors who own and operate machines, the software companies that build tools for them, and the machines themselves. agrirouter sits in the middle as a neutral data exchange hub, so these participants can communicate without each building a direct integration to every other one.
## Application Types in the Developer Portal [#application-types-in-the-developer-portal]
The **Application type** field in the Developer Portal describes the application record that belongs to an app provider. It is not the same thing as the endpoint type your integration creates later through the API.
For new current-API integrations, choose **Default application type**. That application record can create `cloud_software` endpoints, and it can also create `virtual_communication_unit` endpoints when the product represents physical machines on behalf of an end user.
The other application types are legacy compatibility values:
| Portal application type | Use it when |
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Default application type** | You are building a current REST API integration. This is the default for new applications. |
| **Telemetry Platform** | You are maintaining an older application that was registered under the historical telemetry-platform model. New fleet integrations should use the default application type and create one `virtual_communication_unit` endpoint per machine. |
| **Farming Software** | You are maintaining an older farming-software application. New software integrations should use the default application type and create `cloud_software` endpoints. |
| **Communication Unit** | You are maintaining or registering a physical hardware Communication Unit (CU) that uses the legacy API. The Developer Portal shows legacy auth fields for this type and does not require a Deep URL. |
The application type is selected when the application profile is created. After creation, the value is shown read-only in the workspace. Endpoint types are still chosen by your integration when it creates endpoints for an end user's tenant.
## Endpoint Types [#endpoint-types]
Endpoint types describe the participants inside an end user's agrirouter account. An application creates endpoints through `PUT /endpoints/{externalId}`, and each endpoint declares its `endpoint_type`.
The API distinguishes endpoint types you can **create** through the current API from types you may only **observe** on existing endpoints created via the legacy API.
Types you can create with `PUT /endpoints/{externalId}`:
| Type | Represents |
| ---------------------------- | --------------------------------------------------------------------------------------------- |
| `cloud_software` | A cloud-hosted application acting on behalf of an end user. |
| `virtual_communication_unit` | A single physical machine, implement, or machine group participating in ISOBUS data exchange. |
| `farming_software` | Legacy alias for `cloud_software`. **Deprecated**, use `cloud_software` for new endpoints. |
Types you may also observe on endpoints in a tenant, but cannot create through the current API:
| Type | Represents |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `communication_unit` | A physical hardware Communication Unit registered through the [legacy API](/api/legacy). |
| `telemetry_platform` | A legacy concentrator-style platform endpoint. New integrations register each machine directly as its own `virtual_communication_unit` endpoint instead. |
The rest of this page covers each creatable type in turn, then the concepts that are common to all endpoints: how they are identified, how data flows between them, and who controls that flow.
### `cloud_software` [#cloud_software]
`cloud_software` endpoints are **cloud-hosted applications** such as web applications, mobile backends, fleet management platforms, advisory tools, and farm management systems. They connect to agrirouter over HTTPS to exchange data on behalf of their users. They authenticate using client credentials, so the integration is only viable in environments that can securely store a secret. Untrusted clients like browser-only or desktop-only applications are not a supported deployment model.
A single installation or tenant of a cloud software product usually maps to one endpoint per end-user account: two different end users each have their own agrirouter account with their own `cloud_software` endpoint, isolated from each other. The same end user can also have several `cloud_software` endpoints in a single account when the connected software wants to represent organizational units separately, for example distinct farms, subsidiaries, or user accounts inside the application.
**Telemetry platforms**, the cloud systems that historically acted as the concentrator for a fleet of machines, are not a separate endpoint type in the current API. A telemetry platform only needs its own `cloud_software` endpoint if the platform itself participates in data exchange as a distinct party, for example to receive aggregated fleet data. The machines it manages should be registered directly as `virtual_communication_unit` endpoints.
### `virtual_communication_unit` [#virtual_communication_unit]
A `virtual_communication_unit` (VCU) endpoint represents a **physical machine** such as a tractor, implement, or ISOBUS-connected unit taking part in agricultural data exchange. A VCU is a full endpoint with its own external ID, capabilities, subscriptions, feed, and endpoint ID. It receives its own events and is addressable as a distinct source and destination when the end user configures routes in their account.
Messages sent by a VCU can carry a **TeamSet context ID** that identifies which group of connected machines the message belongs to. A TeamSet is a set of machines that work and move together and are connected to the same virtual communication unit, typically linked physically and informationally over ISOBUS. See [TeamSet Context ID](/message-types/efdi#teamset-context-id) for the wire-level contract.
See the Virtual Communication Units guide for integration details, including how to register a VCU and report its TeamSets.
### `farming_software` (deprecated) [#farming_software-deprecated]
`farming_software` is a legacy endpoint type kept for backward compatibility. New applications should register their endpoints as `cloud_software` instead. The two behave identically today, and `farming_software` may be removed in a future version of the API.
## Legacy Communication Units [#legacy-communication-units]
Physical hardware devices such as terminals, telemetry boxes, or ISOBUS gateways installed on machines historically connected to agrirouter as **Communication Units (CUs)** via the [legacy API](/api/legacy). CUs are not an endpoint type in the current API: there is no way to create a CU through it. Hardware manufacturers building a physical device should follow the separate [Communication Unit integration guide](/integration/communication-unit) for the legacy surface.
Coming from older docs or an existing integration? See Migrating from Legacy for a term-by-term mapping, including the difference between CU and VCU.
## Machine Identification [#machine-identification]
When a VCU participates in data exchange, the platform needs to know which physical machines are present and what they can do. That is captured in **EFDI Device Descriptions**, also known as **TeamSets**: a structured message sent by the VCU that reports the connected devices, their properties, and how they are grouped.
TeamSets are tied together across messages by a **TeamSet context ID** that the VCU carries on every related send, so receivers can line up telemetry, task data, and device descriptions that belong to the same on-machine setup.
See EFDI Device Descriptions for the wire-level message contract.
See TeamSet Context ID for the header name, when to set it, and when to generate a new value.
## Data Flow Control [#data-flow-control]
The core principle of agrirouter is that **the end user controls all data flow**. No data moves between endpoints automatically. The account owner must explicitly configure a route in the agrirouter UI.
A route specifies:
* A **source** endpoint that sends data.
* A **destination** endpoint that receives data.
* The **message types** that are allowed to flow along the route.
This keeps farmers and contractors in full control of their agricultural data. They decide exactly which software and machines can exchange which types of information.
Alongside individually configured routes, the end user can enable **managed routes** for an endpoint. Managed routes automatically connect a machinery endpoint, usually a `virtual_communication_unit`, with every software endpoint, usually a `cloud_software` or legacy `farming_software`, in the account for their shared message types, and vice versa. They only bridge the machinery-to-software divide: two VCUs are never connected to each other by a managed route, and neither are two software endpoints.
Learn more about routes, subscriptions, and message flow in the Messaging section.
## Member Roles [#member-roles]
The agrirouter ecosystem has two primary roles.
### End Users [#end-users]
End users are the **farmers and contractors** who own agrirouter accounts. They are the data owners. An end user:
* Creates and manages an agrirouter account.
* Connects their software and hardware to the account, creating endpoints.
* Controls data flow by configuring routes between endpoints.
* Can share data with other end users, for example a farmer sharing field data with a contractor.
The end user is always in control. No data moves between endpoints unless the account owner has configured a route for it.
### App Providers [#app-providers]
App providers are the **companies that build integrations** with agrirouter. An app provider:
* Registers as a developer on the agrirouter platform.
* Creates one or more applications that integrate with agrirouter.
* Submits software versions for implementation review before they can be used in production.
* Manages the technical aspects of the integration, including authorization, endpoint creation, and messaging.
A single app provider company may offer several applications, for example a farm management software product and a separate fleet management backend.
# Endpoints (/en/docs/concepts/endpoints)
What endpoints are, endpoint types, and the endpoint lifecycle in agrirouter
An **endpoint** is a connection point inside an end user's agrirouter account that an application manages. Every software installation or machine that connects to agrirouter becomes an endpoint. Endpoints are the building blocks of agrirouter: all messaging, routing, and data exchange happens between endpoints.
## Endpoint Types [#endpoint-types]
When creating an endpoint through the API, you specify one of three types:
| Type | Represents |
| ---------------------------- | ------------------------------------------------------------------------------------------- |
| `cloud_software` | A cloud-hosted application acting on behalf of an end user |
| `virtual_communication_unit` | A single physical machine (or group of machines) participating in ISOBUS data exchange |
| `farming_software` | Legacy alias for a direct-to-user application, **deprecated**, use `cloud_software` instead |
All three types use the same API and the same lifecycle described below. What differs is what each type represents in the real world: software installations versus individual machines. A VCU is a full-fledged endpoint, registered via its own `PUT /endpoints/{externalId}` call, with its own external ID, capabilities, and subscriptions.
`farming_software` is kept for backward compatibility only. New applications should register as `cloud_software`. The two are mapped to the same endpoint kind internally, so routing and default routes behave identically.
See the Ecosystem page for a detailed description of each endpoint type and when to use it.
### Communication Units (legacy) [#communication-units-legacy]
**Communication Units (CUs)** are physical hardware devices (terminals, telemetry boxes, ISOBUS gateways) that connect to agrirouter using the [legacy API](/api/legacy). CUs use the `communication_unit` endpoint type, which is not available in the current API.
As a developer using the current API, you do not need to create CU endpoints. However, **you will receive messages from CUs**: they remain active participants in the agrirouter ecosystem. When you see a `communication_unit` sender in your feed, this is a hardware device on a machine sending telemetry data, GPS positions, or device descriptions.
If you are a hardware manufacturer building a physical device, see the [Communication Unit integration guide](/integration/communication-unit).
Coming from older docs or an existing integration? See Migrating from Legacy for a term-by-term mapping, including the difference between CU and VCU.
## Endpoint Lifecycle [#endpoint-lifecycle]
Every endpoint goes through a defined lifecycle from creation to removal.
### Authorization [#authorization]
The end user must authorize the application before any endpoint can be created in their account. This is an OAuth2-based flow where the user logs into agrirouter and approves the application.
Authorization is a one-time action per end-user account. Once the end user has authorized your application, you can create and manage any number of endpoints in their account, of any type, without prompting them again. If the end user revokes the authorization, all endpoints the application created in that account are deleted and the application can no longer touch the account. To start over, the user has to authorize the application again.
See Authorization and Security for the full authorization flow.
### Endpoint Creation [#endpoint-creation]
Endpoint Creation is a single atomic step: one `PUT /endpoints/{externalId}` request that creates the endpoint and applies its initial configuration in one go. The request provides everything agrirouter needs:
* A unique **external ID** (as the path parameter) that identifies this particular instance
* The **application** and **software version** the endpoint belongs to, as UUIDs (`application_id`, `software_version_id`) that were registered beforehand in the Developer Portal
* The endpoint **type** (`cloud_software`, `virtual_communication_unit`, or legacy `farming_software`)
* **Capabilities**, the technical message types the endpoint can send and receive
* **Subscriptions**, the message types it wants to receive when other endpoints broadcast (publish) messages
agrirouter responds with the endpoint's assigned **endpoint ID**. The application stores this ID and uses it in the `x-agrirouter-endpoint-id` header to identify itself when sending messages. The application's credentials for authenticating to the API come from the separate OAuth client credentials flow, not from this call.
### Endpoint Update [#endpoint-update]
After creation, the same `PUT /endpoints/{externalId}` call can be made again at any time to update the endpoint's capabilities and subscriptions. This is an idempotent operation: each call replaces the endpoint's entire configuration with the new values.
Every `PUT` request **replaces all existing capabilities and subscriptions**. Always send the complete set of capabilities and subscriptions in each request to avoid silently losing parts of your configuration.
### Communication [#communication]
Once created, the endpoint can send and receive messages through agrirouter. This is the steady-state phase where the endpoint participates in agricultural data exchange according to the routes configured by the end user.
### Deletion [#deletion]
An endpoint can be deleted through two paths:
* The application calls `DELETE /endpoints/{externalId}`.
* The end user removes it manually from the agrirouter UI.
Either way, the endpoint is removed from the agrirouter account along with all its data, including feed entries and routes. If the endpoint [owns other endpoints](#endpoint-ownership), those owned endpoints are deleted as well.
## Endpoint Ownership [#endpoint-ownership]
Endpoints can form a **parent–child relationship**: one endpoint can be the **owner** of one or more other endpoints. This is useful when a single logical system consists of several endpoints — for example a platform endpoint that owns the individual machine endpoints it manages on the user's behalf.
Ownership is set when creating or updating the child endpoint, by passing the owner's external ID in the `owner_endpoint_external_id` field of the [`PUT /endpoints/{externalId}`](/api/putEndpoint) call. Two rules apply:
* The owner endpoint must belong to the **same tenant and the same application** as the child endpoint.
* Deleting an owner endpoint **cascades**: every endpoint it owns is deleted along with it.
An owner endpoint you reference must already exist and be visible to agrirouter. Since endpoint state propagates asynchronously, the gateway retries resolving a freshly created owner for a few seconds before rejecting the request — see [Create or update endpoint](/api/putEndpoint#owner-parent-endpoint).
## External IDs [#external-ids]
Every endpoint is identified by an **external ID**: a string that uniquely identifies a specific software instance or machine. The external ID is provided by the application at creation time and is used for:
* **Re-creation after deletion**: creating the endpoint again with the same external ID after a previous deletion, to resume with the same identity
* **Identification**: correlating an agrirouter endpoint with the corresponding software instance or machine
The recommended format for external IDs is a **URN** (Uniform Resource Name), for example `urn:mycompany:my-app:user-12345`. It gives you global uniqueness and stays readable.
### Uniqueness Scope [#uniqueness-scope]
External ID uniqueness is scoped to the **tenant** (account). This means:
* The same external ID can be used in different accounts
* But an external ID cannot be used twice within the same account, even by different applications
See Tenant IDs for what a tenant is in agrirouter, how it relates to the other IDs in an integration, and how to pass the tenant ID on API calls.
## API Reference [#api-reference]
Endpoints are created, updated, and removed through two operations. Open them in the API playground to see the request and response shapes:
Create or update endpoint
Delete endpoint
# Concepts (/en/docs/concepts)
Understand the core concepts behind agrirouter: the ecosystem, endpoints, messaging, accounts, and security
These pages explain the core concepts of agrirouter. Whether you are building a farming software integration, a telemetry platform fleet manager, or connecting a communication unit, start here to understand how the system is structured before you start calling the API.
agrirouter is a universal data exchange platform for agriculture. It connects farming software, telemetry systems, and hardware devices so they can exchange standardized agricultural data regardless of manufacturer. The farmer or contractor stays in control of their data at all times, deciding exactly which data flows between which systems.
If you are new to agrirouter, read through these concept pages in order. Each page builds on the previous one.
Once you are comfortable with the concepts, move on to building your integration:
Follow the Getting Started tutorials for a hands-on walkthrough from registration to your first message.
Browse the Integration Guides for step-by-step instructions for each endpoint type.
# Messaging (/en/docs/concepts/messaging)
How messages flow through agrirouter: capabilities, subscriptions, routing, addressing, and the message lifecycle
Messaging is the core function of agrirouter. Every piece of agricultural data (task files, telemetry positions, device descriptions, images, and more) travels through the agrirouter messaging system. Understanding how messages flow is essential for building a working integration.
## Message Structure [#message-structure]
A message is a standard HTTP request. The parts of the request each carry a different kind of information:
* **URI** (path and query) addresses the API operation you are invoking.
* **Headers** carry request metadata as `x-agrirouter-*` parameters: the technical message type (`x-agrirouter-message-type`), addressing mode (`x-agrirouter-is-publish`), direct recipients (`x-agrirouter-direct-recipients`), sender identity, and other per-operation parameters, alongside standard HTTP headers for authentication.
* **Payload** (request body) carries the agricultural data itself: a task file, a device description, a GPS position stream, and so on. The technical message type in the headers tells agrirouter how to interpret the payload.
For the exact headers and payload schema per operation, see the [API Reference](/api).
## Capabilities [#capabilities]
Before an endpoint can participate in any data exchange, it must declare its **capabilities**: the technical message types it can send or receive. Capabilities are how you tell agrirouter "I can produce GPS data" or "I can receive task files."
Capabilities serve two purposes:
1. **They tell agrirouter what the endpoint can do.** The routing engine uses capabilities to validate that a message can actually be delivered to a recipient.
2. **They appear in the agrirouter UI.** The end user sees each endpoint's capabilities when configuring routes, so they know what data each endpoint supports.
### Declaring Capabilities [#declaring-capabilities]
Capabilities are provided when creating an endpoint via `PUT /endpoints/{externalId}` and can be updated at any time by sending the same request again. Each capability entry specifies:
* A **technical message type** (e.g., `iso:11783:-10:taskdata:zip`)
* A **direction**: send only, receive only, or send and receive
An endpoint without capabilities cannot send or receive any agricultural data.
Capabilities are required when creating the endpoint.
## Subscriptions [#subscriptions]
Subscriptions declare which **published** message types an endpoint wants to receive. Capabilities define what an endpoint *can* handle, subscriptions define what it *wants* to receive proactively. Subscriptions work like a newsletter opt-in: you tell agrirouter "notify me whenever anyone publishes GPS data", and only then will broadcast messages of that type be delivered to you.
Subscriptions only apply to the [publish addressing mode](#publishing). Directly addressed messages are delivered regardless of subscriptions (as long as a valid route and matching capabilities exist).
An endpoint should subscribe to every message type it can handle. Skip
subscription only if the endpoint must exclusively receive directly-addressed
messages, the narrow case where any published message would be noise.
Subscribing to everything you can receive is the default.
### How Subscriptions Work [#how-subscriptions-work]
1. Endpoint A declares a subscription for message type X
2. Endpoint B publishes a message of type X
3. The following conditions are checked: Is there a route from B to A? Does A have the capability to receive type X? Is A subscribed to type X?
4. If all three conditions are met, the message is delivered to A's feed
### Declaring Subscriptions [#declaring-subscriptions]
Subscriptions are included alongside capabilities in the `PUT /endpoints/{externalId}` request. Each subscription entry specifies:
* A **technical message type** to subscribe to
Each `PUT` request replaces all previous capabilities and subscriptions with the values in the request.
## Routing [#routing]
Routes are how the **end user** (farmer or contractor) controls data flow. They are configured in the agrirouter UI, not by the application. You do not create routes as a developer, but you need to understand them because they directly affect whether your messages are delivered.
A route specifies:
* A **source** endpoint
* A **destination** endpoint
* The **information types** that are allowed to flow from source to destination
Routes operate on **information types**: the semantic category of data being exchanged (for example, "task data" or "telemetry"). This is coarser than the technical message type, which identifies the exact payload format. One information type typically maps to several technical message types.
If no route exists between two endpoints for a given information type, the message will not be delivered, even if both endpoints have the correct capabilities and subscriptions.
### Default routes [#default-routes]
For endpoints whose routes are managed by agrirouter (the default setting), routes are created automatically, but only between **software endpoints** (`cloud_software`, its legacy alias `farming_software`, and legacy telemetry platforms) and **machine endpoints** (`communication_unit`, `virtual_communication_unit`). Whenever an endpoint of one kind is created in an account, routes in both directions are created to every existing endpoint of the other kind for any compatible information type.
No default routes are created between two software endpoints. If your application needs to exchange data with another cloud application, for example a farm management system, in the same account *without making use of Virtual Communication Units* the account owner has to create that route in the agrirouter UI. Whether the counterpart registered as `cloud_software` or as the legacy `farming_software` alias makes no difference; both are the same endpoint kind internally.
## Message Addressing [#message-addressing]
When sending a message, the application must choose how to address it. There are two addressing modes, and they can be combined. Direct addressing is like sending a letter to a specific person. Publishing is like posting a notice on a bulletin board that anyone interested can read.
### Direct Addressing [#direct-addressing]
The sender specifies one or more recipient endpoint IDs explicitly. The message is delivered to each specified recipient, provided:
* A route exists from sender to recipient for the information type
* The recipient has the capability to receive the message type
Direct addressing is used when the sender knows exactly who should receive the data, for example sending a task file to a specific machine.
This mode is most common for interactive, user-invoked messaging, where a user explicitly selects a recipient in the application's UI.
Your application discovers the endpoints it can directly address from the
gateway itself: call [`GET /tenants`](/api/listAuthorizedTenants) at startup
to bootstrap the per-tenant endpoint list, and react to the
[`ENDPOINTS_LIST_CHANGED`](/api/events/endpoints-list-changed) SSE event to
keep the local cache up to date without polling. To refresh a single tenant
out of band, call [`GET /tenants/{tenantId}/endpoints`](/api/listTenantEndpoints).
### Publishing [#publishing]
The sender does not specify recipients. Instead, the message is delivered to **all endpoints that are subscribed** to the message type, provided:
* A route exists from the sender to the subscriber for the information type
* The subscriber has the capability to receive the message type
* The subscriber has an active subscription for the message type
Publishing is used when the sender does not know or care who specifically will receive the data, for example a VCU publishing telemetry data that any interested software can pick up.
This mode is most common for non-interactive, automated processes, such as continuous telemetry streaming from machines.
### Combined Addressing [#combined-addressing]
A single message can use both modes simultaneously: publish to all subscribers **and** directly address additional specific recipients. This is useful when you want broad distribution plus guaranteed delivery to specific endpoints.
## Message Flow [#message-flow]
Knowing the full flow helps you design your integration correctly.
### Sender sends message [#sender-sends-message]
The application constructs a message with the payload, technical message type, and addressing information. It sends the message to agrirouter via the API.
### The message is received [#the-message-is-received]
The message arrives at agrirouter for processing.
### Routing engine processes the message [#routing-engine-processes-the-message]
The addressing mode, routes, recipient capabilities, and subscriptions are evaluated to determine which endpoints should receive the message.
### Message placed in recipient feeds [#message-placed-in-recipient-feeds]
For each valid recipient, agrirouter places a copy of the message in that endpoint's **feed**.
### Recipient receives the message [#recipient-receives-the-message]
The recipient application receives messages via a Server-Sent Events (SSE) stream. agrirouter opens a long-lived HTTP connection and pushes an event to the client as each message arrives in the feed.
### Recipient confirms the message [#recipient-confirms-the-message]
After processing a message, the recipient must **confirm** (acknowledge) it. This removes the message from the feed.
## Feed Management [#feed-management]
The feed stores messages that have been delivered to the recipient but not yet confirmed. After processing a message, the recipient must **confirm** (acknowledge) it to remove it from the feed.
## Server-Sent Events (SSE) [#server-sent-events-sse]
Applications receive messages by opening a long-lived `GET /events` connection. agrirouter pushes events to the client as they arrive; the gateway exposes no poll-based message retrieval alternative. The connection accepts a `types` query parameter to restrict the stream to specific event types.
## Chunking [#chunking]
agrirouter splits large message payloads into transport chunks of about 768 KB and reassembles them before delivery. This is transparent to the API: you send one logical message and the recipient receives one logical message, up to the per-request body limit of **256 MB**.
The chunking boundary is still visible outside the API: the endpoint detail view in the agrirouter UI lists each message the endpoint received, including chunk metadata, so a single chunked payload may show up as several entries there.
## Technical Message Types [#technical-message-types]
Every message in agrirouter has a **technical message type** (TMT) that identifies the format of the payload. TMTs are standardized across the platform so that all applications can interoperate.
TMT identifiers use a colon-delimited structure: `standard:part:category:encoding`. For example, `iso:11783:-10:taskdata:zip` means: ISO 11783 standard, part -10, task data category, ZIP-compressed encoding. The complete list of supported identifiers is in the [Message Types reference](/message-types).
Common categories include:
| Category | Examples |
| --------------- | ------------------------------------------------------------------------------ |
| **Task data** | ISO 11783 TaskData (task files for machines) |
| **Telemetry** | EFDI Device Descriptions (TeamSets), EFDI TimeLog (live telemetry) |
| **Positioning** | GPS position data |
| **Geospatial** | Shape files (field boundaries, application maps, as-applied/as-harvested maps) |
| **Documents** | PDF files, images (PNG, JPEG), video |
See the Message Types reference for the complete list of supported technical
message types and their identifiers.
## Common Failure Modes [#common-failure-modes]
Knowing what can go wrong helps you design a resilient integration from the start.
### No route to recipient [#no-route-to-recipient]
Capabilities are checked both when a route is created and again at send time. At route creation, agrirouter refuses to build a route for an information type if the sender's sending capabilities and the receiver's receiving capabilities do not intersect on it. If a recipient later trims its capabilities, existing routes are not removed automatically, but the send-time check filters them out, so a stale route behaves the same as no route at all. From the sender's perspective, you are only ever in one of two situations at send time: the message is routed, or it is not. You will never see a distinct "capability mismatch" failure.
The behavior when no route exists depends on the addressing mode:
#### Publish with no matching subscribers [#publish-with-no-matching-subscribers]
When you send a message in publish mode (`x-agrirouter-is-publish: true` with no direct recipients) and no subscribed endpoint is reachable by a route for that information type, agrirouter returns HTTP `200` and the message is dropped. This is intentional: publishing with nobody listening is a no-op, not an error. This is the most common cause of "missing messages": check that the end user has configured a route before assuming there is a bug in your code.
#### Direct send with unroutable recipients [#direct-send-with-unroutable-recipients]
When you send a message with direct recipients (`x-agrirouter-direct-recipients: `) and no route exists from your endpoint to one or more of the named recipients for that information type, agrirouter rejects the request with HTTP `400` and a human-readable `message` field in the JSON error body. Treat this as "the end user has not configured a route to one of the recipients you named". See the [Errors](/api/errors) reference for the full HTTP status code list.
## Communication Protocol [#communication-protocol]
agrirouter uses:
* **REST over HTTPS** for sending messages and managing endpoints (HTTP POST / PUT / DELETE on `/messages`, `/confirmations`, `/endpoints/{externalId}`)
* **Server-Sent Events over HTTPS** for receiving messages (`GET /events`)
MQTT is no longer used. All communication with agrirouter now happens over
HTTPS: REST for sending, SSE for receiving. If you previously bounced off
the integration because of MQTT, it is worth another look.
See the API Reference for endpoint specifications, request/response formats,
and authentication details.
## API Reference [#api-reference]
Open the operations that carry a message from sender to recipient in the API playground:
Send one or several messages
Receive events
Confirm received messages
# Tenant IDs (/en/docs/concepts/tenants)
How tenant IDs identify the end user account your application acts under, where to obtain them, and how to pass them in API requests
A **tenant ID** identifies the end user account that your application is acting under. Every endpoint your application creates belongs to exactly one tenant, and the tenant ID is the link between the client credentials your application holds and the specific end user account those credentials are operating on behalf of.
This page covers what a tenant is, how the tenant ID relates to other IDs in the system, where to obtain it, and how to use it.
## What a tenant is [#what-a-tenant-is]
In agrirouter, every account is a **tenant**. A tenant is the unit of data isolation: each end user (a farmer or contractor) owns a tenant, every endpoint belongs to exactly one tenant, and routes are configured within a tenant.
A single application can act across many tenants. Each end user that connects your application to their account shows up as a separate tenant from your application's perspective. Your application has to keep a list of the tenant IDs it has been granted access to and use the right one on every request.
For the broader account model and how tenants relate to applications and endpoints, see [Accounts & Tenants](/concepts/accounts-and-tenants).
## Tenant IDs and other IDs [#tenant-ids-and-other-ids]
A tenant ID is a UUID. Several other UUIDs appear in agrirouter integrations and they are easy to mix up:
| ID | What it identifies | Where you encounter it |
| ------------------- | ----------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| **Tenant ID** | The end user account your application is acting under | `x-agrirouter-tenant-id` request header, `tenant_id` field on endpoint and event payloads |
| **OAuth client ID** | Your application's identity for authentication | `client_id` parameter when requesting an access token |
| **Application ID** | The application registered in the developer portal | `application_id` field on the endpoint object |
| **Endpoint ID** | A specific endpoint inside a tenant | `id` field on the endpoint object, `x-agrirouter-endpoint-id` request header |
| **External ID** | An application-controlled identifier for an endpoint | `external_id` field on the endpoint object, path segment of `PUT /endpoints/{externalId}` |
The same tenant UUID appears in two places once an endpoint exists: as the value of the `x-agrirouter-tenant-id` header you send, and as the `tenant_id` field on the endpoint object returned by `PUT /endpoints/{externalId}`. Both refer to the same underlying tenant.
## How you obtain a tenant ID [#how-you-obtain-a-tenant-id]
For **end user accounts**, you can pick up the tenant ID through three complementary mechanisms. They all surface the same authorization, so you can mix them based on where in your application the next step is happening.
* **OAuth authorization callback** — when a user approves your application on the consent page, the browser is redirected back to your `redirect_uri` with the `tenant_id` query parameter set to the tenant the user selected. This is the natural source when a frontend continues straight into endpoint creation. See [Authorization Flow](/concepts/authorization-and-security#authorization-flow) for the full redirect contract.
```text
https://yourapp.com/callback?state={randomStateValue}&tenant_id={userTenantId}
```
* **`AUTHORIZATION_ADDED` SSE event** — delivered on the [`GET /events`](/api/receiveEvents) stream when the user grants the authorization, carrying the same `tenant_id` along with the granted scope. This is the natural source when a backend keeps a long-lived SSE connection and reacts to authorizations there, without taking a dependency on the redirect URL.
* **[`GET /tenants`](/api/listAuthorizedTenants)** — returns every tenant for which your application currently holds an authorization. This is the natural source for application startup, recovery, or any time you need to rebuild your view of authorized tenants from scratch.
There is no way to derive an end user's tenant ID from the access token; the access token is opaque.
Your application's **developer-account tenant ID** is a separate value. The agrirouter team issues it when your developer account is provisioned, and you use it to sign into the developer portal and manage your application record. It is not the tenant ID you pass in `x-agrirouter-tenant-id` on per-user API calls.
Tenant IDs are stable for the lifetime of the account and do not need to be refreshed.
## How you use a tenant ID [#how-you-use-a-tenant-id]
Pass the tenant ID as the `x-agrirouter-tenant-id` request header on every call that operates inside that tenant.
| Operation | Endpoint | Tenant header required |
| ---------------------------- | -------------------------------- | ----------------------------------------------------------------------------------------- |
| Create or update an endpoint | `PUT /endpoints/{externalId}` | yes |
| Delete an endpoint | `DELETE /endpoints/{externalId}` | yes |
| Send a message | `POST /messages` | yes |
| Confirm received messages | `POST /confirmations` | yes |
| Receive events | `GET /events` | no, the stream covers all endpoints across every tenant the application is authorized for |
Example:
```http
PUT /endpoints/my-external-id
x-agrirouter-tenant-id: 12345678-abcd-ef01-2345-6789abcdef01
Authorization: Bearer ...
Content-Type: application/json
```
Event payloads delivered through `GET /events` carry a `tenant_id` field on the event data, so a multi-tenant application can dispatch each incoming event to the right tenant context.
## What not to do [#what-not-to-do]
Do not attempt to decode, verify, or introspect the access token to extract a tenant ID or any other information. The token format is not part of the public API and may change without notice.
Treat the access token as opaque. The tenant ID is not encoded in it, and the token's shape, claims, and signing algorithm are implementation details that may change without notice. Code that reaches into the token to pull out user or tenant information will eventually break.
For the full guidance on access token handling, see [Access Tokens in Authorization & Security](/concepts/authorization-and-security#access-tokens).
Read Accounts & Tenants for the broader account and tenant model.
## API Reference [#api-reference]
The tenant ID header is required on every operation below. Open them in the API playground to see the header in context:
Create or update endpoint
Delete endpoint
Send one or several messages
Confirm received messages
# Tools & SDKs (/en/docs/tools)
Developer tools and SDKs for building and testing agrirouter integrations
The API spec alone is not enough to get an integration running. This section covers the developer tools and SDKs we provide for building, testing, and debugging against agrirouter.
The most feature-complete testing tool for agrirouter integrations. Send and receive files, simulate telemetry, and debug data exchange.
SDKs for Java, C#/.NET, PHP, Python, and C++. These SDKs target the legacy agrirouter API only.
Download the current API contract as YAML or JSON for client generators and API tooling.
# IO-Tool (/en/docs/tools/io-tool)
The IO-Tool, a developer tool for testing agrirouter integrations with data exchange, simulation, and debugging
The IO-Tool is the most feature-complete developer tool for testing agrirouter integrations. From a web UI, you can send and receive files, simulate telemetry, debug message exchange, and exercise all the major agrirouter data exchange concepts.
The IO-Tool is a **developer tool** for testing and debugging. It is **not** part of the agrirouter platform itself and is not intended for production use.
* **URL**: [https://io.my-agrirouter.com/](https://io.my-agrirouter.com/)
* **Registration**: Not self-service. To request an account, write to .
## Features [#features]
* Send and receive files in all supported agrirouter formats
* Send and receive telemetry data (EFDI)
* Test data exchange concepts such as routing, publishing, and subscriptions
* Debug integrations with detailed message logs
* Simulate endpoints and machine telemetry
## Getting Started [#getting-started]
### Request an account [#request-an-account]
Write to requesting access to the IO-Tool. You will receive credentials once your account is provisioned.
### Log in [#log-in]
Navigate to [https://io.my-agrirouter.com/](https://io.my-agrirouter.com/) and sign in with the credentials you received.
{/* TODO: SCREENSHOT */}
### Connect to agrirouter [#connect-to-agrirouter]
Go to **Settings** and choose the environment you want to connect to. **MQTT Production** is the right choice for most cases.
{/* TODO: SCREENSHOT */}
### Choose the correct environment [#choose-the-correct-environment]
The IO-Tool environment must match the environment your application is registered in, which for partner integrations is Production. Only if DKE has directed you to the QA environment, connect the IO-Tool to QA as well. Endpoints across environments cannot talk to each other.
### Select MQTT or REST [#select-mqtt-or-rest]
Pick MQTT. It is faster and bidirectional. REST requires polling and is slower for testing workflows.
### Complete the onboarding process [#complete-the-onboarding-process]
After you pick environment and protocol, the IO-Tool redirects to the agrirouter login page. Sign in, confirm the connection, and wait for setup to finish.
{/* TODO: SCREENSHOT */}
### Explore the IO-Tool [#explore-the-io-tool]
Once onboarding completes, five menu items appear: **Data Storage**, **Endpoints**, **Simulation**, **Logging**, and **Settings**.
## Capabilities and Subscriptions [#capabilities-and-subscriptions]
After onboarding, set your capabilities and subscriptions under **Settings**. Capabilities define which message types the IO-Tool endpoint can send and receive. Subscriptions define which message types it actively listens for.
Sending a new capabilities message clears all existing subscriptions. Re-send your subscriptions after updating capabilities.
{/* TODO: SCREENSHOT */}
## Data Storage [#data-storage]
The Data Storage section lets you upload, organize, and exchange files through agrirouter.
* **Upload files** via drag-and-drop (maximum 15 MB per file)
* Organize files by technical message type
* **Send** files directly to specific endpoints or **publish** them to all subscribers
* **Receive** files from other endpoints
* Built-in ISOXML validation for TaskData files
{/* TODO: SCREENSHOT */}
## Endpoints [#endpoints]
The Endpoints view shows all reachable endpoints and their capabilities. From this view you can:
* See which endpoints are available for communication
* Inspect the capabilities of each endpoint
* Send files directly to a specific endpoint
{/* TODO: SCREENSHOT */}
## Simulation [#simulation]
Simulation replays machine telemetry from ISOXML or EFDI data. Use it to test how your application handles live telemetry streams.
Available simulation options:
* **Endless loop**: Repeat the telemetry data continuously
* **Replace time**: Substitute original timestamps with current time
* **Skip initial**: Skip the device description at the beginning
* **Interval**: Set the time between telemetry messages
* **Scale**: Adjust playback speed
* **DDI details**: Inspect the Data Dictionary Identifiers in the telemetry data
{/* TODO: SCREENSHOT */}
## Logging [#logging]
Logging gives you detailed message logs for debugging. You can:
* Filter logs by message type, timestamp, or keyword
* Enable **trace logging** for detailed protocol-level information (available for 24 hours after activation)
* Inspect individual message payloads and headers
{/* TODO: SCREENSHOT */}
## Telemetry MapView [#telemetry-mapview]
The MapView displays EFDI and GPS telemetry data on an interactive map. You can:
* Visualize device positions and movement paths
* Inspect telemetry values at specific positions
* Download telemetry data as EFDI or ISOXML format
{/* TODO: SCREENSHOT */}
## Troubleshooting [#troubleshooting]
| Problem | Solution |
| -------------------- | ------------------------------------------------------------------------------------------------------ |
| UI not responding | Log out and log back in to refresh the session |
| Cannot send messages | Reconnect the MQTT connection in Settings |
| Onboarding errors | Check that you picked the correct environment and that your agrirouter account has endpoint slots left |
| Upload errors | Check that the file is under 15 MB and matches a supported format |
## Offboarding [#offboarding]
To disconnect the IO-Tool from agrirouter, use the offboarding option in **Settings**. This removes the IO-Tool endpoint from your agrirouter account and deletes all associated data.
Offboarding permanently deletes the endpoint. To reconnect, you have to onboard again.
## Next steps [#next-steps]
# SDKs (/en/docs/tools/sdks)
SDKs for integrations against the legacy API (Java, C#/.NET, PHP, Python, C++)
The SDKs on this page target the **legacy agrirouter API** only (MQTT and REST with protobuf envelopes). They do not work against the current API.
The current API is a small REST+SSE surface, meant to be consumed directly or through a client generated from the [OpenAPI specification](/api#openapi-specification) with a code-generation tool from your language's ecosystem. Thin SDKs for the current API may show up later. None exist today.
The SDKs below are for partners maintaining integrations against the legacy API. For context on which API surface they cover, see the [Legacy API reference](/api/legacy) and the [migration guide](/appendix/migrating-from-legacy).
## Available SDKs [#available-sdks]
| Language | Status | Repository |
| --------- | --------------------- | -------------------------------------------------------------------------------------------- |
| Java | Official | [agrirouter-sdk-java](https://github.com/DKE-Data/agrirouter-sdk-java) |
| C# / .NET | Official | [agrirouter-sdk-dotnet-standard](https://github.com/DKE-Data/agrirouter-sdk-dotnet-standard) |
| PHP | Official | [agrirouter-sdk-php](https://github.com/DKE-Data/agrirouter-sdk-php) |
| Python | Official | [agrirouter-sdk-python](https://github.com/DKE-Data/agrirouter-sdk-python) |
| C++ | Community (3rd party) | [agrirouter-sdk-cpp](https://github.com/DKE-Data/agrirouter-sdk-cpp) |
Feature coverage differs between SDKs. If something is missing, open a pull request or reach out to DKE Data.
## Choosing an SDK [#choosing-an-sdk]
Pick the SDK that matches your stack. All official SDKs cover the core legacy-API workflows: onboarding, capability management, sending messages, and retrieving them. The C++ SDK is maintained by a third party and is not officially supported by DKE Data.
## Using the Current API Without an SDK [#using-the-current-api-without-an-sdk]
For the current API, generate a client from the hosted OpenAPI specification with a tool from your language's ecosystem (for example, `openapi-generator` or `oapi-codegen`). Use for YAML or for JSON.
The current API is a small REST surface (four endpoints plus an SSE stream), so partners often skip the generator entirely and hit it directly with the standard HTTP client from their language.
API Reference
# Authorization added (/en/docs/api/events/authorization-added)
SSE event emitted when a user grants your application an authorization for a tenant
```text
event: AUTHORIZATION_ADDED
```
Emitted when a user grants your application an authorization for a tenant. Use it as the server-side counterpart to the OAuth consent redirect, so that long-lived backends can react to new authorizations without parsing the redirect URL on the user-facing side.
An authorization is uniquely identified by the triple `(tenant_id, application_id, scope)`. The `application_id` is implicit from the authenticated subscription on the SSE connection; `tenant_id` and `scope` are explicit on the event. At present, the only scope in use is `endpoints:manage`. Additional scopes may be introduced in future revisions; if so, treat authorizations with different scopes as distinct, even when `tenant_id` matches.
## Data fields [#data-fields]
| Field | Type | Required | Description |
| ------------ | ------------------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `event_type` | string (`AUTHORIZATION_ADDED`) | yes | Discriminator; matches the `event:` line. |
| `tenant` | `TenantInfo` | yes | The tenant for which the authorization was added, with its currently visible endpoints. |
| `scope` | string | yes | The OAuth scope granted. Today the only value in use is `endpoints:manage`. |
| `state` | string | no | Echoes the `state` you supplied on the [authorization request](/getting-started/your-first-endpoint), so your backend can correlate this event with the customer that started the flow. Absent when no `state` was supplied. |
`tenant.endpoints` follows the same privacy rule as elsewhere in the API: until your application has at least one of its own endpoints in this tenant, the array is empty even if other endpoints exist there.
Because `state` is echoed both on the consent redirect and on this event, a backend that never sees the redirect can still map the authorization to the customer it belongs to. Treat it as opaque: agrirouter stores and returns it unchanged, and never interprets it.
## Sample frame [#sample-frame]
```text
event: AUTHORIZATION_ADDED
data: {"event_type":"AUTHORIZATION_ADDED","tenant":{"tenant_id":"12345678-abcd-ef01-2345-6789abcdef01","endpoints":[]},"scope":"endpoints:manage","state":"c3RhdGUtZnJvbS15b3VyLWFwcA"}
```
## See also [#see-also]
# Authorization revoked (/en/docs/api/events/authorization-revoked)
SSE event emitted when a user revokes your application's authorization for a tenant
```text
event: AUTHORIZATION_REVOKED
```
Emitted when a user revokes your application's authorization for a tenant. By the time you receive this event, your access to the target tenant for the given scope is already gone. Treat it as a cleanup signal: drop locally cached state for the tenant and stop calling APIs scoped to it.
All endpoints that were previously accessible via this authorization are removed as part of the revocation. You will receive a separate [`ENDPOINT_DELETED`](/api/events/endpoint-deleted) event for each one.
Authorizations are uniquely identified by the triple `(tenant_id, application_id, scope)`; only the authorization matching the `scope` below was revoked. At present the only scope in use is `endpoints:manage`, but if additional scopes are introduced later, other scopes for the same tenant remain in effect.
## Data fields [#data-fields]
| Field | Type | Required | Description |
| ------------ | -------------------------------- | -------- | --------------------------------------------------------------------------------------------------------- |
| `event_type` | string (`AUTHORIZATION_REVOKED`) | yes | Discriminator; matches the `event:` line. |
| `tenant_id` | UUID | yes | The tenant whose authorization was revoked. |
| `scope` | string | yes | The OAuth scope of the authorization that was revoked. Today the only value in use is `endpoints:manage`. |
## Sample frame [#sample-frame]
```text
event: AUTHORIZATION_REVOKED
data: {"event_type":"AUTHORIZATION_REVOKED","tenant_id":"12345678-abcd-ef01-2345-6789abcdef01","scope":"endpoints:manage"}
```
## See also [#see-also]
# Endpoint deleted (/en/docs/api/events/endpoint-deleted)
SSE event emitted when one of the application's endpoints is deleted
```text
event: ENDPOINT_DELETED
```
Emitted when one of the application's endpoints is deleted, either by the application itself calling [`DELETE /endpoints/{externalId}`](/api/deleteEndpoint) or by the account owner removing it from the agrirouter UI. Clean up any local state tied to the endpoint on this event.
## Data fields [#data-fields]
| Field | Type | Required | Description |
| ------------- | --------------------------- | -------- | -------------------------------------------------------------------- |
| `event_type` | string (`ENDPOINT_DELETED`) | yes | Discriminator; matches the `event:` line. |
| `id` | UUID | yes | agrirouter-internal ID of the deleted endpoint. |
| `external_id` | string | yes | The external ID the application supplied when creating the endpoint. |
## Sample frame [#sample-frame]
```text
event: ENDPOINT_DELETED
data: {"event_type":"ENDPOINT_DELETED","id":"9f8e7d6c-5b4a-3210-fedc-ba0987654321","external_id":"urn:my-app:endpoint:42"}
```
## See also [#see-also]
# Endpoints list changed (/en/docs/api/events/endpoints-list-changed)
SSE event emitted when the set of endpoints visible to the application in a tenant changes
```text
event: ENDPOINTS_LIST_CHANGED
```
Emitted when the set of endpoints visible to your application in a tenant changes, or when a visible endpoint's capabilities or routes change. Use it to keep a local cache of tenant endpoints up to date without polling.
This event only fires once your application has at least one endpoint of its own in the tenant. Until that first own endpoint exists, changes to other endpoints in the tenant are not reported, even when an authorization is in place. To bootstrap or refresh the cache outside of this event, call [`GET /tenants`](/api/listAuthorizedTenants) or [`GET /tenants/{tenantId}/endpoints`](/api/listTenantEndpoints).
## Data fields [#data-fields]
| Field | Type | Required | Description |
| ------------ | --------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `event_type` | string (`ENDPOINTS_LIST_CHANGED`) | yes | Discriminator; matches the `event:` line. |
| `tenant_id` | UUID | yes | The tenant whose endpoint list changed. |
| `endpoints` | array of `TenantEndpointInfo` | yes | Complete current list of endpoints in the tenant that are visible to the application. Replaces any cached list, do not merge. |
Each `endpoints[]` entry carries `id`, `external_id`, `name`, `endpoint_type`, `application_id`, `tenant_id`, `owned_by_your_application`, `capabilities` (`can_send` / `can_receive`), and, for endpoints owned by your application, `routed_endpoints` (`can_send_to` / `can_receive_from` maps of agrirouter endpoint ID to message types). See [`GET /tenants/{tenantId}/endpoints`](/api/listTenantEndpoints) for the field-by-field schema.
## Sample frame [#sample-frame]
```text
event: ENDPOINTS_LIST_CHANGED
data: {"event_type":"ENDPOINTS_LIST_CHANGED","tenant_id":"12345678-abcd-ef01-2345-6789abcdef01","endpoints":[{"id":"9f8e7d6c-5b4a-3210-fedc-ba0987654321","external_id":"urn:my-app:endpoint:42","name":"FarmApp user-12345","endpoint_type":"cloud_software","application_id":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","tenant_id":"12345678-abcd-ef01-2345-6789abcdef01","owned_by_your_application":true,"capabilities":{"can_send":["iso:11783:-10:taskdata:zip"],"can_receive":["iso:11783:-10:taskdata:zip"]},"routed_endpoints":{"can_send_to":{"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee":["iso:11783:-10:taskdata:zip"]},"can_receive_from":{}}}]}
```
## See also [#see-also]
# File received (/en/docs/api/events/file-received)
SSE event emitted when agrirouter has finished reassembling a chunked payload
```text
event: FILE_RECEIVED
```
Emitted when agrirouter has finished reassembling a chunked payload (TaskData, Shape, PDF, image, video). The gateway hides individual chunks from partners, so only one `FILE_RECEIVED` event fires per complete payload. The frame arrives on the [`GET /events`](/api/receiveEvents) SSE stream.
## Data fields [#data-fields]
| Field | Type | Required | Description |
| ----------------------- | ------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `event_type` | string (`FILE_RECEIVED`) | yes | Discriminator; matches the `event:` line. |
| `receiving_endpoint_id` | UUID | yes | agrirouter-internal ID of the receiving endpoint. |
| `message_type` | string | yes | Technical message type of the reassembled payload. |
| `size` | integer (bytes) | yes | Total size of the reassembled payload. |
| `message_ids` | array of UUIDs | yes | agrirouter message IDs of every chunk that carried the payload. **All** of these must be confirmed via [`POST /confirmations`](/api/confirmMessages) to acknowledge the file — see [Confirming the file](#confirming-the-file). |
| `payload` | string (base64) | no | Inline payload. Mutually exclusive with `payload_uri`. |
| `payload_uri` | URI | no | Pre-signed URL to download the reassembled payload. Mutually exclusive with `payload`. Expires after at most 15 minutes. |
| `filename` | string | no | Optional filename metadata supplied by the sender. |
| `tenant_id` | string | no | Tenant that owns the receiving endpoint. |
| `teamset_context_id` | string | no | Teamset context ID the sender attached, if any. |
`payload_uri` expires after at most 15 minutes. Download the file before the link expires. If it does expire, reconnect to the SSE stream so the event is replayed with a fresh URL.
## Confirming the file [#confirming-the-file]
A `FILE_RECEIVED` event represents a single logical file, but on the wire it may be a concatenation of several chunks the sender produced — for example when the original payload exceeded agrirouter's per-message size limit and was split, or when the sender intentionally streamed the file in pieces. Each chunk has its own agrirouter message ID, and all of those IDs are listed in `message_ids`.
To acknowledge the file, your application must confirm **every** message ID in `message_ids` via [`POST /confirmations`](/api/confirmMessages), each paired with the same `receiving_endpoint_id`. The confirmations may be sent in a single request or split across several — agrirouter only considers the file fully confirmed once all chunk IDs have been confirmed for the receiving endpoint.
## Sample frame [#sample-frame]
```text
event: FILE_RECEIVED
data: {"event_type":"FILE_RECEIVED","receiving_endpoint_id":"9f8e7d6c-5b4a-3210-fedc-ba0987654321","message_type":"iso:11783:-10:taskdata:zip","size":12582912,"message_ids":["a1b2c3d4-e5f6-4789-abcd-ef0123456789","b2c3d4e5-f6a7-4890-bcde-f01234567890"],"payload_uri":"https://s3.eu-central-1.amazonaws.com/prod-agrirouter-file-payloads/a1b2c3d4-e5f6-4789-abcd-ef0123456789?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=900&X-Amz-Signature=...","filename":"field-42-task.zip"}
```
## See also [#see-also]
# Message received (/en/docs/api/events/message-received)
SSE event emitted when a non-chunked message arrives in the feed of one of the application's endpoints
```text
event: MESSAGE_RECEIVED
```
Emitted when a non-chunked message arrives in the feed of one of the application's endpoints. The frame arrives on the [`GET /events`](/api/receiveEvents) SSE stream.
## Data fields [#data-fields]
| Field | Type | Required | Description |
| ----------------------- | --------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `event_type` | string (`MESSAGE_RECEIVED`) | yes | Discriminator; matches the `event:` line. |
| `id` | UUID | yes | agrirouter-generated message ID. This is the field to send as `message_id` when confirming via [`POST /confirmations`](/api/confirmMessages) — see [Confirming the message](#confirming-the-message). |
| `app_message_id` | string | yes | Sender-side message identifier, kept for troubleshooting together with the agrirouter team. For messages sent through the current API it is generated by agrirouter; legacy senders supply their own value. It is not guaranteed to be a UUID and must not be used as a stable or unique identifier. |
| `message_type` | string | yes | Technical message type, for example `iso:11783:-10:taskdata:zip`. |
| `sent_at` | RFC 3339 date-time | yes | When the sending application called `POST /messages`. |
| `receiving_endpoint_id` | UUID | yes | agrirouter-internal ID of the receiving endpoint. Pair it with `id` when confirming. |
| `received_at` | RFC 3339 date-time | no | When agrirouter accepted the message. |
| `payload` | string (base64) | no | Inline payload for small messages. Mutually exclusive with `payload_uri`. |
| `payload_uri` | URI | no | Pre-signed URL to download the payload. Mutually exclusive with `payload`. Expires after at most 15 minutes. |
| `filename` | string | no | Optional filename metadata supplied by the sender. |
| `tenant_id` | string | no | Tenant that owns the receiving endpoint. Useful when confirming back on behalf of a user. |
| `teamset_context_id` | string | no | Teamset context ID the sender attached via the `x-agrirouter-teamset-context-id` header, if any. |
Exactly one of `payload` and `payload_uri` is present per event.
## Confirming the message [#confirming-the-message]
Once your application has successfully processed the payload, confirm the message via [`POST /confirmations`](/api/confirmMessages) so agrirouter marks it as handled for the receiving endpoint and removes it from the feed. The confirmation body uses two fields from this event:
* Pass the event's `id` (the agrirouter-generated message ID) as the `message_id` of the confirmation. This is the only identifier agrirouter recognises here — do **not** use `app_message_id`, which is not unique on the receiving side.
* Pass the event's `receiving_endpoint_id` as the `endpoint_id` of the confirmation. The same message can be delivered to multiple endpoints in the tenant; each receiving endpoint must confirm independently.
## Sample frame [#sample-frame]
```text
event: MESSAGE_RECEIVED
data: {"event_type":"MESSAGE_RECEIVED","id":"e4f5a6b7-c8d9-0123-4567-89abcdef0123","app_message_id":"3c1f7d2e-9a4b-4c6d-8e5f-0a1b2c3d4e5f-1","message_type":"iso:11783:-10:taskdata:zip","sent_at":"2026-03-20T10:30:00Z","received_at":"2026-03-20T10:30:01Z","receiving_endpoint_id":"9f8e7d6c-5b4a-3210-fedc-ba0987654321","payload_uri":"https://s3.eu-central-1.amazonaws.com/prod-agrirouter-message-payloads/inbox/2026/03/20/10/30/e4f5a6b7-c8d9-0123-4567-89abcdef0123?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=900&X-Amz-Signature=..."}
```
## See also [#see-also]
# Shape (/en/docs/message-types/shape)
ESRI Shape file exchange for field boundaries, prescription maps and field operations
| Property | Value |
| -------------------------- | ------------------------ |
| **Technical Message Type** | `shp:shape:zip` |
| **Information Type** | Shape |
| **Format** | ZIP (binary) |
| **Protobuf Schema** | None (binary zip format) |
## Overview [#overview]
The Shape message type carries ESRI Shapefiles through agrirouter. Shapefiles are a widely-used geospatial vector format for field boundaries, application/prescription maps, and other geographic features relevant to agricultural operations.
The format follows the [ESRI Shapefile specification](https://www.esri.com/content/dam/esrisites/sitecore-archive/Files/Pdfs/library/whitepapers/pdfs/shapefile.pdf).
## Data Format [#data-format]
Shape data is transferred as a **zip file** containing the shapefile components (`.shp`, `.shx`, `.dbf`, and optionally `.prj` and other sidecar files). Send the raw zip bytes as the request body with `Content-Type: application/octet-stream`. Do not Base64-encode the payload yourself; agrirouter applies transport encoding internally for chunked message types.
## Chunking [#chunking]
The API handles chunking for you. If a Shape zip file exceeds the internal chunk size, the API splits it on the way out and the recipient gets the reassembled file as a single `FILE_RECEIVED` event. No manual chunking or reassembly on your side.
## Use Cases [#use-cases]
* **Work records**: results of operations performed on the field as measured by the machine, e.g., yield monitor data, soil sensor data, etc.
* See also [Work Records Shape](/message-types/shape/work-records-shape) for more specific format.
* **Field boundaries**: defining the geographic extent of agricultural fields
* **Application maps**: variable-rate prescription maps for seeding, fertilizing, or spraying
* **Yield zone maps**: spatial representations of yield data
* **Management zones**: soil or performance-based zones within a field
## Libraries [#libraries]
Most languages have a shapefile library, so reading and writing is rarely a blocker. Common options: GDAL/OGR, GeoTools (Java), Shapely (Python), NetTopologySuite (.NET).
## Next steps [#next-steps]
# Work Records Shape (/en/docs/message-types/shape/work-records-shape)
ESRI Shape file exchange for work records
[ESRI Shapefile](https://www.esri.com/content/dam/esrisites/sitecore-archive/Files/Pdfs/library/whitepapers/pdfs/shapefile.pdf) is a generic geospatial vector data format.
Work record is a recorded operation on a field, e.g., a seeding, fertilizing, spraying or harvesting operation.
This documentation is a reference for agricultural-specific shapefile bundles recommended to use for `agrirouter`-connected applications when passing work records.
## Data Format [#data-format]
### Work record bundle layout [#work-record-bundle-layout]
The bundle is a zip archive describing one work record. It holds up to two
layers, each an ordinary ESRI shapefile — a set of four files sharing a basename:
```
.shp # point-level layer — geometry
.shx # point-level layer — shape index
.dbf # point-level layer — attribute table
.prj # point-level layer — coordinate reference system
summary.shp # summary layer — field boundary geometry
summary.shx # summary layer — shape index
summary.dbf # summary layer — attribute table
summary.prj # summary layer — coordinate reference system
```
A real bundle, for an application operation would look like this:
```
dd67f8c0-632c-43c1-8183-5f095b6bd699.dbf
dd67f8c0-632c-43c1-8183-5f095b6bd699.shp
dd67f8c0-632c-43c1-8183-5f095b6bd699.shx
dd67f8c0-632c-43c1-8183-5f095b6bd699.prj
summary.dbf
summary.shp
summary.shx
summary.prj
```
* **Point-level layer** — one feature per recorded sample, carrying the
as-applied detail of the operation. Its basename is the work record id,
sanitized to ASCII. Always present.
* **Summary layer** — always named `summary`; one polygon feature per
product/component, carrying field-level aggregate attributes. Present only when
a usable field boundary is available for the operation; without one the bundle
carries just the point-level layer's four files.
The two layers can be read independently, and the entry order within the zip is
not significant.
**Flat archive.** All files sit at the **root** of the zip — there is no
enclosing directory.
**Column naming.** DBF column names are uppercase in every layer, so the
published schema uses one consistent casing throughout the bundle.
### Summary layer [#summary-layer]
The `summary` layer carries the field boundary polygon together with a row of
field-level summary attributes aggregated from the operation's measurements.
There is **one row per applied product/component**: a single-product operation
yields one row, while a tank mix repeats the same boundary geometry once per
component. Operation-level columns (field, areas, speed, fuel, …) repeat across
rows; the per-product material columns differ from row to row.
Conventions:
* **All numeric values are metric**, and each `*_U` column carries the unit of
the value next to it (e.g. `kg1ha-1`, `l1ha-1`, `seeds1ha-1`).
* `PRODUCT` and `CROP` are mutually exclusive on a row: an applied input fills
`PRODUCT`, while a seeding/harvest cultivar fills `CROP`.
| Column | Type | Meaning | Example | Enumeration |
| ------------ | ------- | ----------------------------------------------------------------------------------------------------------------- | ---------------------- | ---------------------------------------------- |
| `FIELD` | C(64) | Field name. | `North 40` | |
| `OPERATION` | C(32) | Operation type — Application, Seeding, Harvest or Tillage. | `Application` | `Application`, `Seeding`, `Harvest`, `Tillage` |
| `CROPSEASON` | C(8) | Crop season year. | `2025` | |
| `PRODUCT` | C(64) | Applied input/product name for this row (Application; one per tank-mix component). Blank for seeding/harvest. | `UAN 32%` | |
| `CROP` | C(64) | Cultivar/variety name for this row (Seeding/Harvest, e.g. "Corn"). Blank for Application. | `Corn` | |
| `STARTDATE` | C(24) | Operation start timestamp (ISO-8601, UTC). | `2025-04-12T08:30:00Z` | |
| `ENDDATE` | C(24) | Operation end timestamp (ISO-8601, UTC). | `2025-04-12T14:05:00Z` | |
| `FIELDAREA` | F(19,4) | Whole-field boundary area, in hectares. | `16.1880` | |
| `COVAREA` | F(19,4) | Covered/worked area for the operation, in hectares. | `15.9420` | |
| `TOTMAT` | F(19,4) | Total material for this product — total applied/seeded amount, or for harvest the total yield (volume). | `1814.3700` | |
| `TOTMAT_U` | C(12) | Unit of `TOTMAT`. | `kg` | `kg`, `l`, `seeds` \* |
| `AVGMAT` | F(19,6) | Average material rate for this product — applied/seeded rate per area, or for harvest the average yield per area. | `113.812340` | |
| `AVGMAT_U` | C(12) | Unit of `AVGMAT`. | `kg1ha-1` | `kg1ha-1`, `l1ha-1`, `seeds1ha-1` \* |
| `TGTRATE` | F(19,6) | Planned/target average rate for this product, when present. | `112.000000` | |
| `TGTRATE_U` | C(12) | Unit of `TGTRATE`. | `kg1ha-1` | `kg1ha-1`, `l1ha-1`, `seeds1ha-1` \* |
| `AVGSPEED` | F(19,4) | Average vehicle/ground speed for the operation (km/h). | `9.6500` | |
| `AVGDEPTH` | F(19,4) | Average working depth (Tillage), in metres. | `0.0762` | |
| `FUEL` | F(19,4) | Total fuel consumed by the operation (litres). | `48.2000` | |
| `WETMASS` | F(19,4) | Harvest only — total wet mass for this variety. | `21772.4400` | |
| `WETMASS_U` | C(12) | Unit of `WETMASS`. | `kg` | `kg` \* |
| `AVGWETM` | F(19,6) | Harvest only — average wet mass per area for this variety. | `1365.840000` | |
| `AVGWETM_U` | C(12) | Unit of `AVGWETM`. | `kg1ha-1` | `kg1ha-1` \* |
| `MOIST` | F(19,4) | Harvest only — average grain/crop moisture (%). | `18.5000` | |
The Example column shows one plausible value per column; the examples are
illustrative per cell, not a single coherent row (some columns are
Application-only, others Harvest-only, and `PRODUCT`/`CROP` never appear
together).
The Enumeration column lists the complete set of values a column can take when
that set is known in advance, and is left empty otherwise. Note that enumerations by design
are not always exhaustive: a value may be provided by exporting system that does not
appear in the list of this specification yet, in which case it is up to consumer
to handle this gracefully.
`*` A unit token outside the listed set is possible but not expected; see *Unit
tokens* below.
### Unit tokens [#unit-tokens]
Units are written as compact tokens rather than symbols: a token is a product of
factors, where `1` separates factors and a trailing negative exponent marks a
denominator. So `kg1ha-1` is kilograms per hectare, `l1ha-1` litres per hectare,
`km1hr-1` kilometres per hour, `kg1m3-1` kilograms per cubic metre, `ml1kg-1`
millilitres per kilogram.
The bundle is metric: every unit token names a metric unit, and every numeric
value is expressed in the unit its token names. The vocabulary is
| Dimension | Token |
| --------------- | ------------ |
| Length | `m` |
| Area | `ha` |
| Mass | `kg` |
| Volume | `l` |
| Seed count | `seeds` |
| Speed | `km1hr-1` |
| Mass per area | `kg1ha-1` |
| Volume per area | `l1ha-1` |
| Seeds per area | `seeds1ha-1` |
| Pressure | `kpa` |
| Mass per volume | `kg1m3-1` |
| Volume per mass | `ml1kg-1` |
| Energy per mass | `mj1kg-1` |
| Bales per area | `bales1ha-1` |
| Proportion | `percent` |
| Angle | `deg` |
| Temperature | `°C` |
Like the column enumerations, this vocabulary is not closed. A producer that
cannot express a value in metric may emit it in its original unit, carrying that
unit's token verbatim — so a consumer may encounter a token outside the table,
and a value that is not metric. This is the one case where the "all values are
metric" rule does not hold, and it is not expected in practice. A consumer should
read the unit token rather than assume one and handle an unknown token gracefully.
### Point-level layer [#point-level-layer]
The point-level layer is the as-applied detail of the operation: one feature per
sample recorded as the machine moved through the field, with a row of attributes
describing what happened at that spot. The geometry is a point, or a small
polygon for area-based machines. This is the operation's main shapefile, named
after the operation id; the summary layer sits beside it in the same zip.
Unlike the summary layer, this layer has no fixed column set. Which columns are
present depends on the operation type — Application, Seeding, Harvest or
Tillage — and on the machine that recorded the operation. The tables below list
every column that can appear, grouped by the operation type it belongs to. Treat
the set as open: select columns by name, and expect neither every listed column
to be present nor the listed ones to be all there are.
Rules that hold for the whole layer:
* **Uppercase names.** Column names are uppercase.
* **Metric values.** Every numeric value is in the metric unit given for its
column in the tables below.
* **Timestamp.** `TIME` is the first attribute of every record, RFC3339 in UTC
(see *Timestamp* below).
* **Crop.** For Seeding/Harvest, `CROP` holds the crop **name** as text (see
*Crop name* below).
* **Product width.** `PRODUCT` is at least `C(64)` wide, matching the summary
layer (see *Product width* below).
In the tables, **Unit** is the unit of the published value; `—` marks a column
that carries no unit (text, id, timestamp). **Type** is the column's nominal
type, which is not always the DBF field definition emitted — see below.
#### Column types [#column-types]
The **Type** in the tables below is nominal: it tells you what kind of value the
column holds and roughly how big — text of some width, a small integer, a decimal
number of a given precision. It is not a guarantee of the exact DBF field
definition on disk, which may differ in width and decimal count.
What does hold:
* A column's **kind** is as stated: a column typed `Character(n)` holds text, and
a column typed `Number(n,d)` or `Double` holds a number. Only widths and
decimal counts may differ.
* Numeric columns are emitted as DBF numeric (`N`) or float (`F`) fields; both
hold a decimal number written as text, and both should be parsed the same way.
* Character widths are **floors, not fixed sizes**: a column may be emitted wider
than its nominal type, never narrower. `PRODUCT` is at least `C(64)` (see
*Product width* below).
A consumer should read field definitions from the DBF header rather than hardcode
them from these tables, and should not assume a column's width is stable across
bundles.
#### Common columns [#common-columns]
Present across operation types (exact membership still depends on the machine and
operation):
| Column | Type | Unit | Meaning |
| ------------- | ------------- | ------------------------ | ------------------------------------------------------------------------------------------ |
| `TIME` | Character(30) | — | Sample timestamp, RFC3339 in UTC (e.g. `2021-04-28T11:49:31.714Z`). See *Timestamp* below. |
| `HEADING` | Number(18,8) | deg (0 = magnetic north) | Direction of travel. |
| `DISTANCE` | Number(18,8) | m | Distance travelled since the previous sample. |
| `SWATHWIDTH` | Number(18,8) | m | Width of the implement section. |
| `SECTIONID` | Number(5,0) | — | Implement section id for this sample. |
| `ELEVATION` | Double | m | GPS elevation, adjusted for receiver offset. |
| `MACHINE` | Number(5,0) | — | Index of the active machine/configuration for this sample. |
| `PRODUCTHASH` | Character(35) | — | Opaque unique identifier. |
#### Timestamp [#timestamp]
`TIME` is the **first** attribute of every record and holds the sample timestamp
as RFC3339 in UTC (e.g. `2021-04-28T11:49:31.714Z`). Parse it as RFC3339. Where
no timestamp is available for a sample, `TIME` is blank.
#### Optional weather/operating columns [#optional-weatheroperating-columns]
Any of these may be present on any operation type:
| Column | Type | Unit | Meaning |
| ------------- | ------------- | ------- | ------------------------------------------- |
| `FUEL` | Number(18,8) | l | Fuel consumed. |
| `VEHICLSPEED` | Number(18,8) | km1hr-1 | Vehicle speed. |
| `AIRTEMP` | Number(18,8) | °C | Air temperature. |
| `WINDDRCTN` | Character(2) | — | Wind direction (e.g. `SE`). |
| `WINDSPEED` | Number(18,8) | km1hr-1 | Wind speed. |
| `SKYCNDTN` | Character(23) | — | Sky conditions (e.g. `Sunny`). |
| `HUMIDITY` | Number(18,8) | percent | Humidity. |
| `SOILMOIST` | Character(23) | — | Soil moisture (e.g. `Dry`). |
| `SOILTEMP` | Number(18,8) | °C | Soil temperature. |
| `DELTAT` | Number(18,8) | °C | Temperature variation during the operation. |
#### Application columns [#application-columns]
| Column | Type | Unit | Meaning |
| ------------- | ------------- | ----------------- | ---------------------------------------------------------------------------------------------------------- |
| `PRODUCT` | Character(23) | — | Product applied at this sample. Emitted as `C(64)` to match the summary layer — see *Product width* below. |
| `APPLIEDRATE` | Number(18,8) | kg1ha-1 or l1ha-1 | Measured application rate. |
| `CONTROLRATE` | Number(18,8) | kg1ha-1 or l1ha-1 | Prescribed rate sent to the implement. |
| `TARGETRATE` | Number(18,8) | kg1ha-1 or l1ha-1 | Prescribed rate in the absence of a control rate. |
Nutrient-constituent columns, present based on the application type —
applied/total/target/prescription rate and concentration of nitrogen (N),
phosphorus (P₂O₅), potassium (K₂O) and ammonium (NH₄N):
| Column | Type | Unit | Meaning |
| ------------- | ------------ | ------- | ------------------------------ |
| `APLDRTN` | Number(18,8) | kg1ha-1 | Applied rate, nitrogen. |
| `APLDRTP2O5` | Number(18,8) | kg1ha-1 | Applied rate, phosphorus. |
| `APLDRTK2O` | Number(18,8) | kg1ha-1 | Applied rate, potassium. |
| `APLDRTNH4N` | Number(18,8) | kg1ha-1 | Applied rate, ammonium. |
| `APLDTLN` | Number(18,8) | kg | Applied total, nitrogen. |
| `APLDTLP2O5` | Number(18,8) | kg | Applied total, phosphorus. |
| `APLDTLK2O` | Number(18,8) | kg | Applied total, potassium. |
| `APLDTLNH4N` | Number(18,8) | kg | Applied total, ammonium. |
| `TRGTRTN` | Number(18,8) | kg1ha-1 | Target rate, nitrogen. |
| `TRGTRTP2O5` | Number(18,8) | kg1ha-1 | Target rate, phosphorus. |
| `TRGTRTK2O` | Number(18,8) | kg1ha-1 | Target rate, potassium. |
| `TRGTRTNH4N` | Number(18,8) | kg1ha-1 | Target rate, ammonium. |
| `RXRATEN` | Number(18,8) | kg1ha-1 | Prescription rate, nitrogen. |
| `RXRATEP2O5` | Number(18,8) | kg1ha-1 | Prescription rate, phosphorus. |
| `RXRATEK2O` | Number(18,8) | kg1ha-1 | Prescription rate, potassium. |
| `RXRATENH4N` | Number(18,8) | kg1ha-1 | Prescription rate, ammonium. |
| `NCNCNTRN` | Number(18,8) | kg1m3-1 | Nitrogen concentration. |
| `P2O5CNCNTRN` | Number(18,8) | kg1m3-1 | Phosphorus concentration. |
| `K2OCNCNTRN` | Number(18,8) | kg1m3-1 | Potassium concentration. |
| `NH4NCNCNTRN` | Number(18,8) | kg1m3-1 | Ammonium concentration. |
| `DRYMATTER` | Number(18,8) | percent | Dry matter. |
#### Product width [#product-width]
`PRODUCT` names the same thing in both layers and carries it at the same width in
both: **at least `C(64)`**. One product name therefore fits identically wherever
it appears in the bundle, and a consumer sizing a field for it needs only one
number.
The width is a floor, not a fixed size: a `PRODUCT` column wider than 64 keeps
its width. This is the only column with a width floor of its own; every other
column takes the width its nominal type implies.
#### Seeding columns [#seeding-columns]
| Column | Type | Unit | Meaning |
| ------------- | ------------- | --------------------- | ------------------------------------------------- |
| `CROP` | Character(64) | — | Crop name (e.g. `Corn`) — see *Crop name* below. |
| `VARIETY` | Character(23) | — | Seed variety/hybrid planted here. |
| `APPLIEDRATE` | Number(18,8) | kg1ha-1 or seeds1ha-1 | Measured seeding rate. |
| `CONTROLRATE` | Number(18,8) | kg1ha-1 or seeds1ha-1 | Prescribed rate sent to the planter. |
| `TARGETRATE` | Number(18,8) | kg1ha-1 or seeds1ha-1 | Prescribed rate in the absence of a control rate. |
#### Harvest columns [#harvest-columns]
| Column | Type | Unit | Meaning |
| ------------ | ------------- | ---------- | ------------------------------------------------ |
| `CROP` | Character(64) | — | Crop name (e.g. `Corn`) — see *Crop name* below. |
| `VARIETY` | Character(23) | — | Seed variety/hybrid harvested here. |
| `MOISTURE` | Number(18,8) | percent | Crop moisture reading. |
| `WETMASS` | Number(18,8) | kg1ha-1 | Wet-mass yield per area at this sample. |
| `VRYIELDVOL` | Number(18,8) | l1ha-1 | Volumetric yield (volumetric crops only). |
| `VRYIELDMAS` | Number(18,8) | kg1ha-1 | Yield by mass (mass-based crops only). |
| `VRYIELDBAL` | Number(18,8) | bales1ha-1 | Yield in bales (cotton only). |
Constituent/quality columns, present based on the harvest type:
| Column | Type | Unit | Meaning |
| ------------- | ------------ | ------- | ----------------------------------- |
| `GROSSYLDA` | Number(18,8) | kg1ha-1 | Gross yield per area. |
| `GROSSYLD` | Number(18,8) | kg | Gross yield. |
| `NETYLD` | Number(18,8) | kg | Net yield. |
| `TRASH` | Number(18,8) | percent | Out-the-back mass measurement. |
| `ADFPRCNT` | Number(18,8) | percent | Acid detergent fiber percentage. |
| `NDFPRCNT` | Number(18,8) | percent | Neutral detergent fiber percentage. |
| `STRCHPRCNT` | Number(18,8) | percent | Starch percentage. |
| `CRDPRPRCNT` | Number(18,8) | percent | Crude protein percentage. |
| `SUGARPRCNT` | Number(18,8) | percent | Sugar percentage. |
| `GINTURNOUT` | Number(18,8) | percent | Gin turnout. |
| `CRUDEASH` | Number(18,8) | percent | Crude ash. |
| `CRUDEFIBER` | Number(18,8) | percent | Crude fiber. |
| `CRUDEFAT` | Number(18,8) | percent | Crude fat. |
| `OIL` | Number(18,8) | percent | Oil. |
| `METABENERGY` | Number(18,8) | mj1kg-1 | Metabolizable energy. |
| `LENGTHOFCUT` | Number(18,8) | m | Length of cut. |
| `IDHIGHRATE` | Number(18,8) | ml1kg-1 | Inoculant dosing high rate. |
| `IDHIGHTOTAL` | Number(18,8) | ml1kg-1 | Inoculant dosing high total. |
| `IDLOWRATE` | Number(18,8) | ml1kg-1 | Inoculant dosing low rate. |
| `IDLOWTOTAL` | Number(18,8) | ml1kg-1 | Inoculant dosing low total. |
| `DRYMATTER` | Number(18,8) | percent | Dry matter. |
#### Crop name [#crop-name]
For Seeding and Harvest, `CROP` holds the crop name as text (e.g. `Corn`). It is
`Character(64)`, the same type and width as the summary layer's `CROP`, so the
column reads identically in both layers.
The crop is an operation-level value — one crop per work record — so every
sample in the layer carries the same name. The variety or hybrid is a separate
value and has its own column, `VARIETY`. Where no crop name is available, `CROP`
is blank.
#### Tillage columns [#tillage-columns]
| Column | Type | Unit | Meaning |
| ----------- | ------------- | ---- | --------------------------- |
| `TILLTYPE` | Character(23) | — | Tillage type (e.g. `Disk`). |
| `APPLDEPTH` | Number(18,8) | m | Measured applied depth. |
| `CTRLDEPTH` | Number(18,8) | m | Measured control depth. |
| `TRGTDEPTH` | Number(18,8) | m | Measured target depth. |
| `APPLPRESS` | Number(18,8) | kpa | Measured applied pressure. |
| `CTRLPRESS` | Number(18,8) | kpa | Measured control pressure. |
| `TRGTPRESS` | Number(18,8) | kpa | Measured target pressure. |
***
## John Deere export implementation details [#john-deere-export-implementation-details]
How the bundle above is produced from a John Deere Operations Center (JDOC) work
record.
### Source material [#source-material]
For one JDOC work record (aka field operation), John Deere provides a shapefile
export together with a `.json` metadata sidecar. The sidecar carries, among
other things, each column's original unit, the operation's crop as a top-level
`CropId`/`CropName` pair, and the mapping from the machine index to
machine/operator ids.
The two published layers have different origins:
* The **summary layer** is composed here. Its columns, types and metric units are
defined by this document; its values are aggregated from the operation's
measurements as reported by JDOC.
* The **point-level layer** is John Deere's own shapefile export, republished
with the transformations below. Its column set and types are John Deere's and
depend on the operation type. The point-level column tables list every column
John Deere documents, and their **Type** column is John Deere's documented
source type.
### Source archive layout [#source-archive-layout]
John Deere's download nests the shapefile under a `doc/` directory, names it with
spaces and other special characters, and ships a `{base}-Deere-Metadata.json`
sidecar alongside it. Restructuring it into the flat bundle described under
*Bundle layout* means:
* taking the first complete `.shp`/`.dbf` pair, sorted by basename;
* renaming the layer to the field operation id, sanitized to ASCII;
* flattening the four layer files to the zip root, with no enclosing directory;
* dropping the metadata sidecar, after its units and crop name have been read;
* carrying over John Deere's `.prj` when the download has one, and otherwise
writing the default WGS-84 WKT.
Geometry is copied verbatim; John Deere's coordinates are WGS-84 degrees.
### Transformations applied to the John Deere export [#transformations-applied-to-the-john-deere-export]
| Spec rule | What John Deere ships | What is published |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Metric normalization | Column values in the unit named in the `.json` sidecar, often imperial | Value scaled to metric, metric unit token published; converted columns re-typed to `F` (see *Emitted DBF types*) |
| Uppercase names | Mixed-case names, e.g. `Swathwidth` | `SWATHWIDTH` |
| Timestamp | A redundant pair: `Time` (locale-formatted, e.g. `4/28/2021 11:49:31 AM`) and `IsoTime` (ISO-8601 UTC, millisecond precision) | One `TIME` column parsed from `IsoTime` and re-emitted as RFC3339 UTC; the locale `Time` column is dropped and `TIME` is moved to the front of the record |
| Crop | `CROP` as a numeric crop id (e.g. `173`), opaque without John Deere's crop dictionary | `CROP` re-typed to `Character(64)` and re-valued to the name (e.g. `Corn`) resolved from the sidecar's `CropId`/`CropName` |
| Product width | `PRODUCT` as `Character(23)` | `PRODUCT` widened to `C(64)` |
A sample whose id does not match the sidecar's `CropId` — or an operation whose
sidecar carries no crop name — yields a blank `CROP`.
Everything else is left alone: non-numeric columns and columns already in metric
pass through untouched apart from the uppercasing, and a numeric column whose
unit is not recognized is passed through unconverted.
#### Emitted DBF types [#emitted-dbf-types]
Each column is emitted in one of two ways.
**Pass-through** — every non-numeric column, every numeric column already in
metric, and every numeric column whose unit is not recognized. John Deere's DBF
field definition is re-emitted intact (same type, width and decimal count) and
only the name is uppercased. `PRODUCT` is the exception: its width is raised to
64 when John Deere ships it narrower.
**Converted** — the numeric columns scaled to metric. Each is emitted as a DBF
float (`F`), its width recomputed to fit the converted values and capped at 19,
its decimal precision taken from John Deere's source precision with a floor of 3.
A source `Number(18,8)` becomes `F(w,8)` with `w ≤ 19`; a source column that
declared no decimals becomes `F(w,3)`.
**Timestamp precision.** `IsoTime` carries millisecond precision, so the
point-level `TIME` carries milliseconds; the summary layer's
`STARTDATE`/`ENDDATE` are second-precision. Both are RFC3339 in UTC, as the data format above specifies.
### Known unit normalization [#known-unit-normalization]
Values are requested from John Deere in metric where possible; whatever comes
back in another unit is converted on publish. Each column's incoming unit is read
from the `.json` sidecar.
Recognized John Deere unit tokens and the metric token published in their place;
the value is scaled by the same conversion. Note the spellings `[m3]`,
`kg11000gal-1` and `floz1ton-1`:
| Dimension | Original units (John Deere) | Published unit |
| --------------- | ------------------------------ | -------------- |
| Length | `ft`, `feet`, `in`, `mi`, `cm` | `m` |
| Speed | `mph`, `mi1hr-1`, `m1s-1` | `km1hr-1` |
| Volume | `gal`, `[m3]`, `m3` | `l` |
| Mass | `lb`, `t` | `kg` |
| Area | `ac` | `ha` |
| Mass per area | `lb1ac-1`, `t1ha-1` | `kg1ha-1` |
| Volume per area | `gal1ac-1`, `[m3]1ha-1` | `l1ha-1` |
| Seeds per area | `seeds1m2-1` | `seeds1ha-1` |
| Pressure | `bar`, `psi` | `kpa` |
| Mass per volume | `kg11000gal-1` | `kg1m3-1` |
| Volume per mass | `floz1ton-1` | `ml1kg-1` |
Tokens that are already metric (e.g. `m`, `kg`, `l`, `ha`, `kg1ha-1`, `l1ha-1`,
`km1hr-1`, `seeds1ha-1`, `kpa`, `kg1m3-1`, `ml1kg-1`) are typically passed through
unchanged, and their columns count as pass-through for the purposes of *Emitted
DBF types*.
Note that `cm` is converted to `m` - one case where a metric unit is rescaled
so that we have a consistent unit for the same dimension. John Deere sends `cm` for
tillage depth measurements.
If John Deere sends a unit in neither set — not metric, not in the table above —
the value is published unconverted and its original token passes through
verbatim. Such a value may be non-metric, and its token falls outside the
vocabulary in *Unit tokens*. It is expected that clients would handle this situation
depending on the needs of their application.
### Verified coverage [#verified-coverage]
All four operations were test exported from a testing organization — Application,
Seeding, Harvest and Tillage. A column marked **verified** below has been
published from that real data; every other column in the format tables rests on
John Deere's documentation alone, as at this time we were unable to simulate
an operation that would produce it.
In all four captures both layers came out as shape type 5 (Polygon), the
point-level layer included, and every bundle held all eight files.
| Group | Verified | Not yet seen |
| --------------------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Common | `TIME`, `HEADING`, `DISTANCE`, `SWATHWIDTH`, `SECTIONID`, `ELEVATION`, `MACHINE`, `PRODUCTHASH` | — |
| Optional weather/operating | `FUEL`, `VEHICLSPEED` | `AIRTEMP`, `WINDDRCTN`, `WINDSPEED`, `SKYCNDTN`, `HUMIDITY`, `SOILMOIST`, `SOILTEMP`, `DELTAT` |
| Application | `PRODUCT`, `APPLIEDRATE`, `CONTROLRATE`, `TARGETRATE` | — |
| Application nutrient constituents | — | all (`APLDRT*`, `APLDTL*`, `TRGTRT*`, `RXRATE*`, `*CNCNTRN`, `DRYMATTER`) |
| Seeding | `CROP`, `VARIETY`, `APPLIEDRATE`, `CONTROLRATE`, `TARGETRATE` | — |
| Harvest | `CROP`, `VARIETY`, `MOISTURE`, `WETMASS`, `VRYIELDVOL` | `VRYIELDMAS`, `VRYIELDBAL` |
| Harvest constituents/quality | `DRYMATTER` | `GROSSYLDA`, `GROSSYLD`, `NETYLD`, `TRASH`, `ADFPRCNT`, `NDFPRCNT`, `STRCHPRCNT`, `CRDPRPRCNT`, `SUGARPRCNT`, `GINTURNOUT`, `CRUDEASH`, `CRUDEFIBER`, `CRUDEFAT`, `OIL`, `METABENERGY`, `LENGTHOFCUT`, `IDHIGHRATE`, `IDHIGHTOTAL`, `IDLOWRATE`, `IDLOWTOTAL` |
| Tillage | `TILLTYPE`, `APPLDEPTH`, `CTRLDEPTH`, `TRGTDEPTH`, `APPLPRESS`, `CTRLPRESS`, `TRGTPRESS` | — |
### Summary-layer row model [#summary-layer-row-model]
A JDOC single-product operation produces one summary row; a tank mix produces one
row per component, repeating the boundary geometry and the operation-level
columns. Application fills `PRODUCT` per component; Seeding and Harvest fill
`CROP` from the operation's crop.