Capture Public API
A customer-facing, read-only REST API for the Matter Capture platform. Each customer is issued an API key that grants read access to the data of a single organisation. Every request is automatically scoped to that organisation, so a key can never read another organisation's data.
All endpoints are GET, return JSON, and live under the /v1 path prefix.
Base URL
Production requests go to the custom domain below. Paths are versioned under /v1/....
https://api.matter.city
A full request URL therefore looks like:
https://api.matter.city/v1/assets
Authentication
Authenticate every request (except /v1/health) with your API key.
Provide it using either header:
| Header | Example |
|---|---|
x-api-key |
x-api-key: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2 |
Authorization |
Authorization: Bearer a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2 |
curl https://api.matter.city/v1/assets \
-H "x-api-key: $CAPTURE_API_KEY"
401 unauthorized.
Response format
Every response uses a single JSON envelope. Success responses carry a data field; failures carry
an error object.
Success
{
"data": { /* resource or { items, next_cursor, meta } */ }
}
Error
{
"error": {
"code": "not_found",
"message": "Asset 123 not found",
"details": null
}
}
Response headers
These headers are present on every response:
| Header | Description |
|---|---|
X-Request-Id |
Per-request UUID for correlating logs and support tickets. |
Cache-Control |
no-store — organisation-scoped data must not be cached by proxies/CDNs. |
X-Content-Type-Options |
nosniff |
Strict-Transport-Security |
max-age=63072000; includeSubDomains |
X-RateLimit-* |
Rate-limit budget headers (authenticated routes only). See Rate limiting. |
Pagination
List endpoints use keyset (cursor) pagination. Pass ?limit to control page size
and
?cursor to fetch the next page. The response includes next_cursor: pass it as
?cursor= on the next request. A null next_cursor means there are no
more pages.
| Param | Type | Default | Notes |
|---|---|---|---|
limit |
integer | 50 |
Max 200 for most lists; 1000 for telemetry. |
cursor |
string | — | The next_cursor from the previous page. |
/v1/events is ordered newest-first and its cursor is the numeric
id (int8), not a string key — pass it as a plain integer.
Time ranges
Telemetry endpoints accept optional ?from and ?to query parameters. Each accepts
either an ISO-8601 date-time string (e.g.
2026-06-01T00:00:00Z) or Unix epoch milliseconds as a bare
integer (e.g. 1748736000000). The two forms can be mixed across the two bounds. The underlying
timestamp column is Unix epoch milliseconds; the API normalises both forms to epoch milliseconds
before filtering.
- Both parameters are optional; omitting one leaves that bound open.
- Omitting both returns the most recent records (newest-first, up to
limit). - When both are present,
frommust not be later thanto. - The maximum allowed window is 31 days. Larger ranges return
400 bad_request.
Paging through large result sets
The asset telemetry and asset data
endpoints return rows newest-first and are capped by limit. They do not use a
cursor, but because they accept from/to you can page backward through
history using the to parameter: fetch a page, then request the next (older) page by setting
to to the oldest timestamp you just received. (The
device telemetry endpoint takes only limit and always
returns the most recent records, so this technique does not apply there.)
- Call the endpoint with your
limit(and optionally a startingto). - Read the oldest
timestampin the page — the last item, since rows are newest-first. - Call again with
to=<oldestTimestamp>. - Stop when a page returns fewer than
limitrows.
Unlike offset paging, this is stable: newly inserted (newer) records never shift the older pages you are walking
through. Passing only to (no from) also means the 31-day window cap does not apply, so
you can walk arbitrarily far back.
const limit = 1000;
let to = null; // start at the most recent record
const seen = new Set(); // dedupe on (uid, timestamp)
while (true) {
const qs = new URLSearchParams({ limit: String(limit) });
if (to !== null) qs.set("to", String(to)); // `to` is INCLUSIVE
const res = await fetch(`/v1/assets/${assetId}/device-data?${qs}`, {
headers: { "x-api-key": API_KEY },
});
const { data } = await res.json();
const items = data.items;
if (items.length === 0) break;
for (const row of items) {
const id = `${row.uid}:${row.timestamp}`;
if (!seen.has(id)) { seen.add(id); handle(row); }
}
if (items.length < limit) break; // last page
to = items[items.length - 1].timestamp; // oldest ts -> next `to`
}
to is inclusive, so the boundary record is returned again on the next call. An
asset may also have several devices, and two devices can report the same timestamp
(epoch milliseconds). Re-sending the oldest timestamp as to and de-duplicating on
(uid, timestamp) is lossless and never skips tied rows. (Using to = oldestTimestamp - 1
avoids duplicates but can drop rows that share that exact millisecond — only safe when an asset has a single
device.)
Rate limiting
Requests are throttled per API key using two fixed-window buckets:
| Bucket | Granularity | Default |
|---|---|---|
| Burst | Per minute | 250 requests / minute |
| Daily quota | Per UTC day | 50,000 requests / day |
When a bucket is exhausted, the API returns 429 rate_limit_exceeded with a
Retry-After header.
Rate-limit headers
| Header | Description |
|---|---|
X-RateLimit-Limit-Minute |
Requests allowed per minute for this key. |
X-RateLimit-Remaining-Minute |
Remaining requests in the current minute window. |
X-RateLimit-Reset-Minute |
UTC datetime when the minute window resets. |
X-RateLimit-Limit-Day |
Requests allowed per day for this key. |
X-RateLimit-Remaining-Day |
Remaining requests in the current day window. |
X-RateLimit-Reset-Day |
UTC datetime when the day window resets. |
Retry-After |
Seconds until the minute window resets (on 429 only). |
Errors
All errors use the { "error": { code, message } } envelope. The HTTP status maps to the
code as follows:
| Status | Code | Meaning |
|---|---|---|
400 |
bad_request |
Invalid query parameter (e.g. malformed date, range too large). |
401 |
unauthorized |
Missing, invalid, inactive, or expired API key. |
404 |
not_found |
Resource does not exist, or is not in your organisation. |
405 |
method_not_allowed |
Path exists but the HTTP method is not supported. |
415 |
unsupported_media_type |
Body request without Content-Type: application/json. |
429 |
rate_limit_exceeded |
Per-minute or per-day budget exhausted. |
500 |
internal_error |
Unexpected server error. |
Health check Public
Unauthenticated liveness check. Does not require an API key.
Responses
Service is up.
{
"data": {
"status": "ok",
"timestamp": "2026-06-17T00:37:00.000Z"
}
}
List assets API key
List the assets in your organisation. Keyset-paginated, ascending by id.
Query parameters
| Name | Type | Description | |
|---|---|---|---|
limit |
integer | optional | Page size. Default 50, max 200. |
cursor |
string | optional | The next_cursor from the previous page. |
Responses
A page of assets.
{
"data": {
"items": [
{
"id": "a1b2c3",
"name": "Bin 42",
"asset_type": "bin",
"lat": -33.8688,
"long": 151.2093,
"status": "active",
"zone": "cbd",
"tags": ["tag_7f3a"],
"address": "1 George St",
"description": null
}
],
"next_cursor": "a1b2c3",
"meta": { "limit": 50, "cursor": null, "organisation_id": "org_123" }
}
}
Missing or invalid API key.
{ "error": { "code": "unauthorized", "message": "Invalid or expired API key" } }
Get asset API key
Fetch a single asset by its primary key, including detailed fields.
Path parameters
| Name | Type | Description | |
|---|---|---|---|
id |
string | required | The asset's primary key. |
Responses
The asset.
{
"data": {
"id": "a1b2c3",
"name": "Bin 42",
"asset_type": "bin",
"lat": -33.8688,
"long": 151.2093,
"status": "active",
"zone": "cbd",
"tags": ["tag_7f3a"],
"thresholds": { "fill": 80 },
"measurement_settings": {},
"address": "1 George St",
"description": null,
"customer_data": {},
"created_at": "2025-01-04T08:00:00Z",
"updated_at": "2026-06-01T08:00:00Z"
}
}
No asset with that id in your organisation.
{ "error": { "code": "not_found", "message": "Asset a1b2c3 not found" } }
Asset telemetry API key
Telemetry for all devices attached to an asset, newest-first. Rows from different devices are
interleaved and distinguished by each row's uid (the device IMEI).
Prefer asset data when you need every reading stamped with this
asset's ID across device swaps and concurrent reporters.
Path parameters
| Name | Type | Description | |
|---|---|---|---|
id |
string | required | The asset's primary key. |
Query parameters
| Name | Type | Description | |
|---|---|---|---|
from |
string (ISO-8601) or integer (epoch ms) | optional | Start of the window. Accepts an ISO-8601 date-time string or Unix epoch milliseconds. Leave both from and to empty to return the most recent data. See Time ranges. |
to |
string (ISO-8601) or integer (epoch ms) | optional | End of the window. Accepts an ISO-8601 date-time string or Unix epoch milliseconds. Max window 31 days. |
limit |
integer | optional | Default 20, max 1000. Global across all the asset's devices. |
cursor. To read more than limit rows, page backward with the
to parameter — see Paging through large result sets.
Responses
Telemetry rows plus the distinct devices present in this page.
{
"data": {
"asset_id": "a1b2c3",
"range": { "from": "2026-06-01T00:00:00Z", "to": null },
"items": [
{ "uid": "imei-A", "timestamp": 1750000003000, "level": 10,
"fill_status": "low", "depth": 12.4, "temperature": 21.5,
"tilt": 0, "debris": false, "debris_level": 0,
"light_ambient": 138.4, "light_clear": 402.1,
"is_collection": false, "location": null, "network": {} },
{ "uid": "imei-B", "timestamp": 1750000002000, "level": 55 }
],
"num_devices": 2,
"device_ids": ["imei-A", "imei-B"],
"meta": { "limit": 100, "organisation_id": "org_123" }
}
}
Invalid date or a window larger than 31 days.
{ "error": { "code": "bad_request",
"message": "Time range exceeds the maximum allowed window of 31 days" } }
Asset data API key
Telemetry stamped with this asset's ID (device_data.asset_id), newest-first.
Unlike looking up currently attached devices, this returns every reading recorded against the asset —
including history from devices that have since been swapped out, and concurrent readings when multiple
devices report for the same asset. Each row identifies its source device via uid (IMEI);
the response also lists the distinct devices present in the page.
Path parameters
| Name | Type | Description | |
|---|---|---|---|
id |
string | required | The asset's primary key. Matched against device_data.asset_id. |
Query parameters
| Name | Type | Description | |
|---|---|---|---|
from |
string (ISO-8601) or integer (epoch ms) | optional | Start of the window. Accepts an ISO-8601 date-time string or Unix epoch milliseconds. Leave both from and to empty to return the most recent data. See Time ranges. |
to |
string (ISO-8601) or integer (epoch ms) | optional | End of the window. Accepts an ISO-8601 date-time string or Unix epoch milliseconds. Max window 31 days. |
limit |
integer | optional | Default 20, max 1000. Global across all devices that have reported for this asset. |
uid (or the page-level device_ids) to attribute readings
to a device. For a single device's recent readings by IMEI, use the
device-level telemetry endpoint.
cursor. To read more than limit rows, page backward with the
to parameter — see Paging through large result sets.
Responses
Telemetry rows keyed by asset_id, plus the distinct reporting devices in this page.
{
"data": {
"asset_id": "a1b2c3",
"range": { "from": "2026-06-01T00:00:00Z", "to": null },
"items": [
{ "uid": "imei-A", "asset_id": "a1b2c3", "timestamp": 1750000003000,
"level": 10, "fill_status": "low", "depth": 12.4, "temperature": 21.5,
"tilt": 0, "debris": false, "debris_level": 0,
"light_ambient": 138.4, "light_clear": 402.1,
"is_collection": false, "location": null, "network": {} },
{ "uid": "imei-B", "asset_id": "a1b2c3", "timestamp": 1750000002000, "level": 55 }
],
"num_devices": 2,
"device_ids": ["imei-A", "imei-B"],
"meta": { "limit": 100, "organisation_id": "org_123" }
}
}
Invalid date or a window larger than 31 days.
{ "error": { "code": "bad_request",
"message": "Time range exceeds the maximum allowed window of 31 days" } }
List devices API key
List the devices in your organisation. Keyset-paginated, ascending by id.
Query parameters
| Name | Type | Description | |
|---|---|---|---|
limit |
integer | optional | Page size. Default 50, max 200. |
cursor |
string | optional | The next_cursor from the previous page. |
Responses
A page of devices.
{
"data": {
"items": [
{
"id": "d9e8f7",
"name": "Sensor A",
"IMEI": "352000000000001",
"status": "active",
"parent_asset": "a1b2c3",
"tags": [],
"description": null,
"created_at": "2025-02-01T08:00:00Z",
"updated_at": "2026-06-01T08:00:00Z"
}
],
"next_cursor": "d9e8f7",
"meta": { "limit": 50, "cursor": null, "organisation_id": "org_123" }
}
}
Get device by IMEI API key
Fetch a single device by its IMEI.
Path parameters
| Name | Type | Description | |
|---|---|---|---|
imei |
string | required | The device IMEI. |
Responses
The device.
{
"data": {
"id": "d9e8f7",
"name": "Sensor A",
"IMEI": "352000000000001",
"status": "active",
"parent_asset": "a1b2c3",
"tags": [],
"description": null,
"created_at": "2025-02-01T08:00:00Z",
"updated_at": "2026-06-01T08:00:00Z"
}
}
No device with that IMEI in your organisation.
{ "error": { "code": "not_found", "message": "Device with IMEI 352000000000001 not found" } }
Recent telemetry API key
The most recent telemetry records for a single device, newest-first, keyed by IMEI.
Path parameters
| Name | Type | Description | |
|---|---|---|---|
imei |
string | required | The device IMEI (device_data.uid). |
Query parameters
| Name | Type | Description | |
|---|---|---|---|
limit |
integer | optional | Number of records. Default 20, max 1000. |
Responses
Array of telemetry documents for the device.
{
"data": {
"imei": "352000000000001",
"items": [
{ "uid": "352000000000001", "timestamp": 1750000003000,
"level": 10, "fill_status": "low", "depth": 12.4,
"temperature": 21.5, "tilt": 0, "debris": false,
"debris_level": 0, "light_ambient": 138.4,
"light_clear": 402.1, "is_collection": false,
"location": null, "network": {} }
],
"meta": { "limit": 20, "organisation_id": "org_123" }
}
}
Resource not found.
{ "error": { "code": "not_found", "message": "Not found" } }
Get device API key
Fetch a single device by its primary key.
Path parameters
| Name | Type | Description | |
|---|---|---|---|
id |
string | required | The device's primary key. |
Responses
The device (same shape as Get device by IMEI).
{
"data": {
"id": "d9e8f7",
"name": "Sensor A",
"IMEI": "352000000000001",
"status": "active",
"parent_asset": "a1b2c3",
"tags": [],
"description": null,
"created_at": "2025-02-01T08:00:00Z",
"updated_at": "2026-06-01T08:00:00Z"
}
}
No device with that id in your organisation.
{ "error": { "code": "not_found", "message": "Device d9e8f7 not found" } }
List events API key
List events in your organisation, ordered newest-first (descending id).
Query parameters
| Name | Type | Description | |
|---|---|---|---|
limit |
integer | optional | Page size. Default 50, max 200. |
cursor |
integer | optional | The numeric id from next_cursor; the next page returns
id < cursor.
|
Responses
A page of events.
{
"data": {
"items": [
{
"id": 90210,
"created_at": "2026-06-16T22:14:00Z",
"asset_id": "a1b2c3",
"device_id": "d9e8f7",
"type": "fill_threshold",
"status": "open",
"confidence": 0.92,
"data": { "level": 85 }
}
],
"next_cursor": "90210",
"meta": { "limit": 50, "cursor": null, "organisation_id": "org_123" }
}
}
The cursor was not a numeric event id.
{ "error": { "code": "bad_request", "message": "cursor must be a numeric event id" } }
List organisations API key
Organisations accessible to the key. Since a key is scoped to a single organisation, this returns at most one row.
Responses
The organisation(s) for this key.
{
"data": {
"items": [
{
"id": "org_123",
"name": "City Council",
"status": "active",
"description": null,
"domain": "council.example",
"lat": -33.8688,
"long": 151.2093,
"timezone": "Australia/Sydney",
"profile": { "type": "waste" }
}
],
"meta": { "organisation_id": "org_123" }
}
}
Get tag API key
Fetch a single tag by its primary key. The tags array returned on assets, devices and
zones contains tag ids — use this endpoint to resolve one of those ids to its
name and type.
Path parameters
| Name | Type | Description | |
|---|---|---|---|
id |
string | required | The tag's primary key, as it appears in a resource's tags array. |
Responses
The tag.
{
"data": {
"id": "tag_7f3a",
"name": "priority",
"tag_type": "operational",
"status": "active",
"description": null,
"created_at": "2025-03-11T08:00:00Z"
}
}
No tag with that id in your organisation.
{ "error": { "code": "not_found", "message": "Tag tag_7f3a not found" } }
List zones API key
List the zones in your organisation. Keyset-paginated, ascending by id.
Query parameters
| Name | Type | Description | |
|---|---|---|---|
limit |
integer | optional | Page size. Default 50, max 200. |
cursor |
string | optional | The next_cursor from the previous page. |
Responses
A page of zones.
{
"data": {
"items": [
{
"id": "z1",
"name": "CBD",
"status": "active",
"description": null,
"city": "Sydney",
"country": "AU",
"state": "NSW",
"postal_code": "2000",
"parent_zone": null,
"is_sub_zone": false,
"tags": [],
"timezone": "Australia/Sydney",
"polygon_coordinates_map": {}
}
],
"next_cursor": "z1",
"meta": { "limit": 50, "cursor": null, "organisation_id": "org_123" }
}
}