> ## Documentation Index
> Fetch the complete documentation index at: https://docs.telemax.com.au/llms.txt
> Use this file to discover all available pages before exploring further.

# Request & response format

> JSON conventions, query vs body, datetime serialization, pagination, and rate limiting for the V2 External API.

## Content types

| Usage                                                                | Content-Type                                          |
| -------------------------------------------------------------------- | ----------------------------------------------------- |
| Token endpoints                                                      | `application/x-www-form-urlencoded`                   |
| [`POST /v2/api/vehicles/list`](/v2/api-reference/post-vehicles-list) | `application/json` (body `{ "vehicleIds": [ ... ] }`) |
| `GET` and `PUT` routes                                               | No body; parameters in **path** or **query string**   |
| Successful JSON responses                                            | `application/json; charset=utf-8`                     |

Token routes in the API Reference: [Dashboard user token](/v2/api-reference/post-authentication-token-user) · [API key token](/v2/api-reference/post-authentication-token-api-key).

## Response envelope

Successful operations return the **payload directly** (`200 OK` with DTO JSON). There is no global wrapper like `{ "data": ... }`.

Service-layer results use `ApiResult` / `ApiResult<T>` internally; controllers map them to HTTP status codes and return either the DTO or the **error object** as the JSON body (see [Errors](/v2/errors)).

## Response headers

All V2 responses include an API version header:

| Header          | Type    | Description                                         |
| --------------- | ------- | --------------------------------------------------- |
| `X-API-Version` | integer | Always `2` on V2 responses. Absent on V1 responses. |

***

## Rate limiting

Rate limiting is enforced on all V2 endpoints (`/v2/api/...`). V1 endpoints are not rate-limited.

Every V2 response includes the following headers:

| Header                  | Type    | Description                                                                                               |
| ----------------------- | ------- | --------------------------------------------------------------------------------------------------------- |
| `X-RateLimit-Limit`     | integer | Maximum requests allowed per window for your token                                                        |
| `X-RateLimit-Remaining` | integer | Requests remaining in the current window                                                                  |
| `X-RateLimit-Reset`     | integer | Unix timestamp when the current window resets                                                             |
| `X-RateLimit-Policy`    | string  | Which rate limit window you're currently closest to hitting — one of `second`, `minute`, `hour`, or `day` |

When a rate limit is exceeded, the server returns `429 Too Many Requests` with an additional header:

| Header        | Type    | Description                     |
| ------------- | ------- | ------------------------------- |
| `Retry-After` | integer | Seconds to wait before retrying |

**Retry strategy:** Wait the full `Retry-After` value before retrying. If you receive consecutive 429s, double the wait interval on each attempt up to a maximum of 60 seconds.

**Limits:**

Company/fleet-wide endpoints are subject to significantly tighter limits than per-vehicle endpoints.

| Endpoint                                                | Per second | Per minute | Per hour | Per day |
| ------------------------------------------------------- | ---------- | ---------- | -------- | ------- |
| **Vehicles**                                            |            |            |          |         |
| `GET /v2/api/vehicles/{imei}/device-ids`                | 10         | 60         | 6,000    | 144,000 |
| `GET /v2/api/vehicles/{id}/last-position`               | 10         | 120        | 6,000    | 144,000 |
| `GET /v2/api/vehicles/{id}/positions`                   | 10         | 60         | 6,000    | 144,000 |
| `PUT /v2/api/vehicles/{id}/odometer`                    | 10         | 30         | 6,000    | 144,000 |
| `PUT /v2/api/vehicles/{id}/odometer-source`             | 10         | 30         | 6,000    | 144,000 |
| `PUT /v2/api/vehicles/{id}/name`                        | 10         | 30         | 6,000    | 144,000 |
| `GET /v2/api/vehicles/{id}/last-online`                 | 10         | 60         | 6,000    | 144,000 |
| `POST /v2/api/vehicles/list` ³                          | 10         | 30         | 6,000    | 144,000 |
| `GET /v2/api/vehicles/{id}/dtc`                         | 10         | 30         | 6,000    | 144,000 |
| `GET /v2/api/vehicles/{id}/engine-codes`                | 10         | 30         | 6,000    | 144,000 |
| `GET /v2/api/vehicles/info`                             | 10         | 60         | 6,000    | 144,000 |
| **Companies (fleet-wide)**                              |            |            |          |         |
| `GET /v2/api/companies`                                 | 4          | 30         | 600      | 14,400  |
| `GET /v2/api/companies/{id}/vehicles/last-time-online`  | 4          | 20         | 600      | 14,400  |
| `GET /v2/api/companies/{id}/vehicles/nearest` ²         | 4          | 20         | 600      | 14,400  |
| `GET /v2/api/companies/{id}/vehicles/last-position` ¹ ² | 4          | 20         | 600      | 14,400  |
| `GET /v2/api/companies/{id}/alert-records`              | 4          | 20         | 600      | 14,400  |
| `PUT /v2/api/companies/{id}/set-all-alerts-read`        | 4          | 20         | 600      | 14,400  |
| `GET /v2/api/companies/{id}/alerts`                     | 4          | 20         | 600      | 14,400  |
| **Alerts**                                              |            |            |          |         |
| `PUT /v2/api/alerts/{id}/{date}/update-read-status`     | 10         | 60         | 6,000    | 144,000 |
| **Replay**                                              |            |            |          |         |
| `GET /v2/api/replay/{id}`                               | 4          | 20         | 600      | 14,400  |
| **Safety & insights**                                   |            |            |          |         |
| `GET /v2/api/safety-score/{id}`                         | 10         | 30         | 6,000    | 144,000 |
| `GET /v2/api/battery-health`                            | 4          | 30         | 600      | 14,400  |
| **Webhooks**                                            |            |            |          |         |
| `POST /v2/api/webhooks`                                 | 4          | 20         | 600      | 14,400  |
| `GET /v2/api/webhooks`                                  | 4          | 20         | 600      | 14,400  |
| `GET /v2/api/webhooks/{id}`                             | 10         | 60         | 6,000    | 144,000 |
| `PUT /v2/api/webhooks/{id}`                             | 4          | 20         | 600      | 14,400  |
| `DELETE /v2/api/webhooks/{id}`                          | 4          | 20         | 600      | 14,400  |
| `POST /v2/api/webhooks/{id}/link-alert`                 | 4          | 20         | 600      | 14,400  |

