Base URL
Replace with your actual host (local or production), for example:
http://localhostor
https://agrogat.comSee sensors-dragino-lse01.md for the LSE01 field contract, LoRa→HTTP forwarding, and the difference between simulator and live mode.
Receives and stores sensor readings. Does not require dashboard login, but a valid X-API-Key is mandatory.
Rows are linked to a gateway via GATEWAY_CODE in .env (resolved against gateways.id). If the database is missing the gateway_id column, ingest.php falls back to inserting without that column.
Headers
| Header | Value | Required |
|---|---|---|
Content-Type |
application/json |
Yes |
X-API-Key |
Your API key (INGEST_API_KEY) |
Yes |
Request body
{
"sensor_id": "string (required)",
"payload": { }
}| Field | Type | Required | Description |
|---|---|---|---|
sensor_id |
string | Yes | Unique sensor identifier |
payload |
object/any | No | Measurement data — any JSON object. If payload is omitted, the full request object is stored (subject to sensor_id handling in code) |
Maximum request body size: 1 MB
Success response — 200 OK
{
"status": "success",
"id": "42"
}Error responses
| HTTP code | Cause |
|---|---|
400 Bad Request |
Missing sensor_id or invalid JSON |
401 Unauthorized |
Missing or invalid X-API-Key |
415 Unsupported Media Type |
Content-Type is not application/json |
500 Internal Server Error |
Internal server error (see server log) |
Example 401 response:
{
"status": "error",
"message": "Unauthorized"
}- The dashboard API and
sync_receiveare protected by separate keys (SYNC_API_KEYmust be set on the central server to accept push).
Used by gateway instances (via scripts/sync_push_to_central.php) to send batches of readings to a central server. Does not require a dashboard session.
| Header | Value |
|---|---|
Content-Type |
application/json |
X-Sync-Key |
Same string as SYNC_API_KEY in the central server's .env |
Central server: SYNC_API_KEY must be set (non-empty). Otherwise the endpoint responds with 503 and sync.not_configured.
Body (summary):
{
"cluster": "TRAT",
"source_gateway_code": "TRAT-GATEWAY-001",
"readings": [
{
"id": 1,
"sensor_id": "sensor-a",
"data": { },
"received_at": "2026-05-03T12:00:00+00:00",
"gateway_code": "TRAT-GATEWAY-001"
}
]
}source_gateway_code is always the source that owns the numeric id values in the batch. Each row must have a matching active gateways record on the central server for gateway_code (used for sensor_data.gateway_id).
Success: {"success":true,"inserted":N,"skipped":M,"errors":E,...}
Used by scripts/sync_push_entities_to_central.php when SYNC_ENTITIES_ENABLE is enabled on the gateway. Same authentication as sync_receive (X-Sync-Key = SYNC_API_KEY). The receiver upserts clients (by email) and sensors (unique sensor_id). The sender uses the same base URLs as for readings; only the filename changes from sync_receive.php to sync_entities_receive.php.
| Header | Value |
|---|---|
Content-Type |
application/json |
X-Sync-Key |
Same string as SYNC_API_KEY on the receiver |
Body (summary):
{
"source_gateway_code": "TRAT-GATEWAY-001",
"clients": [
{
"company_name": "Example Ltd",
"email": "contact@example.com",
"country_code": "NO",
"currency_code": "NOK",
"active": true,
"default_gateway_code": "TRAT-GATEWAY-001"
}
],
"sensors": [
{
"sensor_id": "sensor-a",
"client_email": "contact@example.com",
"country_code": "NO",
"active": true,
"gateway_code": "TRAT-GATEWAY-001"
}
]
}country_code must be exactly two characters (ISO 3166-1 alpha-2). For sensors, client_email must exist on the receiver after the client list is applied (clients are processed first).
Success: {"success":true,"clients_upserted":N,"sensors_upserted":M,"errors":E}. If errors > 0, the sender does not advance the watermark (watermark_ts, channel entities_push).
Dashboard API (/api/*.php)
These endpoints are used by dashboard.php (JavaScript) and require an authenticated session (cookie after successful login via login.php / api/auth.php). Unauthenticated calls typically receive 401 with a JSON error.
Overview (all under /api/):
| File | Method / purpose | |
|---|---|---|
auth.php |
POST — login / logout (JSON action, email, password) |
|
me.php |
GET — current user and role | |
locale.php |
POST — update language preference (en, no, th) |
|
clients.php |
CRUD for clients (role restrictions in code) | |
sensors.php |
CRUD for sensors | |
gateways.php |
CRUD for gateways incl. optional latitude / longitude |
|
users.php |
User administration | |
data.php |
GET — sensor data with joins (sensor, client, gateway code) | |
gateway_map.php |
GET — gateway (from GATEWAY_CODE in .env) and sensors with coordinates linked to that gateway |
|
simulator.php |
Simulator / LIVE mode and data generation | |
reset.php |
Controlled table reset (after UI confirmation) | |
sync_receive.php |
POST — receive sensor_data batch from gateway (X-Sync-Key) — see dedicated section |
|
sync_entities_receive.php |
POST — receive clients + sensors batch (X-Sync-Key) — see dedicated section |
|
geo_status.php |
GET — ground stability last 24h (pore pressure, temperature, geo_status) |
|
permafrost_depth.php |
GET — thaw depth from thermistor JSONB (`?sensor_id=&at=latest\ | history`) |
geo_trends.php |
GET — monthly averages Jan–May + linear forecast toward 0°C | |
runway_status.php |
GET — runway VWC, estimated penetration, and status per section |
Detailed JSON contracts follow the implementation in each PHP file; the common pattern is application/json and error objects with success: false where applicable.
Geotechnical module (JSONB in sensor_data.data)
All geotechnical measurements are stored in the existing sensor_data table via ingest.php. Set type in payload and use the fields below. Thresholds are configured in lib/operations.php → geo (and evaluated in lib/geo_payload.php).
Ingest — Dragino RS485 (LoRaWAN, hex → pore pressure)
When the network server sends raw hex or base64 from a Dragino RS485 node, ingest.php can decode water column height (metres) and store it under pore_pressure in JSONB.
{
"sensor_id": "JM-PIEZO-01",
"payload_hex": "0B45014041A00000"
}Alternatively: hex, data (base64), or payload.payload_hex. Without the Dragino header (Modbus data only): set RS485_HEADER_SKIP_BYTES=0 in .env.
| .env | Default | Description |
|---|---|---|
RS485_HEADER_SKIP_BYTES |
3 |
Skip battery (2) + payload version (1) on FPort 2 |
RS485_DATA_OFFSET |
1 |
Extra offset before float (often 1 Modbus byte after header) |
RS485_DECODE_FORMAT |
float32_be |
float32_be, float32_le, uint16_mm, int16_mm, uint32_mm |
Stored payload (excerpt):
{
"type": "piezometer",
"pore_pressure": 20.0,
"pore_pressure_unit": "m",
"pore_pressure_kpa": 196.133,
"source": "dragino_rs485_lorawan",
"raw_hex": "0b45014041a00000"
}Alarms use pore_pressure_kpa (calculated from metres × 9.80665).
Ingest — example payloads
Permafrost (thermistor string):
{
"sensor_id": "JM-PERM-01",
"payload": {
"type": "permafrost_thermistor",
"depths_m": { "1": -1.2, "3": -0.6, "5": 0.2, "10": -3.1 }
}
}Piezometer (pore pressure):
{
"sensor_id": "JM-PIEZO-01",
"payload": {
"type": "piezometer",
"pore_pressure_kpa": 95.5
}
}When pore_pressure_kpa ≥ pore_pressure_critical_kpa (default 120), geo_status is set to CRITICAL on the row. When temperature exceeds 0°C for more than 72 hours (all permafrost rows in the window), thermal_erosion: true and CRITICAL are set.
Optional sensor_id. Returns overall (STABLE / WARNING / CRITICAL) and per sensor: min/max temperature and pore pressure over the last 24h, latest geo_status.
Required sensor_id. at=latest (default) or at=history (last 50). Returns thaw_depth_m (shallowest depth ≥ 0°C), depths_m, and fully_frozen.
Required sensor_id. Optional depth_m, years_back (default 10). Returns monthly_averages (Jan–May per year), daily_series, and forecast (linear regression, forecast_zero_crossing_at).
Simulator (geo)
In the dashboard Simulator tab (staff only): sensor IDs containing PERM, PIEZ, or INCL receive geo payloads instead of LSE01.
| Action | Description |
|---|---|
?action=generate |
All active sensors (LSE01 + geo) |
?action=generate_geo |
Geo sensors only |
?action=seed_geo_history |
Weekly permafrost history (~3 years) for trend charts |
The dashboard Geotechnical tab displays status, thaw depth, and Chart.js graphs from the APIs above.
Runway monitoring (Jan Mayensfield et al.). ?group=jan_mayensfield (default).
Returns sections[] with location, vwc_pct, penetration_cm, status_label (Optimal/Caution/Critical), status_color, received_at.
Thresholds are configured in lib/runway.php (runway_config()) and can be overridden per client/sensor via lib/operations.php → runway or later via the database.
Defaults for Jan Mayensfield:
- Optimal: est. penetration < 5 cm
- Caution: 5–15 cm
- Critical: > 15 cm
Seed demo data: php scripts/seed_runway_enja.php
Web UI
| Path | Description |
|---|---|
/ |
Welcome page (guests); dashboard (signed-in users) |
/login.php |
Sign in |
/ |
Welcome page (public, SEO) |
/dashboard.php |
Dashboard (requires session) |
/docs/api.php |
API reference (HTML) |
Security
- Requests to
/ingest.phpmust include a validX-API-Key; comparison uses timing-safehash_equals. sync_receive.phprequiresX-Sync-KeymatchingSYNC_API_KEY. The endpoint is a no-op (503) whenSYNC_API_KEYis not set.sync_entities_receive.phpfollows the same pattern.- Dashboard API is session-protected; do not strip the
Cookieheader in a reverse proxy for/api/. - For
500responses, user-facing messages are intentionally generic; details are logged on the server.