Capture Public API Reference

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/....

Base URL
https://api.matter.city

A full request URL therefore looks like:

Example
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
curl https://api.matter.city/v1/assets \
  -H "x-api-key: $CAPTURE_API_KEY"
Note
Keys are opaque 64-character hex tokens. The raw key is never stored server-side — only its SHA-256 hash. Keep your key secret and treat it like a password. A missing or invalid key returns 401 unauthorized.

Response format

Every response uses a single JSON envelope. Success responses carry a data field; failures carry an error object.

Success

JSON
{
  "data": { /* resource or { items, next_cursor, meta } */ }
}

Error

JSON
{
  "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.
Events cursor
/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, from must not be later than to.
  • 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.)

  1. Call the endpoint with your limit (and optionally a starting to).
  2. Read the oldest timestamp in the page — the last item, since rows are newest-first.
  3. Call again with to=<oldestTimestamp>.
  4. Stop when a page returns fewer than limit rows.

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.

Backward paging (pseudocode)
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`
}
Why dedupe on (uid, timestamp)?
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.

GET/v1/health

Responses

Service is up.

200 · application/json
{
  "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.

GET/v1/assets

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.

200 · application/json
{
  "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.

401 · application/json
{ "error": { "code": "unauthorized", "message": "Invalid or expired API key" } }

Get asset API key

Fetch a single asset by its primary key, including detailed fields.

GET/v1/assets/:id

Path parameters

Name Type Description
id string required The asset's primary key.

Responses

The asset.

200 · application/json
{
  "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.

404 · application/json
{ "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.

GET/v1/assets/:id/device-data

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.
Limit is global, not per-device
For telemetry from a specific device only, use the device-level telemetry endpoint.
Paging through history
This endpoint has no 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.

200 · application/json
{
  "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.

400 · application/json
{ "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.

GET/v1/assets/:id/asset-data

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.
Multi-device history
An asset may receive data from several devices at once, and the set of devices can change over time. Always use each row's 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.
Paging through history
This endpoint has no 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.

200 · application/json
{
  "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.

400 · application/json
{ "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.

GET/v1/devices

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.

200 · application/json
{
  "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.

GET/v1/devices/imei/:imei

Path parameters

Name Type Description
imei string required The device IMEI.

Responses

The device.

200 · application/json
{
  "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.

404 · application/json
{ "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.

GET/v1/devices/recent-data/: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.

200 · application/json
{
  "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.

404 · application/json
{ "error": { "code": "not_found", "message": "Not found" } }

Get device API key

Fetch a single device by its primary key.

GET/v1/devices/:id

Path parameters

Name Type Description
id string required The device's primary key.

Responses

The device (same shape as Get device by IMEI).

200 · application/json
{
  "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.

404 · application/json
{ "error": { "code": "not_found", "message": "Device d9e8f7 not found" } }

List events API key

List events in your organisation, ordered newest-first (descending id).

GET/v1/events

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.

200 · application/json
{
  "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.

400 · application/json
{ "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.

GET/v1/organisations

Responses

The organisation(s) for this key.

200 · application/json
{
  "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.

GET/v1/tags/:id

Path parameters

Name Type Description
id string required The tag's primary key, as it appears in a resource's tags array.
Resolving several tags
There is no list endpoint for tags, so resolve ids one at a time. If you display tag names alongside a page of assets, cache the resolved tags client-side — tags change rarely, and each lookup counts against your rate limit.

Responses

The tag.

200 · application/json
{
  "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.

404 · application/json
{ "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.

GET/v1/zones

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.

200 · application/json
{
  "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" }
  }
}