# Send readings over HTTP (https://logable.app/docs/integrate/rest-api)

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 [#the-request]

```http title="HTTP 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 [#the-body]

Table: see https://logable.app/docs/integrate/rest-api#the-body

A complete body:

```json title="Request 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](/docs/reference/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 [#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 [#the-response]

```json title="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 [#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.

#### cURL

```bash
# 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"}}'
```

#### Python

```python
"""
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()
```

#### TypeScript

```typescript
/**
 * 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();
```

#### Bash

```bash
#!/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
```

#### PowerShell

```powershell
<#
.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
```

#### C (libcurl)

```c
/*
 * 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;
}
```

#### Arduino / ESP32

```cpp
/*
 * 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 [#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 [#errors]

Every error carries a JSON body with an `error` message and, in most cases, a `code`.

Table: see https://logable.app/docs/integrate/rest-api#errors

A 429 body:

```json
{
  "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 [#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 [#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.

## Read next [#read-next]

* [Publish readings over MQTT](/docs/integrate/mqtt)
* [Device types](/docs/reference/device-types)
* [Get started with Logable](/docs/guides/getting-started)
