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 value | Streamed? | Body shape | Use it when |
|---|---|---|---|
application/json | No | Paginated envelope { meta, links, data[] } | Default. Browsing or building UI pages; you want pagination links. |
application/geo+json | No | GeoJSON FeatureCollection (paginated) | You need geometries in a standard GIS interchange format. |
application/x-ndjson | Yes | Newline-delimited JSON | Bulk export / pulling a whole result set without cursor-walking. |
application/x-ndgeo+json | Yes | Newline-delimited GeoJSON Features | Bulk 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.
| Strategy | How you ask for it | How you advance | Best for | Watch out for |
|---|---|---|---|---|
| Page-number pagination | application/json (or geo+json) + page[number] / page[size] | Increment page[number]; server returns links.next and a meta.total | Small result sets, UI "page X of Y", known-shallow paging | Gets slower the deeper you page. Avoid for large walks. |
| Cursor (keyset) pagination | application/json (or geo+json) + page[cursor]= | Follow meta.next_cursor / links.next until absent | Large result sets, deep pagination, non-UI integrations that still want an envelope | No meta.total (it's null); forward-only — no prev. |
| Streaming | Accept: application/x-ndjson or application/x-ndgeo+json | Nothing — every matching row arrives in one response | Bulk export / sync, "give me the whole dataset" jobs | Pagination 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
application/jsonThe 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
geois omitted fromattributes. Passinclude_geometries=trueto include it — see Query Parameters → Geometry.
▶application/geo+json
application/geo+jsonReturns 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], andpage[cursor]have no effect in streaming mode — every matching row is emitted. There is no cursor to follow and nolinks.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:
| Line | Contents |
|---|---|
| 1 | The meta object (e.g. units) as a single JSON object. |
| 2 | The 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)
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)
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 GeoJSONFeature.application/x-ndjson/application/x-ndgeo+json— the same line structure as list streams (line 1meta, line 2links, line 3 the single item /Feature).
▶Content negotiation rules
- Required. Every request must send an
Acceptheader 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
;(includingq=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"
}
}