Skip to content

Sending telemetry

Telemetry is the whole point of connecting a device. Whatever transport you use — HTTP, MQTT, CoAP, LwM2M, SNMP — every reading is normalized into one internal shape: the TelemetryPoint. This page is the definitive reference for that shape, the decoders that turn your wire format into it, the validation a point must pass, and what CORE-M does with a point once it is accepted.

A TelemetryPoint is one reading from one device at one instant. It carries an identity, a timestamp, and two maps of named values:

message TelemetryPoint {
string device_id = 1;
string tenant_id = 2;
google.protobuf.Timestamp timestamp = 3;
map<string, double> numeric_values = 4;
map<string, string> string_values = 5;
}
FieldMeaning
device_idCORE-M’s identifier for the device (not the hardware_id).
tenant_idThe owning tenant. Derived from your credential — you never set it to something your key doesn’t own.
timestampWhen the reading was taken, RFC 3339 on the wire. Truncated to whole seconds for anchoring — see below.
numeric_valuesMap of metric name → double. Use for anything measured: temperature, humidity, rpm, battery_v.
string_valuesMap of metric name → string. Use for states and labels: status, mode, firmware.

Pick the map by what the value is, not by how it happens to print:

  • Numeric for measurements you’ll chart, threshold, or aggregate. 22.5, 1013.2, -40.0. Even integers go here as doubles.
  • String for discrete states and identifiers you’ll filter or display. "online", "heating", "v2.0.0".

A single point can mix both — they share the same timestamp:

{
"numeric_values": { "temperature": 22.5, "humidity": 65 },
"string_values": { "status": "ok", "mode": "auto" }
}

The timestamp is Unix seconds for anchoring

Section titled “The timestamp is Unix seconds for anchoring”

On the wire the timestamp is RFC 3339 (2026-05-29T14:30:00Z). But when a point is hashed for blockchain anchoring, that timestamp is taken as Unix seconds — nanoseconds are discarded. The canonical hash is:

SHA256( device_id_utf8 || timestamp_uint64_be(seconds) || jcs_payload_utf8 )

where jcs_payload_utf8 is the payload canonicalized per RFC 8785 (JCS).

This matters because a third-party verifier may only have second precision. By truncating to whole seconds, CORE-M guarantees the verifier can reproduce the exact same hash from the same logical reading. If you send two points for the same device in the same second, they hash against the same timestamp — give them distinct values or space them at least a second apart if each must anchor independently. See Anchoring and Merkle proofs for how that hash becomes an on-chain proof.

Devices rarely speak protobuf natively. CORE-M’s ingest adapters decode common wire formats into the TelemetryPoint shape. The canonical HTTP path (POST /api/v1/telemetry, port 8080) takes JSON; MQTT and CoAP devices can use JSON, CBOR, binary protobuf, CSV, or a custom field mapping configured on the device profile.

The default and simplest format. The keys map straight onto the TelemetryPoint fields.

POST /api/v1/telemetry HTTP/1.1
Host: api.kronoxdata.com:8080
Authorization: Bearer sk_live_9Qk3pR2wXn7vJ4mB...
Content-Type: application/json
{
"points": [
{
"device_id": "dev_8f3a",
"timestamp": "2026-05-29T14:30:00Z",
"numeric_values": { "temperature": 22.5, "humidity": 65 },
"string_values": { "status": "ok" }
}
]
}

A decoded point is not yet accepted. It must pass validation first.

  1. Authentication. The credential must be valid and authorized to ingest for the claimed tenant. An unauthenticated point is rejected and never published; metric telemetry_rejected_total{reason="auth"} increments.

  2. Known device. The device_id must exist in the tenant’s registry. If it doesn’t, the point is dropped silently — not published to the validated subject — and telemetry_dropped_total{reason="unknown_device"} increments, with a warning log carrying trace context. This is the single most common cause of “my data isn’t showing up”: the device wasn’t provisioned, or you’re sending the wrong device_id.

  3. Schema validation (if the profile defines one). If the device’s profile carries a telemetry_schema, the point is checked against it. A profile that requires numeric key temperature rejects a point that omits it with INVALID_ARGUMENT, and corem_telemetry_schema_rejections_total{profile="…"} increments. Profiles with no schema accept any well-formed point.

Once a point passes validation, a lot happens — and the device doesn’t wait for any of it. The edge acknowledges immediately and the point propagates asynchronously:

flowchart TB
  IN["Accepted point<br/>telemetry.raw.{tenant}"] --> VAL["validate + enrich<br/>(known device, schema, last_seen)"]
  VAL --> VD["telemetry.validated.{tenant}"]
  VD --> HOT[("Aerospike hot<br/>CDT list · 900s TTL")]
  VD --> COLD[("TimescaleDB cold<br/>historical archive")]
  VD --> LIVE["telemetry.live.{tenant}.{device}<br/>real-time fan-out"]
  VD --> ANC["queued for anchoring<br/>(Merkle batch → on-chain)"]

Concretely, on acceptance CORE-M:

  • Updates last_seen on the device record, and if the device was offline, flips it to online and publishes a status-change event.
  • Enriches the point with the device’s name, groups, and tags.
  • Dual-writes it: appended to the hot Aerospike CDT list (keyed {tenant}:{device}:{metric}, ~900-second TTL window — a fast cache, not the system of record) and inserted into the cold TimescaleDB hypertable (batched for throughput) as the durable historical record.
  • Fans out to telemetry.live.{tenant}.{device} so dashboards and WebSocket subscribers see it in real time.
  • Queues it for anchoring — its hash joins a Merkle batch that is later committed to the blockchain.

The hot write failing does not block the cold write; the cold path always proceeds. See Telemetry query & retention for how the hot and cold stores are queried and how long data is kept.