# Get Readings

Source: https://help.zira.us/developers/api-reference/get-readings
Summary: Read meter data with validated time-range filters.
Updated: 2026-08-06

## `GET /reading`

Use this endpoint to retrieve readings for a meter across a validated time range.

## Authentication

```http
x-api-key: <your-api-key>
```

See [API Authentication](/developers/api-reference/api-authentication) for how to create a key.

## Query parameters

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `meterId` | string | yes | Data source to read from. |
| `startTime` | string | no | Start of an absolute time range, e.g. `2026-01-14T00:00:00`. Send it together with `endTime`. |
| `endTime` | string | no | End of an absolute time range. Send it together with `startTime`. |
| `startHour` | string | no | Start of a daily window, as `HH:mm:ss`. Part of the relative range — send all four of these together. |
| `endHour` | string | no | End of a daily window, as `HH:mm:ss`. Part of the relative range. |
| `startOffset` | string | no | How far back the relative range starts, as `<amount> <unit>`, e.g. `1 day`. |
| `endOffset` | string | no | How far back the relative range ends, as `<amount> <unit>`, e.g. `0 days`. |
| `limit` | number | no | Maximum number of rows to return. 1 to 500, and 500 by default. |
| `lastValue` | string | no | Cursor for the next page. Pass the `lastValue` string from the previous response back verbatim. |
| `sortDesc` | boolean | no | Order rows by timestamp descending. Newest-first is already the default. |

## Response

`200 OK`

Returns one flat row per reading timestamp, newest first. Metric values are keys named after the metric ID, alongside `meter_id` and `event_date`. An opaque `lastValue` cursor sits next to `data` for fetching the next page.

```json
{
  "data": [
    {
      "33": 0.5,
      "34": 2.5,
      "meter_id": 17794,
      "event_date": "2026-01-14T12:44:20.000000Z"
    },
    {
      "33": 1,
      "34": 2,
      "meter_id": 17794,
      "event_date": "2026-01-14T12:28:36.000000Z"
    },
    {
      "33": null,
      "34": 1.5,
      "meter_id": 17794,
      "event_date": "2026-01-14T05:44:20.000000Z"
    }
  ],
  "lastValue": "eyJldmVudF9kYXRlIjoiMjAyNi0wMS0xNFQwNTo0NDoyMC4wMDAwMDBaIn0="
}
```

| Field | Type | Description |
| --- | --- | --- |
| `data` | array (nullable) | One object per reading timestamp. `null` — not an empty array — when the data source has no readings in the window you asked for. |
| `data[].<metricId>` | number | string | null (nullable) | One key per metric, named with the metric’s ID (e.g. `"33"`). `null` when that metric had no value at that timestamp. Get the ID-to-name mapping from `GET /data-sources/{id}` with `withSchema=true`. |
| `data[].meter_id` | number | ID of the data source the row belongs to. A number here, even though the `meterId` you send in the query is a string. |
| `data[].event_date` | string | Timestamp of the reading, UTC with microsecond precision. This is also the field pagination sorts on. |
| `lastValue` | string | Opaque cursor for the next page. Pass it back verbatim as `?lastValue=…`. |

### Good to know

- Rows are flat and keyed by metric ID. There is no nested `readings` array.
- Do not treat the presence of `lastValue` as proof that more data exists — on this endpoint it comes back on every non-empty page, including the last one. Keep paging until `data` is `null`, or until you get fewer rows than the `limit` you asked for.
- Newest-first is the effective default ordering.
- `limit` accepts 1 to 500 and defaults to 500, so 500 rows is the largest page you can get.
- Metric filtering is not available on this endpoint — every metric on the data source comes back on every row.

## Paging through readings

Send a `limit`, then pass the `lastValue` from each response back as `?lastValue=…` to get the
next page. Treat the cursor as opaque — do not decode or build one yourself.

Stop when `data` comes back `null`, or when you receive fewer rows than the `limit` you asked
for. On this endpoint a `lastValue` is returned even on the final page, so its presence alone does
not mean more data exists.

## Mapping metric IDs to names

Row keys are metric IDs. Call
[Get Data Source List](/developers/api-reference/get-data-source-list) — every data source comes back
with its `meterSchema`, where `metrics[].metric.id` is the key you see here and
`metrics[].displayName` is its human-readable name.

## Errors

Failures return a JSON body with a `message` and an `internalErrorCode`, at the status shown below. Whenever the underlying error carries structured detail — which every schema-validation failure does — the body is replaced with a generic `Internal server error.` / `02-001` pair while the real status code is kept. Branch on the HTTP status and on `internalErrorCode`; never parse the message text.

```json
{
  "message": "Internal server error.",
  "internalErrorCode": "02-001"
}
```

| Status | Meaning |
| --- | --- |
| `400` | The request failed validation — a missing required field, a field the schema does not allow, an unparseable body, a value out of range, or a malformed `lastValue`. |
| `401` | The API key resolved to a user context that is not allowed to read or write the record you asked for. |
| `403` | The `x-api-key` header is missing or is not a recognised key. API Gateway rejects the call before the endpoint runs, so the body is only `{"message":"Forbidden"}`. |
| `409` | The write conflicts with an existing record, or it references a record that does not exist. |
| `429` | The API key exceeded the rate limit or quota on its usage plan. Also returned by API Gateway rather than the endpoint. |
| `500` | Unexpected server error, or the database returned no result where one was required. |
