Send readings over HTTP
POST a device reading to the ingest service with a device token. Payload, validation rules, error codes and rate limits.
One device sends its own readings to its own endpoint, with its own token. One device, one token, one call: each request carries one reading for one device, never more than one device at a time.
The request#
POST https://ingest.logable.app/ingest/<DEVICE_ID>
Authorization: Bearer <DEVICE_TOKEN>
Content-Type: application/json<DEVICE_ID> is the device's UUID. <DEVICE_TOKEN> is the token Logable showed once when you created the device. Both are on the device's Connection tab.
The body#
| Field | Type | Required | Description |
|---|---|---|---|
| timestamp | string | No | ISO 8601. Omit it and the server uses its own clock. Rejected if it is more than one hour from server time. |
| status | online | offline | error | No | The device reporting its own state. Omit it unless the device knows something the reading does not show. |
| data | object | Yes | The reading itself: field name to number, string or boolean. |
| metadata | object | No | Anything else you want stored with the reading, for example a firmware version. |
A complete body:
{
"timestamp": "2026-07-01T10:00:00.000Z",
"status": "online",
"data": {
"weight": 18750.5,
"status": "filling"
}
}The field names inside data come from the device type. See Device types for the exact list per type, with an example payload for each.
A reading may carry any subset of the type's fields. A missing field is not an error, so a device can report one value at a time.
Custom fields#
Any key in data that the device type does not declare is a custom data point. Logable stores it and shows it on a widget when you turn on Show custom data.
Custom data points never drive an alert, a trigger or a forecast. If you need a value to raise an alert, it has to be a field the device type declares.
A custom key must start with a letter and contain only letters, digits or underscores. The limits below cap how many a payload may carry and how long a key or a string value may be.
| Limit | Value | Description |
|---|---|---|
| custom keys | 20 | Maximum number of custom (unregistered) keys in one payload. |
| key length | 64 | Maximum characters in a custom key. |
| string value length | 256 | Maximum characters in a custom key's string value. |
The response#
{
"acknowledged": true,
"timestamp": "2026-07-01T10:00:00.123Z",
"deviceId": "0d9f7f52-3a1c-4a6e-9b2d-1f4e5a6b7c8d"
}The timestamp in the response is the time Logable stored the reading, which is not always the time you sent in the body.
Code samples#
These are generated from the same registry the platform validates against, so they cannot drift from what the service accepts. Replace the placeholder id and token with the values from the device's Connection tab.
# Raw Meal Silo - Quick Test (curl)
# Device Type: silo | Device ID: <DEVICE_ID>
#
# The "data" fields shown are this device type's registered metrics.
# Optionally add your OWN custom fields to "data" (e.g. "totalizer": 1284300.0).
# Custom keys: start with a letter; letters/digits/underscores; max 64 chars.
# Up to 20 custom fields; values: number, string (max 256 chars) or boolean.
# Custom values appear on dashboards via a widget's "Show Custom Data" option.
curl -X POST "https://ingest.logable.app/ingest/<DEVICE_ID>" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <DEVICE_TOKEN>" \
-d '{"timestamp":"2026-07-01T10:00:00.000Z","status":"online","data":{"weight":18750.5,"status":"filling"}}'"""
Raw Meal Silo - Logable HTTP Integration (Python)
Device Type: silo
Device ID: <DEVICE_ID>
Installation: pip install requests
"""
import requests
from datetime import datetime, timezone
API_ENDPOINT = "https://ingest.logable.app/ingest/<DEVICE_ID>"
API_TOKEN = "<DEVICE_TOKEN>"
def send_data():
# The "data" fields shown are this device type's registered metrics.
# Optionally add your OWN custom fields to "data" (e.g. "totalizer": 1284300.0).
# Custom keys: start with a letter; letters/digits/underscores; max 64 chars.
# Up to 20 custom fields; values: number, string (max 256 chars) or boolean.
# Custom values appear on dashboards via a widget's "Show Custom Data" option.
payload = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"status": "online",
"data": {
"weight": 18750.5,
"status": "filling"
}
}
response = requests.post(
API_ENDPOINT,
json=payload,
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {API_TOKEN}"
},
timeout=10
)
response.raise_for_status()
print(f"Data sent successfully: {response.status_code}")
if __name__ == "__main__":
send_data()/**
* Raw Meal Silo - Logable HTTP Integration (TypeScript)
* Device Type: silo
* Device ID: <DEVICE_ID>
*
* Runtime: Node.js 18+ or modern browser (uses built-in fetch)
*/
const API_ENDPOINT = "https://ingest.logable.app/ingest/<DEVICE_ID>";
const API_TOKEN = "<DEVICE_TOKEN>";
async function sendData(): Promise<void> {
// The "data" fields shown are this device type's registered metrics.
// Optionally add your OWN custom fields to "data" (e.g. "totalizer": 1284300.0).
// Custom keys: start with a letter; letters/digits/underscores; max 64 chars.
// Up to 20 custom fields; values: number, string (max 256 chars) or boolean.
// Custom values appear on dashboards via a widget's "Show Custom Data" option.
const payload = {
timestamp: new Date().toISOString(),
status: "online",
data: {
"weight": 18750.5,
"status": "filling"
},
};
const response = await fetch(API_ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${API_TOKEN}`,
},
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
console.log("Data sent successfully:", response.status);
}
sendData();#!/bin/bash
# Raw Meal Silo - Logable HTTP Integration (Bash)
# Device Type: silo
# Device ID: <DEVICE_ID>
API_ENDPOINT="https://ingest.logable.app/ingest/<DEVICE_ID>"
API_TOKEN="<DEVICE_TOKEN>"
# NOTE: the heredoc below is pure JSON, so it cannot contain comments.
# The "data" fields shown are this device type's registered metrics.
# Optionally add your OWN custom fields to "data" (e.g. "totalizer": 1284300.0).
# Custom keys: start with a letter; letters/digits/underscores; max 64 chars.
# Up to 20 custom fields; values: number, string (max 256 chars) or boolean.
# Custom values appear on dashboards via a widget's "Show Custom Data" option.
# Quoted heredoc delimiter: '$' in metric values is taken literally.
# "timestamp" is optional - the platform stamps arrival time when omitted.
read -r -d '' PAYLOAD <<'EOF'
{
"status": "online",
"data": {
"weight": 18750.5,
"status": "filling"
}
}
EOF
HTTP_CODE=$(curl -s -w "%{http_code}" -o /dev/null \
-X POST "${API_ENDPOINT}" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${API_TOKEN}" \
-d "${PAYLOAD}")
if [ "${HTTP_CODE}" -ge 200 ] && [ "${HTTP_CODE}" -lt 300 ]; then
echo "Data sent successfully (HTTP ${HTTP_CODE})"
exit 0
else
echo "Error sending data (HTTP ${HTTP_CODE})"
exit 1
fi<#
.SYNOPSIS
Raw Meal Silo - Logable HTTP Integration (PowerShell)
Device Type: silo
Device ID: <DEVICE_ID>
#>
$ApiEndpoint = "https://ingest.logable.app/ingest/<DEVICE_ID>"
$ApiToken = "<DEVICE_TOKEN>"
function Send-DeviceData {
$Timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
# The "data" fields shown are this device type's registered metrics.
# Optionally add your OWN custom fields to "data" (e.g. "totalizer": 1284300.0).
# Custom keys: start with a letter; letters/digits/underscores; max 64 chars.
# Up to 20 custom fields; values: number, string (max 256 chars) or boolean.
# Custom values appear on dashboards via a widget's "Show Custom Data" option.
$Payload = @{
timestamp = $Timestamp
status = "online"
data = @{
weight = 18750.5
status = 'filling'
}
} | ConvertTo-Json -Depth 3
$Headers = @{
"Content-Type" = "application/json"
"Authorization" = "Bearer $ApiToken"
}
try {
$Response = Invoke-RestMethod `
-Uri $ApiEndpoint `
-Method Post `
-Headers $Headers `
-Body $Payload `
-TimeoutSec 10
Write-Host "Data sent successfully" -ForegroundColor Green
return $Response
} catch {
Write-Error "Error: $($_.Exception.Message)"
throw
}
}
Send-DeviceData/*
* Raw Meal Silo - Logable HTTP Integration (C / libcurl)
* Device Type: silo
* Device ID: <DEVICE_ID>
*
* Install: sudo apt-get install libcurl4-openssl-dev
* Compile: gcc -o device_client device_client.c -lcurl
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <curl/curl.h>
#define API_ENDPOINT "https://ingest.logable.app/ingest/<DEVICE_ID>"
#define API_TOKEN "<DEVICE_TOKEN>"
/* Example metric payload - CUSTOMIZE DATA HERE */
static const char *DATA_JSON =
"{\"weight\":18750.5,\"status\":\"filling\"}";
int main(void) {
CURL *curl;
CURLcode res;
struct curl_slist *headers = NULL;
time_t now = time(NULL);
char timestamp[64];
strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%SZ", gmtime(&now));
/* Build JSON payload - CUSTOMIZE DATA HERE */
/*
* The "data" fields shown are this device type's registered metrics.
* Optionally add your OWN custom fields to "data" (e.g. "totalizer": 1284300.0).
* Custom keys: start with a letter; letters/digits/underscores; max 64 chars.
* Up to 20 custom fields; values: number, string (max 256 chars) or boolean.
* Custom values appear on dashboards via a widget's "Show Custom Data" option.
*/
/* Metric data is passed as a %s argument (never spliced into the
* format string), so '%' characters in values stay intact. */
char payload[1024];
snprintf(payload, sizeof(payload),
"{\"timestamp\": \"%s\", \"status\": \"online\", \"data\": %s}",
timestamp, DATA_JSON);
curl_global_init(CURL_GLOBAL_DEFAULT);
curl = curl_easy_init();
if(curl) {
curl_easy_setopt(curl, CURLOPT_URL, API_ENDPOINT);
headers = curl_slist_append(headers, "Content-Type: application/json");
char auth_header[256];
snprintf(auth_header, sizeof(auth_header), "Authorization: Bearer %s", API_TOKEN);
headers = curl_slist_append(headers, auth_header);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload);
res = curl_easy_perform(curl);
if(res != CURLE_OK) {
fprintf(stderr, "Request failed: %s\n", curl_easy_strerror(res));
} else {
printf("Data sent successfully\n");
}
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
}
curl_global_cleanup();
return 0;
}/*
* Raw Meal Silo - Logable HTTP Integration (Arduino / ESP32)
* Device Type: silo
* Device ID: <DEVICE_ID>
*
* Libraries: WiFi, HTTPClient, ArduinoJson
* Board: ESP32 or ESP8266
*/
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
const char* WIFI_SSID = "YOUR_WIFI_SSID";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* API_ENDPOINT = "https://ingest.logable.app/ingest/<DEVICE_ID>";
const char* API_TOKEN = "<DEVICE_TOKEN>";
void setup() {
Serial.begin(115200);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("Connecting to WiFi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println(" Connected!");
}
void sendMetrics() {
if (WiFi.status() != WL_CONNECTED) {
Serial.println("WiFi not connected");
return;
}
HTTPClient http;
http.begin(API_ENDPOINT);
http.addHeader("Content-Type", "application/json");
char authHeader[256];
snprintf(authHeader, sizeof(authHeader), "Bearer %s", API_TOKEN);
http.addHeader("Authorization", authHeader);
/* Build JSON payload - CUSTOMIZE DATA HERE */
JsonDocument doc;
doc["status"] = "online";
doc.createNestedObject("data");
doc["data"]["weight"] = 18750.5;
doc["data"]["status"] = "filling";
// The "data" fields shown are this device type's registered metrics.
// Optionally add your OWN custom fields to "data" (e.g. "totalizer": 1284300.0).
// Custom keys: start with a letter; letters/digits/underscores; max 64 chars.
// Up to 20 custom fields; values: number, string (max 256 chars) or boolean.
// Custom values appear on dashboards via a widget's "Show Custom Data" option.
String payload;
serializeJson(doc, payload);
int httpCode = http.POST(payload);
if (httpCode > 0) {
Serial.printf("Data sent (HTTP %d)\n", httpCode);
} else {
Serial.printf("HTTP error: %s\n", http.errorToString(httpCode).c_str());
}
http.end();
}
void loop() {
sendMetrics();
delay(10000); /* Send every 10 seconds */
}Payload limits#
These bound the whole payload, whether a key is a declared field or a custom one.
| Limit | Value | Description |
|---|---|---|
| data keys | 100 | Maximum number of keys in the data object. |
| key length | 100 | Maximum characters in any data or metadata key. |
| string value length | 1000 | Maximum characters in a string value. |
| metadata keys | 50 | Maximum number of keys in the metadata object. |
| metadata size | 16 KB | Maximum serialised size of the metadata object. |
| nesting depth | 32 | Maximum object nesting Logable walks before it rejects the payload. |
Errors#
Every error carries a JSON body with an error message and, in most cases, a code.
| Status | Code | Description |
|---|---|---|
| 400 | VALIDATION_ERROR | The body broke the schema, a limit, or the device type's own field rules. |
| 401 | UNAUTHORIZED | The Authorization header is missing, or the token does not belong to this device. |
| 403 | INGESTION_BLOCKED | Ingestion is blocked for this device's organization. Contact the organization owner. |
| 404 | NOT_FOUND | No route matches this path or method. Check the URL. |
| 429 | RATE_LIMIT_EXCEEDED | This device sent too many requests. The body carries a retryAfter of 1 second. |
| 500 | INTERNAL_ERROR | Something failed inside Logable. Details are deliberately omitted. |
A 429 body:
{
"error": "Too many requests",
"code": "RATE_LIMIT_EXCEEDED",
"details": {
"message": "Rate limit: 10 requests per 1000ms per device",
"retryAfter": 1
}
}A second body, with per organization in the message, means the organization's combined rate is over its ceiling.
Rate limits#
Logable applies four limits, in two services: three in ingest, one in the API. They are not interchangeable.
| Service | Limit | Description |
|---|---|---|
| Ingest over HTTP | 10 per second | Per device or per stream. Over the limit, the request gets a 429 with retryAfter. |
| Ingest over MQTT | 10 per second | Per device or per stream. Over the limit, the message is dropped without a reply. The device log shows the drop. |
| Ingest, per organization | 200 per second | Across every device and stream the organization owns, on both transports. A gateway that fronts many devices counts against its organization, not against an address. |
| Logable API | 100 per minute | Per signed-in user. The web and mobile apps stay under it. Devices never touch it. |
Streams#
A stream carries data that is not a device: a spreadsheet export, a line from another system, a manual reading. Streams have their own endpoint, their own token, and a body with no status field. Everything else on this page applies to them unchanged.