Sending & 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.
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 for the full header reference.
If your integration uses generated clients, start from the hosted 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.
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
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 for how the limits are scoped and Limitations for the current values.
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
400with the messageNo 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 messageRecipient 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 for the exact messages.
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
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 for the wire format and a sample MESSAGE_RECEIVED event, and the Events catalog for the per-event payloads.
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
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
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
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
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.
Unconfirmed events accumulate in your feed and will replay on every reconnect. Confirm events after your application has processed them.
Error handling
| Error | Cause | Resolution |
|---|---|---|
400 on send | Malformed headers, unsupported message type, or missing required field. | Validate the request against the API spec 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. |
429 on send | The application exceeded its request rate. | Back off exponentially and retry. No Retry-After header is sent. See 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 for the full HTTP status code list and response body shape.