---
title: "Ingesting data"
description: "Sending telemetry into the Aerovy Platform"
icon: "arrow-up-from-bracket"
---

> **For AI agents:** the complete documentation index is at [llms.txt](/llms.txt). Append `.md` to any page URL for its markdown version.

Before a reading can be accepted, the platform needs to know what kind of device it's coming
from. Ingestion is a short pipeline, not a single call:

1. **Define metrics** (`POST /v2/definitions/metrics`), once per metric.
2. **Define a Thing Type** (`POST /v2/definitions/thing-types`), once per device type.
3. **Register a Thing** (`POST /v2/sites/{placeId}/things`), once per device.
4. **Send telemetry** (`POST /v2/thing/{thingId}/data`), for every reading.

Steps 1–3 are one-time setup. Step 4 is the call you make for every batch of readings.


> **Prerequisites:** an API key (prefix `avy`) sent as `X-Api-Key`, with `write` scopes
> covering the resources you create and send data for (see
> [Resource filters](/platform/resource-filters)). Your organization is derived from the key
> (you don't pass `orgId` yourself). See [API fundamentals](/platform/api-fundamentals).

If your data comes from a supported third party (Geotab, Salesforce, UtilityAPI…), you may not
need steps 1–4 at all. See [Ingesting via integrations](#ingesting-via-integrations). If you
have a raw payload in your own shape, see [Ingesting via templates](#ingesting-via-templates).

---

## Step 1: Define your metrics

A metric must exist before a Thing Type can reference it. Create one per measurable quantity.

```http
POST /v2/definitions/metrics
X-Api-Key: avy...
Content-Type: application/json
```
```json
{
  "name": "soc",
  "displayName": "State of Charge",
  "description": "Battery state of charge, 0–100",
  "dataType": "Double",
  "defaultAggregation": "Last",
  "unitLabel": "%"
}
```

`name`, `dataType`, and `defaultAggregation` are required. The response returns the metric's
id (`mdef_…`). Repeat for every metric your device reports (`soh`, `moduleVoltage`, …). See
[Data model](/platform/data-model#metric-definition) for field details
and enum-shaped metrics.

## Step 2: Define a Thing Type

Tie your metrics together into a device type, and declare any static properties.

```http
POST /v2/definitions/thing-types
X-Api-Key: avy...
Content-Type: application/json
```
```json
{
  "displayName": "Battery",
  "description": "Aerovy battery",
  "metricIds": ["mdef_<soc>", "mdef_<soh>", "mdef_<moduleVoltage>"],
  "propertyDefinitions": [
    {
      "name": "capacity",
      "description": "Battery capacity",
      "unit": "kWh",
      "valueType": "double",
      "constraints": [
        { "operator": ">",  "value": "0" },
        { "operator": "<=", "value": "100" }
      ]
    }
  ]
}
```

`displayName` and `metricIds` are required. The response returns the **stable type id**
(`tdefi_…`) and its **first version id** (`tdef_…`). You'll reference the type when
registering Things. To evolve the type later, create a new version with
`POST /v2/definitions/thing-types/{thingTypeId}/versions`. Existing Things keep the version
they were created with.

## Step 3: Register the Thing

Create a device of that type, inside a Place (a Site or Fleet you've already created). The
Place is part of the URL: post to `/v2/sites/{placeId}/things` for a Site, or
`/v2/fleets/{placeId}/things` for a Fleet, where `{placeId}` is the Place's id.

```http
POST /v2/sites/<siteId>/things
X-Api-Key: avy...
Content-Type: application/json
```
```json
{
  "thingName": "Battery A1",
  "thingType": "Battery",
  "thingTypeId": "tdefi_<batteryType>",
  "thingDescription": "Rack 1, slot A1",
  "manufacturer": "Aerovy",
  "model": "AVY-100",
  "latitude": 37.7749,
  "longitude": -122.4194,
  "properties": { "capacity": "75" }
}
```

Only `thingName` is required. You'll almost always set `thingTypeId` too (the `tdefi_…` from
step 2), so the platform knows the device's metric contract. `properties` are validated
against the type's Property Definitions: an unknown key or out-of-range value is rejected.
A successful call returns **`201 Created`** and the Thing's id (`<thingId>`).

> To register many devices at once, `POST /v2/sites/{placeId}/things/bulk` (or
> `POST /v2/fleets/{placeId}/things/bulk`) accepts an array of the same request body.

## Step 4: Send telemetry

Post readings for a specific Thing. The Thing is identified by the route, so the path
carries only its `{thingId}`:

```http
POST /v2/thing/{thingId}/data
X-Api-Key: avy...
Content-Type: application/json
```

The body is a batch of **frames**. Each frame has a `timestamp` and a `metrics` map of
metric name to value, so one request can stream or backfill many readings at once:

```json
{
  "frames": [
    {
      "timestamp": 1718884800000,
      "metrics": {
        "status": 1,
        "moduleSoC": 87.4,
        "moduleSoH": 98.1,
        "moduleVoltage": 52.3,
        "moduleCurrent": 12.1
      }
    }
  ]
}
```

- `timestamp` is the reading's event time as **Unix epoch milliseconds (UTC)**, not the time
  the platform received it, so you can backfill or buffer and send later.
- The keys in `metrics` are metric **names**. They're matched **case-insensitively** against
  the Thing's active Thing Type Definition and mapped to their immutable metric ids before
  being written. Values are scalars (number, boolean, or string) and are coerced to each
  metric's declared data type.
- At least one frame is required, and each frame needs at least one metric.

### Validation rules

The platform validates each frame against the Thing's type, and rejects mismatches:

| Rule | If violated |
|------|-------------|
| The request must contain at least one frame | `400 Bad Request` |
| Every metric name must exist on the Thing's Thing Type Definition | `400 Bad Request` |
| Each metric value must be a scalar coercible to the metric's data type | `400 Bad Request` |
| The Thing's type timeseries table must be initialized | `424 Failed Dependency` |

### Response

A successful call returns **`200 OK`** with an ingest summary: the `thingId`, the number of
`framesIngested`, and the distinct `metrics` written across the frames.

```json
{
  "thingId": "<thingId>",
  "framesIngested": 1,
  "metrics": ["status", "moduleSoC", "moduleSoH", "moduleVoltage", "moduleCurrent"]
}
```

---

## Ingesting via templates

When your data arrives as a **raw third-party JSON payload** rather than the `frames` shape
above, you can map it on the way in with a [Template](/platform/templates). A template is a
reusable mapping from an external payload onto your metric definitions, so you don't have to
reshape the payload yourself.

Apply a `ThingData` template to a Thing by POSTing the raw payload:

```http
POST /v2/thing/{thingId}/data/template/{templateId}?previewOnly=false
X-Api-Key: avy...
Content-Type: application/json
```

The body is the **raw external JSON**, sent as-is. The platform loads the Thing, resolves its
active Thing Type Definition, maps the payload through the template, writes the valid readings,
and returns a processing report:

```json
{
  "previewOnly": false,
  "templateId": "tmpl_abc123",
  "templateRevision": 7,
  "recordsParsed": 100,
  "readingsWritten": 98,
  "metricValuesWritten": 540,
  "recordsSkipped": [
    { "index": 12, "reason": "invalid_timestamp", "value": "n/a" }
  ]
}
```

Set `previewOnly=true` to map and report **without persisting** — useful for validating a
template against sample data before you commit to it. The template's `target` must be
`ThingData`, and the Thing must carry a Thing Type Definition. See
[Templates](/platform/templates) for how to author, test, and manage templates.

---

## Ingesting via integrations

You don't always have to POST data yourself. If your devices report through a supported
platform (**Geotab**, **Zubie**, **UtilityAPI**, and others), you can
configure an **Integration** instead:

1. Create an Integration (`POST /v2/integrations`) describing the connection and how its
   external resources map to your Things.
2. The platform then receives or pulls data from that system and routes it onto the mapped
   Things automatically, with no per-reading API calls on your side.

This is the preferred path when a first-party connector exists for your hardware. Direct
ingestion (steps 1–4 above) is for custom devices or systems without a connector.

---

## Verify

Confirm readings landed by querying them back:

```http
GET /v2/things/{thingId}/latest-event
```

This returns the most recent reading for the Thing. For history, ranges, and aggregates, see
[Querying data](/platform/querying-data).

## Recap

| Step | Call | Frequency |
|------|------|-----------|
| Define metrics | `POST /v2/definitions/metrics` | Once per metric |
| Define a type | `POST /v2/definitions/thing-types` | Once per device type |
| Register a device | `POST /v2/sites/{placeId}/things` | Once per device |
| Send telemetry | `POST /v2/thing/{thingId}/data` | Continuously |

The first three are setup; the fourth is the steady-state call. Once data is flowing,
[Querying data](/platform/querying-data) covers reading it back out.
