API overview
Base URL, auth, errors, idempotency, rate limits
The OpenSense HTTP API is a small REST surface plus a single ingest endpoint and a few non-REST helpers. Everything is JSON over HTTPS. There is one production base URL and no staging environment.
Base URL
All endpoints are versioned under /v1. The version is major version
only. Backwards-compatible changes go inside v1; breaking changes ship as
/v2 with a 12-month overlap.
Authentication
Three token types, all carried in the Authorization: Bearer <token> header
unless otherwise noted.
| Token | Prefix | Scope | Rotation |
|---|---|---|---|
| Device token | ds_live_ | one device's ingest endpoints | per device, any time |
| User API token | ua_live_ | management endpoints (CRUD) | per user, any time |
| Webhook secret | whk_ | inbound from Shelly / TTN URLs | per integration |
Device tokens are issued by the dashboard when you add a device. They can be
embedded in a query string (?token=…) for the legacy Shelly path; everywhere
else, use the Authorization header.
Magic-link auth is the web UI's auth, not the API's. The web UI exchanges the magic-link code for a session cookie; the cookie is not intended for use against the API.
Errors
All errors are JSON with a stable shape:
{
"error": {
"code": "channel_not_found",
"message": "No channel with id chn_4f3c... in this account",
"request_id": "req_b1d2e3f4"
}
}
code is the stable, machine-readable identifier. message is human and
may change wording. request_id is what you put in a support email — we
keep server-side logs keyed by it for 30 days.
| HTTP | Meaning | Common codes |
|---|---|---|
| 400 | Bad request — payload invalid | bad_json, invalid_measurement |
| 401 | Missing or wrong token | auth_required, token_invalid |
| 403 | Token correct, scope wrong | forbidden |
| 404 | Resource does not exist for this account | device_not_found, channel_not_found |
| 409 | Idempotency conflict — different body, same key | idempotency_conflict |
| 422 | Payload parsed but semantically invalid | out_of_range, unknown_kind |
| 429 | Rate limited | rate_limited |
| 5xx | Server error — safe to retry with backoff | internal, db_unavailable |
Idempotency
Mutating endpoints (everything that is POST/PUT/PATCH/DELETE and not
strictly idempotent on its own) accept an Idempotency-Key request header.
Pick any opaque string up to 128 chars; we recommend a UUID.
OpenSense remembers the (account, idempotency_key) tuple for 24 hours.
A repeated request with the same key and the same body returns the same
response (cached). A repeated request with the same key and a different
body returns 409 idempotency_conflict.
The ingest endpoint (POST /v1/ingest) is content-idempotent: the
deduplication key is (device_id, ts, channel_id). You do not need to
send an Idempotency-Key for ingest; sending the same measurement twice is
a no-op.
Rate limits
Per-account, per-route family. Limits are advisory — we will warn before enforcing, and the limit grows automatically with your subscription.
| Route family | Default limit | Header |
|---|---|---|
POST /v1/ingest | 60 req / min / device | X-RateLimit-Device |
GET /v1/measurements | 120 req / min / account | X-RateLimit-Read |
POST /v1/reports | 4 req / min / account | X-RateLimit-Report |
* /v1/* (everything else) | 600 req / min / account | X-RateLimit-Mgmt |
Responses include X-RateLimit-Remaining and X-RateLimit-Reset (epoch
seconds). 429 responses include Retry-After (seconds).
Pagination
List endpoints use cursor pagination. Two query params:
limit: 1–500, default 100.cursor: opaque, returned in the response body asnext_cursor.
Response shape:
{
"data": [ … ],
"next_cursor": "eyJ0cyI6...",
"has_more": true
}
When has_more is false, next_cursor is null.
Timestamps
- All timestamps are RFC 3339 with explicit timezone offset, always UTC
(
Z). - Resolution: microseconds for measurement
ts, seconds elsewhere. - The server tolerates RFC 3339 with non-UTC offsets on input and converts.
Content negotiation
We accept and emit application/json. The reports endpoint additionally
emits application/pdf. Accept: application/json is the default; no other
formats are negotiated.
SDKs
There is no first-party SDK yet. The HTTP surface is small enough that hand- written code is fine. See the language tabs on each endpoint page for copy-paste recipes in Python, Go, ESPHome, and Arduino.