¹ This endpoint returns the last known position for every vehicle in the company. Recommended usage is approximately **1 request every 5 minutes** (≈12/hour, ≈300/day) — polling more frequently than this is unlikely to yield new data and burns through the hourly/daily budget quickly.

² Each request queries **all vehicles in the company** simultaneously. The rate limit applies per request regardless of fleet size.

³ Accepts a maximum of **50 vehicle IDs** per request.

***

## Pagination

Most V2 list endpoints accept `page` (1-based, default `1`) and `pageSize` (default `50`) query parameters and return:

```json theme={null}
{
  "items": [...],
  "currentPage": 1,
  "numberOfPages": 7,
  "totalResults": 342,
  "lastResultIndex": 24
}
```

| Field             | Type    | Description                           |
| ----------------- | ------- | ------------------------------------- |
| `items`           | array   | Records on the current page           |
| `currentPage`     | integer | Current page number (1-based)         |
| `numberOfPages`   | integer | Total number of pages                 |
| `totalResults`    | integer | Total records across all pages        |
| `lastResultIndex` | integer | Index of the last record on this page |

<Note>
  `GET /v2/api/battery-health` uses different envelope fields: `page`, `pageSize`, and `totalCount` (instead of `currentPage`, `numberOfPages`, `totalResults`). It also omits `lastResultIndex`.
</Note>

**V2 pagination parameters:**

| Parameter  | Default | Max   | Description                |
| ---------- | ------- | ----- | -------------------------- |
| `page`     | `1`     | —     | Page to retrieve (1-based) |
| `pageSize` | `50`    | `500` | Records per page           |

**Legacy V1 pagination parameters** (applies only to `POST /api/GetCompanyVehiclesBatteryHealth`):

| Parameter        | Default | Max   | Description                    |
| ---------------- | ------- | ----- | ------------------------------ |
| `pageNumber`     | `1`     | —     | Page to retrieve (1-based)     |
| `resultsPerPage` | `25`    | `500` | Records per page               |
| `searchString`   | —       | —     | Optional filter (name or IMEI) |

**Endpoints with pagination support:**

| Endpoint                                                | Parameters                                                           |
| ------------------------------------------------------- | -------------------------------------------------------------------- |
| `POST /api/GetCompanyVehiclesBatteryHealth` (V1 legacy) | `pageNumber` / `resultsPerPage`                                      |
| `GET /v2/api/companies/{id}/vehicles/last-position`     | `page` / `pageSize`                                                  |
| `GET /v2/api/companies/{id}/alert-records`              | `page` / `pageSize`                                                  |
| `GET /v2/api/companies/{id}/alerts`                     | `page` / `pageSize`                                                  |
| `POST /v2/api/vehicles/list`                            | `page` / `pageSize`                                                  |
| `GET /v2/api/vehicles/{id}/positions`                   | `page` / `pageSize`                                                  |
| `GET /v2/api/battery-health`                            | `page` / `pageSize` — returns `totalCount` instead of `totalResults` |
| All other V2 list endpoints                             | `page` / `pageSize`                                                  |

## Date and time

* **JSON serialization** for `DateTime` uses custom converters (`SimplifiedDateTimeConverter`) that write **without fractional seconds**, e.g. `2026-04-07T14:32:00`.
* **Request parameters**: pass ISO-like strings that `DateTime.Parse` accepts (e.g. `2026-04-07T14:32:00` or `2026-04-07T14:32:00Z` depending on client). Many actions call `DateTime.SpecifyKind(..., Utc)` in code—treat **replay and position** ranges as **UTC** unless the controller comment says otherwise.
* `TimeSpan` in JSON (e.g. trip `Duration`) uses standard .NET format.

## Null vs omitted

* Nullable reference fields may be **omitted** or **null** depending on serializer defaults; treat absent optional fields as null.
* For `PositionDto`, `isOnline` is only set in snapshot-based paths; record-based paths may omit or default it.
