Data API
The Grillo Cloud data API gives your own software read-only access to one project's sensors, health reports, earthquake events, and triggers. Use it to feed a building-management system, a SCADA, a report, or your own dashboard.
- Base URL:
https://api.grillo.io - Every response is JSON.
- The API reads data; it cannot change anything in your project.
- The full OpenAPI 3.1 description is at
https://api.grillo.io/v1/openapi.yaml. It needs no key.
The API is included in every plan. For waveform data, use the waveform download or the real-time export.
Get an API key
Contact Grillo Support with your project name and what the key is for, for example "Partner SCADA". Grillo issues the key and sends it to you.
- Each key reads one project.
- A project can have up to 10 active keys. Use a separate key for each system, so you can revoke one without affecting the others.
- Grillo stores only a fingerprint of the key, so a lost key cannot be recovered. Ask for a new one.
- To revoke a key, contact Grillo Support. Anything using it stops working within a few seconds.
Keep keys out of web pages, apps, and source control. Anyone with the key can read the project's data.
Authentication
Send the key in the Authorization header:
curl -H "Authorization: Bearer grillo_sk_..." https://api.grillo.io/v1/project
Keys start with grillo_sk_. A missing, malformed, or revoked key gets 401 unauthorized.
Conventions
Times are RFC 3339 in UTC, for example 2026-10-12T14:30:00Z. Query parameters also accept a plain date, such as 2026-10-12, meaning midnight UTC.
Lists are paged with limit (1–500, default 100) and offset (default 0). Responses include the total:
{
"data": [ ... ],
"total": 142,
"limit": 100,
"offset": 0
}
Sensors can be named by Device ID (9C139EAE2360) or station code (HT012).
Locations. If your project has location masking on, sensor positions are 500–1000 m from the true site, as in Grillo Cloud. Location names and GPS positions are then withheld, and elevation is rounded to 100 m.
Freshness. Data can be a few seconds behind Grillo Cloud.
Limits
Each key can make 60 requests a minute, with bursts of up to 20. Over the limit, the API answers 429 rate_limited with a Retry-After header giving the seconds to wait.
A query that runs too long is stopped and gets 503 timeout. Narrow the time range or the page size and try again.
Errors
Errors have a code and a message:
{
"error": {
"code": "invalid_range",
"message": "The range can be at most 7 days. Page through longer periods."
}
}
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_parameter, invalid_range | A parameter is wrong. The message says which |
| 401 | unauthorized | The key is missing, malformed, or revoked |
| 404 | not_found | Not found in this project |
| 429 | rate_limited | Too many requests. Wait for Retry-After seconds |
| 503 | timeout | The query took too long |
| 500 | internal_error | Something went wrong at Grillo. Try again, then contact support |
Endpoints
All endpoints use GET.
| Endpoint | Returns |
|---|---|
/v1/project | The project the key belongs to |
/v1/sensors | The project's sensors with their current status |
/v1/sensors/{id} | One sensor |
/v1/sensors/{id}/soh | A sensor's health reports |
/v1/events | Detected earthquakes |
/v1/events/{id} | One earthquake with every solution issued for it |
/v1/triggers | Triggered recordings |
Project
GET /v1/project
{
"id": "…",
"name": "Riverside Tower",
"network_code": "GR",
"sensor_count": 6,
"location_masked": false,
"api_key_id": "…"
}
Sensors
GET /v1/sensors lists sensors ordered by station code, paged with limit and offset. GET /v1/sensors/{id} returns one.
{
"device_id": "9C139EAE2360",
"station": "TWR01",
"network": "GR",
"sensor_type": "grillo-pulse",
"firmware": "1.0.14",
"status": "online",
"last_seen": "2026-10-12T14:30:05Z",
"location": {
"latitude": 51.5072,
"longitude": -0.1276,
"elevation_m": 42,
"name": "Level 12, east core",
"masked": false
},
"power": { "battery_volts": null, "charging": null, "source": "DC" },
"connectivity": { "type": "cell", "signal_strength": -71 }
}
statusisonlineif the sensor reported in the last 150 seconds, as in Grillo Cloud.connectivity.typeiswifi,cell, ornone.signal_strengthis in dBm when negative, and a percentage otherwise.
Sensor health reports
GET /v1/sensors/{id}/soh
| Parameter | Meaning |
|---|---|
from | Start of the range, exclusive. Defaults to 24 hours before to |
to | End of the range, inclusive. Defaults to now |
limit | Reports per response, 1–5000. Default 1000 |
The range can be at most 7 days. Reports are oldest first. When more reports remain, next_from is set: request again with from set to next_from and the same to. When next_from is null, you have them all.
{
"device_id": "9C139EAE2360",
"station": "TWR01",
"from": "2026-10-11T14:30:00Z",
"to": "2026-10-12T14:30:00Z",
"next_from": null,
"data": [
{
"timestamp": "2026-10-11T14:30:28Z",
"received_at": "2026-10-11T14:30:28Z",
"firmware": "1.0.14",
"power_source": "DC",
"connection_type": "cell",
"signal_strength": -71,
"uptime_s": 86400,
"reboot_reason": "normal",
"clock_quality": "synced",
"sync_age_s": 120,
"data_tx_bytes": 1048576,
"data_rx_bytes": 65536
}
]
}
Each report can also include battery_volts, charging, temperature_c, free_heap_bytes, buffer_usage_pct, and GPS position. A field the sensor does not report is null.
timestampis the sensor's time for the report;received_atis when Grillo received it.data_tx_bytesanddata_rx_bytescount the sensor's data sent and received since it last started. They do not include network overhead, so they are lower than the usage your mobile operator bills.
Events
GET /v1/events lists earthquakes detected for the project, newest first. Events need a plan with earthquake detection.
| Parameter | Meaning |
|---|---|
from, to | Issued at or after from, and at or before to |
min_magnitude | Smallest magnitude to include |
bbox | Epicentre within min_lon,min_lat,max_lon,max_lat, for example -74.5,17.9,-71.6,20.1 |
order | desc (default) or asc |
limit, offset | Paging |
{
"id": "…",
"issued_at": "2026-10-07T03:12:44Z",
"magnitude": 3.6,
"origin": {
"id": "…",
"latitude": 18.52,
"longitude": -72.31,
"depth": 10.2,
"origin_time": 1791342750.4,
"issued_at": 1791342764.1
},
"station_count": 5,
"pick_stations": ["HT012", "HT014", "HT020", "HT031", "HT033"]
}
origin is the latest location. Its depth is in kilometres, and its times are Unix seconds.
GET /v1/events/{id} returns one event with every solution issued for it: origins, magnitudes, picks, amplitudes, and descriptions.
Triggers
GET /v1/triggers lists triggers, newest first.
| Parameter | Meaning |
|---|---|
from, to | Started at or after from, and at or before to |
device_id | Only this sensor's triggers |
order | desc (default) or asc |
limit, offset | Paging |
{
"id": 4182,
"device_id": "9C139EAE2360",
"station": "TWR01",
"location_name": "Level 12, east core",
"start_time": "2026-10-12T13:55:46Z",
"end_time": "2026-10-12T13:57:49Z",
"peak_time": "2026-10-12T13:56:46Z",
"pga_horizontal_mg": 273.8,
"pga_hne_mg": 201.5,
"pga_hnn_mg": 186.2,
"pga_hnz_mg": 95.4,
"pgv_ehz_mm_s": 12.7,
"peak_ehz_counts": 1843021,
"peak_hz": 7.8,
"status": "closed"
}
- Accelerations are in mg and velocity in mm/s, measured after filtering to 0.5–25 Hz.
statusisopenwhile the sensor is still recording, thenclosed. Values of an open trigger can still change.stationisnullif the sensor has since moved to another project.
Example: poll for new triggers
This script checks every minute for triggers that have finished, and prints each one once.
import time
from datetime import datetime, timedelta, timezone
import requests
API = "https://api.grillo.io/v1"
HEADERS = {"Authorization": "Bearer grillo_sk_..."}
seen = set()
while True:
since = datetime.now(timezone.utc) - timedelta(hours=1)
r = requests.get(f"{API}/triggers", headers=HEADERS,
params={"from": since.isoformat(), "order": "asc", "limit": 500})
if r.status_code == 429:
time.sleep(int(r.headers.get("Retry-After", "5")))
continue
r.raise_for_status()
for t in r.json()["data"]:
if t["status"] == "closed" and t["id"] not in seen:
seen.add(t["id"])
print(t["station"], t["peak_time"], t["pga_horizontal_mg"], "mg")
time.sleep(60)
It looks back an hour on every request, so a trigger that was still open on one request is picked up once it closes.