Sensor Data
Push sensor readings into Haltless and query them back as latest values, time-series history, or aggregated summaries. Ingestion at `POST /api/v1/ingest` authenticates with an API key; every other endpoint on this page requires a bearer JWT.
Endpoints
| Method | Path | Description |
|---|---|---|
POST | /api/v1/ingest | Ingest a batch of readings (API key) |
GET | /api/v1/machines/{machine_id}/readings/latest | Latest value per metric |
GET | /api/v1/machines/{machine_id}/readings | Time-series history |
GET | /api/v1/machines/{machine_id}/summary | Aggregated stats per metric |
GET | /api/v1/machines/{machine_id}/tag-mappings | Tag mappings for a machine |
GET | /api/v1/machines/{machine_id}/baselines | Computed baselines per metric |
POST | /api/v1/ingest-sources | Create a polling source |
GET | /api/v1/ingest-sources | List polling sources |
GET | /api/v1/ingest-sources/{source_id} | Get a polling source |
PUT | /api/v1/ingest-sources/{source_id} | Update a polling source |
DELETE | /api/v1/ingest-sources/{source_id} | Delete a polling source |
POST | /api/v1/ingest-sources/{source_id}/test-connection | Test a source connection |
Ingest readings
POST /api/v1/ingest
Authenticates with an API key sent in the X-API-Key header, not a bearer JWT. Send up to 1,000 readings per request. See the API Keys guide for issuing keys and the Direct Ingestion guide for a walkthrough.
Reading fields
| Field | Type | Required | Constraints |
|---|---|---|---|
system_identifier | string | Yes¹ | The System the reading comes from (1–255 chars) |
machine_identifier | string | Yes¹ | The same thing under its earlier name; still accepted |
timestamp | string | Yes | ISO 8601 UTC; up to 5 minutes in the future and 3,650 days in the past |
metric_name | string | Yes | 1–255 chars |
value | number | Yes | Must be finite (no NaN or Infinity) |
unit | string | Yes | Unit label, for example celsius, mm/s, psi |
raw_tag | string | No | Original tag name from the source system |
idempotency_key | string | No | A UUID that dedupes a reading across retries |
¹ Send one of the two (both, if they agree). Each reading is routed to its System by this identifier, then to a machine by its tag: its raw_tag, or its metric_name when it has none. A tag the System has not sent before is created unassigned unless the System routes everything to its one machine (a machine created on its own; see Machines). A reading of an unassigned tag is stored but raises no alerts until the tag is assigned on the Systems page; assigning it brings those readings along.
curl -X POST https://api.haltless.io/api/v1/ingest \
-H "X-API-Key: <api-key>" \
-H "Content-Type: application/json" \
-d '{
"readings": [
{
"system_identifier": "CNC-001",
"timestamp": "2026-08-07T10:30:00Z",
"metric_name": "temperature",
"value": 72.5,
"unit": "celsius"
},
{
"system_identifier": "CNC-001",
"timestamp": "2026-08-07T10:30:00Z",
"metric_name": "vibration",
"value": 3.2,
"unit": "mm/s",
"raw_tag": "ns2_i1002"
}
]
}'
Response: 207 Multi-Status
{
"accepted_count": 2,
"rejected_count": 0,
"errors": [],
"unassigned_count": 0
}
When a reading names a System that is not one of your company's active Systems, the others are still stored and that reading is reported by its position in the batch. The entry's machine_identifier field holds the identifier the reading named:
{
"accepted_count": 1,
"rejected_count": 1,
"errors": [
{
"index": 1,
"machine_identifier": "UNKNOWN-001",
"error": "System identifier not found for this company: UNKNOWN-001"
}
],
"unassigned_count": 0
}
A reading that breaks the field constraints above is different: it fails the whole request with 422, and nothing in the batch is stored. See Errors.
Latest readings
GET /api/v1/machines/{machine_id}/readings/latest
Returns the most recent reading for each metric, keyed by metric name.
{
"machine_id": "550e8400-e29b-41d4-a716-446655440000",
"readings": {
"temperature": {
"timestamp": "2026-08-07T10:30:00Z",
"value": 72.5,
"unit": "celsius",
"raw_tag": null
},
"vibration": {
"timestamp": "2026-08-07T10:30:00Z",
"value": 3.2,
"unit": "mm/s",
"raw_tag": "ns2_i1002"
}
}
}
Time-series history
GET /api/v1/machines/{machine_id}/readings
Query historical readings, optionally downsampled to a fixed interval. When start and end are omitted, the last 24 hours are returned.
| Parameter | Type | Description |
|---|---|---|
start | datetime | Start of the range (ISO 8601 UTC) |
end | datetime | End of the range (ISO 8601 UTC) |
metric | string | Filter to a single metric (up to 255 chars) |
interval | string | Downsample bucket: 1m, 5m, 15m, 1h, 6h, 1d |
Response: 200 OK
[
{
"metric_name": "temperature",
"unit": "celsius",
"data_points": [
{ "timestamp": "2026-08-07T00:00:00Z", "value": 71.2 },
{ "timestamp": "2026-08-07T00:05:00Z", "value": 72.1 }
]
}
]
Machine summary
GET /api/v1/machines/{machine_id}/summary
Returns min, max, average, and latest values for each metric.
{
"machine_id": "550e8400-e29b-41d4-a716-446655440000",
"metrics": [
{
"metric_name": "temperature",
"unit": "celsius",
"min_value": 68.4,
"max_value": 87.3,
"avg_value": 73.1,
"latest_value": 72.5,
"latest_timestamp": "2026-08-07T10:30:00Z"
}
]
}
Tag mappings for a machine
GET /api/v1/machines/{machine_id}/tag-mappings
Returns every tag assigned to the machine, with the name it was given (standardized_name is null for a tag nobody has named yet). To create or correct names, see Machines.
[
{
"id": "9b2f...",
"tenant_id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"system_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"machine_id": "550e8400-e29b-41d4-a716-446655440000",
"raw_tag": "ns2_i1001_temp_motor_1",
"metric_name": "temperature",
"standardized_name": "motor_temperature",
"unit": "celsius",
"is_confirmed": true,
"confidence_score": 0.94,
"created_at": "2026-08-07T09:00:00Z",
"updated_at": "2026-08-07T09:05:00Z"
}
]
Metric baselines
GET /api/v1/machines/{machine_id}/baselines
Returns computed baseline statistics (mean and standard deviation) for each metric.
[
{
"id": "a1b2...",
"machine_id": "550e8400-e29b-41d4-a716-446655440000",
"metric_name": "temperature",
"mean_value": 72.8,
"std_dev": 1.9,
"min_value": 68.4,
"max_value": 79.0,
"sample_count": 420,
"window_start": "2026-07-31T00:00:00Z",
"window_end": "2026-08-07T00:00:00Z"
}
]
Polling sources
Polling sources let Haltless connect to an OPC-UA or Modbus endpoint and pull readings on a schedule, as an alternative to pushing to `/api/v1/ingest`. A source feeds a System, and its readings are routed to machines by tag exactly as pushed ones are. These endpoints require an operator or administrator role. A source is System set-up: creating or changing one requires access to every machine, and a user limited to some machine groups sees no sources (404 by id). Responses name the System (system_id, system_name); they no longer carry machine_id.
Create a source
POST /api/v1/ingest-sources
| Field | Type | Required | Description |
|---|---|---|---|
system_id | uuid | Yes¹ | System the source feeds |
machine_id | uuid | Yes¹ | Earlier form: the source feeds this machine's System |
name | string | Yes | Source name (1–200 chars) |
protocol | string | Yes | opcua or modbus (Modbus TCP). Spelled differently from a System's protocol (opc_ua, modbus_tcp) |
host | string | Yes | Externally reachable host (1–255 chars) |
port | integer | Yes | 1–65535 |
poll_interval_seconds | integer | No | 5–3600 (default 30) |
is_active | boolean | No | Whether polling is enabled (default true) |
config | object | No | Protocol-specific settings |
¹ Send system_id; machine_id is still accepted from older clients.
Response: 201 Created
{
"id": "c3d4...",
"tenant_id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"system_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"system_name": "Line 3 PLC",
"name": "Line 3 OPC-UA",
"protocol": "opcua",
"host": "opc.example.com",
"port": 4840,
"poll_interval_seconds": 30,
"is_active": true,
"config": {},
"last_polled_at": null,
"last_error": null,
"created_at": "2026-08-07T10:00:00Z",
"updated_at": "2026-08-07T10:00:00Z"
}
List, get, update, and delete
GET /api/v1/ingest-sources lists your sources and GET /api/v1/ingest-sources/{source_id} returns one. PUT /api/v1/ingest-sources/{source_id} updates a source and DELETE /api/v1/ingest-sources/{source_id} removes it (204 No Content).
Test a connection
POST /api/v1/ingest-sources/{source_id}/test-connection attempts to connect to the configured endpoint and reports the result.
{
"success": true,
"message": "Connected successfully."
}