Aerovy Platform logo

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:

  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). Your organization is derived from the key (you don't pass orgId yourself). 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 (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}:

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
      }
    }
  ]
}
  • 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:

RuleIf violated
The request must contain at least one frame400 Bad Request
Every metric name must exist on the Thing's Thing Type Definition400 Bad Request
Each metric value must be a scalar coercible to the metric's data type400 Bad Request
The Thing's type timeseries table must be initialized424 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:

  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:

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

This returns the most recent reading for the Thing. For history, ranges, and aggregates, see Querying data.

Recap

StepCallFrequency
Define metricsPOST /v2/definitions/metricsOnce per metric
Define a typePOST /v2/definitions/thing-typesOnce per device type
Register a devicePOST /v2/sites/{placeId}/thingsOnce per device
Send telemetryPOST /v2/thing/{thingId}/dataContinuously

The first three are setup; the fourth is the steady-state call. Once data is flowing, Querying data covers reading it back out.