Ingesting data
Sending telemetry into the Aerovy Platform
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:
- Define metrics (
POST /v2/definitions/metrics), once per metric. - Define a Thing Type (
POST /v2/definitions/thing-types), once per device type. - Register a Thing (
POST /v2/sites/{placeId}/things), once per device. - 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 asX-Api-Key, withwritescopes covering the resources you create and send data for (see Resource filters). Your organization is derived from the key (you don't passorgIdyourself). See 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. If you have a raw payload in your own shape, see Ingesting via templates.
Step 1: Define your metrics
A metric must exist before a Thing Type can reference it. Create one per measurable quantity.
POST /v2/definitions/metrics
X-Api-Key: avy...
Content-Type: application/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 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.
POST /v2/definitions/thing-types
X-Api-Key: avy...
Content-Type: application/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.
POST /v2/sites/<siteId>/things
X-Api-Key: avy...
Content-Type: application/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(orPOST /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}:
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:
{
"frames": [
{
"timestamp": 1718884800000,
"metrics": {
"status": 1,
"moduleSoC": 87.4,
"moduleSoH": 98.1,
"moduleVoltage": 52.3,
"moduleCurrent": 12.1
}
}
]
}
timestampis 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
metricsare 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.
{
"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. 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:
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:
{
"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 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:
- Create an Integration (
POST /v2/integrations) describing the connection and how its external resources map to your Things. - 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:
This returns the most recent reading for the Thing. For history, ranges, and aggregates, see 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 covers reading it back out.