Response Formats

The First Mile Supply Chain API can return the same data in several representations. You choose one per request with the standard HTTP Accept header — there are no ?format= query parameters. Two of the four supported formats are streaming: they let you pull an entire result set over a single connection without walking pagination cursors.

The Accept header is required on every request. If it is missing or does not include a supported type, the server responds with 406 Not Acceptable. See Headers for the full header contract.


Supported media types

Accept valueStreamed?Body shapeUse it when
application/jsonNoPaginated envelope { meta, links, data[] }Default. Browsing or building UI pages; you want pagination links.
application/geo+jsonNoGeoJSON FeatureCollection (paginated)You need geometries in a standard GIS interchange format.
application/x-ndjsonYesNewline-delimited JSONBulk export / pulling a whole result set without cursor-walking.
application/x-ndgeo+jsonYesNewline-delimited GeoJSON FeaturesBulk export of geometries for streaming into GIS tooling.

application/json is the default representation used throughout these docs and the only one most integrations need. Pagination, filtering, and sorting work the same regardless of format — see Query Parameters.


Choosing how to traverse a result set

A list endpoint can return more rows than fit in one response. There are three ways to pull the full set, and the right one depends on how much data you're moving and whether a human is watching.

StrategyHow you ask for itHow you advanceBest forWatch out for
Page-number paginationapplication/json (or geo+json) + page[number] / page[size]Increment page[number]; server returns links.next and a meta.totalSmall result sets, UI "page X of Y", known-shallow pagingGets slower the deeper you page. Avoid for large walks.
Cursor (keyset) paginationapplication/json (or geo+json) + page[cursor]=Follow meta.next_cursor / links.next until absentLarge result sets, deep pagination, non-UI integrations that still want an envelopeNo meta.total (it's null); forward-only — no prev.
StreamingAccept: application/x-ndjson or application/x-ndgeo+jsonNothing — every matching row arrives in one responseBulk export / sync, "give me the whole dataset" jobsPagination params are ignored; no random access, no page links. One long-lived connection.

Rules of thumb:

  • Interactive browsing or a paged UI → page-number application/json.
  • Walking a large dataset programmatically but you still want the envelope (meta/links per page) → cursor mode. Each page costs the same regardless of depth — page 10 and page 10,000 take the same time.
  • Exporting or syncing the entire result set → stream it. One request, no cursor-walking, constant memory on both ends.

Page-number and cursor are two modes of the same paginated formats (application/json / geo+json) and are selected by which query parameter you pass; streaming is selected by the Accept header. The full pagination contract — parameters, the meta/links envelope, count strategy, and performance benchmarks — lives in Query Parameters → Pagination. This page covers the response shapes.


Paginated formats

application/json

The standard envelope. Every list response is wrapped in meta (pagination and units), links (self, prev, next), and data (the array of resources). List endpoints support both page-number and cursor (keyset) pagination on this shape — see Query Parameters → Pagination for the full contract and the envelope shape in the overview.

Geometry is opt-in on this format. By default geo is omitted from attributes. Pass include_geometries=true to include it — see Query Parameters → Geometry.

application/geo+json

Returns a GeoJSON FeatureCollection. Each resource becomes a Feature whose geometry is the location's geometry (or null for non-spatial resources) and whose properties carry the resource fields. Geometry is always included on this format — you do not need include_geometries=true (it is implied). Like application/json, this format is paged — one page of features per response, sized by page[size]. The FeatureCollection carries no pagination links (it has only type and features), so to walk pages increment page[number] yourself, or use a streaming format to pull everything in one response.

curl 'https://api.epoch.blue/locations/plots?page[size]=50' \
  -H 'x-api-key: ...' \
  -H 'Accept: application/geo+json'

Streaming formats

application/x-ndjson and application/x-ndgeo+json stream the entire matching result set in one response. This is the main reason to reach for the Accept header rather than the default JSON envelope:

  • Pagination parameters are ignored. page[number], page[size], and page[cursor] have no effect in streaming mode — every matching row is emitted. There is no cursor to follow and no links.next; when the stream ends, you have everything.
  • One row per line. The response is line-delimited, so a consumer can process each record as it arrives instead of buffering the whole payload.
  • Constant memory on both ends. The server writes rows with backpressure and uses Transfer-Encoding: chunked; you can pipe the stream straight into a line-oriented tool.

This makes streaming the right choice for exports, syncs, and any "give me the whole dataset" job. For interactive browsing where you want page links, use application/json.

Wire format

Both streaming formats share the same line structure:

LineContents
1The meta object (e.g. units) as a single JSON object.
2The links object — self only. There is no next; the stream is complete.
3+One data row per line. For NDJSON this is a resource object; for ND-GeoJSON it is a GeoJSON Feature.

So the first two lines mirror the meta and links of the JSON envelope, and every line after that is what would otherwise have been an entry in data.

NDJSON example (application/x-ndjson)

{"units":"t_co2e"}                                          ← line 1: meta
{"self":"/metrics/plots/deforestation"}                     ← line 2: links
{"plot_id":"...","deforestation_ha":0.12, ...}              ← line 3: data row
{"plot_id":"...","deforestation_ha":0.00, ...}              ← line 4: data row
...

ND-GeoJSON example (application/x-ndgeo+json)

{"units":"t_co2e"}                                          ← line 1: meta
{"self":"/locations/plots"}                                 ← line 2: links
{"type":"Feature","geometry":{...},"properties":{...}}      ← line 3: Feature
{"type":"Feature","geometry":{...},"properties":{...}}      ← line 4: Feature
...

Consuming a stream

Pipe the response into a line-oriented JSON tool. Remember that the first two lines are meta and links, not data — skip them when collecting rows:

# Stream every matching plot deforestation row, dropping the meta + links header
curl -sN 'https://api.epoch.blue/metrics/plots/deforestation?filter[assets.task_id]=eq:<task>' \
  -H 'x-api-key: ...' \
  -H 'Accept: application/x-ndjson' \
| tail -n +3 \
| jq -c '.'

curl -N disables output buffering so rows surface as they arrive. Streaming responses also send Vary: Accept, so any caches key on the requested format.


Single-resource endpoints

GET .../:id endpoints honour the same Accept header. Requesting a streaming or GeoJSON type returns that single resource in the matching representation:

  • application/json — the resource inside the standard envelope.
  • application/geo+json — a single GeoJSON Feature.
  • application/x-ndjson / application/x-ndgeo+json — the same line structure as list streams (line 1 meta, line 2 links, line 3 the single item / Feature).

Content negotiation rules

  • Required. Every request must send an Accept header that includes a supported type. A missing or empty header is rejected with 406.
  • Parsed per RFC 7231. The header is a comma-separated list of media ranges; any media-type parameters after ; (including q= quality weights) are stripped before matching, and any supported type appearing in the list is enough to satisfy the requirement.
  • Wildcards accepted. */* and <type>/* (e.g. application/*) match — clients that send */* (curl's default, Pub/Sub push) get the default JSON representation.
  • Examples that pass: application/json, application/x-ndjson, application/json, */*, text/plain, application/geo+json.

When the header includes none of the supported types, the server responds with a standard error envelope:

HTTP/1.1 406 Not Acceptable
content-type: application/json
x-correlation-id: 9c8b88b1-0f4f-4c6d-9c4e-5a7f65e8b123

{
  "meta": {},
  "links": {},
  "data": {
    "error": "Accept header must include one of application/json, application/x-ndjson, application/geo+json, application/x-ndgeo+json",
    "code": "NOT_ACCEPTABLE",
    "tracking_id": "9c8b88b1-0f4f-4c6d-9c4e-5a7f65e8b123"
  }
}