REST API for approved partners to work with BluNotify devices in their scope: notification lights (BluNotify) and energy monitors (BluEM, under /v1/bluem). All endpoints require a bearer API key and HTTPS. Every response carries an X-Request-Id header, and every error body the same value as request_id: quote it to BluNotify support. Every response to an authenticated request carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset for the per-partner request limit. Quick-start guide at /docs; full reference at /docs_adv. Versioning: after launch /v1 only gains things (new routes, optional parameters, fields and error codes), so a client must ignore fields it does not recognise. Anything to be removed is deprecated first, with at least six months before its sunset; a response that uses it carries Deprecation, Sunset and Link (rel="deprecation") headers, and the operation is marked deprecated here. Changes are listed at /changelog. Every /v1 answer carries Cache-Control: no-store. Bodies are JSON objects sent as application/json (415 otherwise). A create answers 201 with Location, an accepted command 202, and an update or removal with nothing to return 204.
This page is generated from the API's OpenAPI description, so it is exactly what the API documents. The guide explains the concepts behind it: scope, permissions, rate limits, and the integrator notes for BluEM meters.
Account
Your key, its permissions and its rate limits.
Partner configuration
Example request
curl -X GET "https://api.blunotify.com/v1/account" \ -H "Authorization: Bearer $KEY"
Responses
200 Partner name, permissions, allowed channels, and rate limits.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Field | Type | Description |
|---|---|---|
| partner always | object | |
| partner.name always | string | |
| partner.key_prefix always | string | |
| partner.permissions always | object | Every permission, granted or not. A 403 names a missing one by these keys (required_permission). |
| partner.permissions.raw_commands always | boolean | |
| partner.permissions.predefined_commands always | boolean | |
| partner.permissions.state_read always | boolean | |
| partner.permissions.health_read always | boolean | |
| partner.permissions.automation_control always | boolean | |
| partner.permissions.relaxed_caps always | boolean | |
| partner.permissions.device_provisioning always | boolean | |
| partner.permissions.device_removal always | boolean | |
| partner.permissions.system_read always | boolean | |
| partner.permissions.system_control always | boolean | |
| partner.permissions.system_destructive always | boolean | |
| partner.permissions.file_upload always | boolean | |
| partner.permissions.file_delete always | boolean | |
| partner.permissions.raw_mqtt_devices always | boolean | |
| partner.permissions.raw_mqtt_global always | boolean | |
| partner.permissions.access_all_devices always | boolean | |
| partner.permissions.config_read always | boolean | |
| partner.permissions.config_write always | boolean | |
| partner.permissions.profile_manage always | boolean | |
| partner.permissions.energy_reset always | boolean | |
| partner.permissions.energy_read always | boolean | |
| partner.permissions.signal_manage always | boolean | |
| partner.allowed_channels always | array of string | |
| partner.rate_limits always | object | |
| partner.rate_limits.requests_per_min | integer | |
| partner.rate_limits.device_commands_per_min | integer |
{
"partner": {
"name": "string",
"key_prefix": "string",
"permissions": {
"raw_commands": true,
"predefined_commands": true,
"state_read": true,
"health_read": true,
"automation_control": true,
"relaxed_caps": true,
"device_provisioning": true,
"device_removal": true,
"system_read": true,
"system_control": true,
"system_destructive": true,
"file_upload": true,
"file_delete": true,
"raw_mqtt_devices": true,
"raw_mqtt_global": true,
"access_all_devices": true,
"config_read": true,
"config_write": true,
"profile_manage": true,
"energy_reset": true,
"energy_read": true,
"signal_manage": true
},
"allowed_channels": [
"string"
],
"rate_limits": {
"requests_per_min": 0,
"device_commands_per_min": 0
}
}
}
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the…
A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
Devices
The devices in your scope, whatever their product: listing, provisioning, removal, state and health.
List devices in scope
The devices in your scope, in device-id order. ?product= narrows the list to one product's devices ('blunotify' or 'bluem'); an unknown value is a 400, never an empty list.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| product | query | string: blunotify, bluem | Only this product's devices. |
Example request
curl -X GET "https://api.blunotify.com/v1/devices" \ -H "Authorization: Bearer $KEY"
Responses
200 Devices in the partner's scope.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Field | Type | Description |
|---|---|---|
| devices always | array of object | |
| devices[].device_id always | string | |
| devices[].name always | string or null | |
| devices[].product always | string or null | The product the device was provisioned as: 'blunotify' (notification light) or 'bluem' (energy monitor). Null only for a device of a product this server does not know. |
| devices[].channels | array of string | The channels the partner may command. Only on devices of a product that has named channels (BluNotify); absent on a BluEM meter. |
| devices[].automation_enabled | boolean | BluNotify devices only: whether temperature automation drives the light (PUT .../automation). |
| devices[].status | string or null | With the health-read permission only. |
| devices[].status_changed_at | string (date-time) or null | With the health-read permission only. |
| devices[].last_seen_at | string (date-time) or null | With the health-read permission only. |
{
"devices": [
{
"device_id": "string",
"name": "string",
"product": "string",
"channels": [
"string"
],
"automation_enabled": true,
"status": "string",
"status_changed_at": "2026-09-30T14:00:00+00:00",
"last_seen_at": "2026-09-30T14:00:00+00:00"
}
]
}
400 Unknown product.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the…
A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
Get one device in scope
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| device_id required | path | string |
Example request
curl -X GET "https://api.blunotify.com/v1/devices/DEVICE_ID" \ -H "Authorization: Bearer $KEY"
Responses
200 Device detail.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Field | Type | Description |
|---|---|---|
| device_id always | string | |
| name always | string or null | |
| product always | string or null | The product the device was provisioned as: 'blunotify' (notification light) or 'bluem' (energy monitor). Null only for a device of a product this server does not know. |
| channels | array of string | The channels the partner may command. Only on devices of a product that has named channels (BluNotify); absent on a BluEM meter. |
| automation_enabled | boolean | BluNotify devices only: whether temperature automation drives the light (PUT .../automation). |
| status | string or null | With the health-read permission only. |
| status_changed_at | string (date-time) or null | With the health-read permission only. |
| last_seen_at | string (date-time) or null | With the health-read permission only. |
{
"device_id": "string",
"name": "string",
"product": "string",
"channels": [
"string"
],
"automation_enabled": true,
"status": "string",
"status_changed_at": "2026-09-30T14:00:00+00:00",
"last_seen_at": "2026-09-30T14:00:00+00:00"
}
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
404 Outside scope or not found.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the…
A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
Provision a new device (derived-v1)
Registers a new device under the firmware's derived-v1 credential contract and places it in the partner's scope. Requires the device-provisioning permission. The device ID (path parameter) is the ESP32 base MAC as exactly 12 uppercase hexadecimal characters (no separators); it is the MQTT username, the HKDF salt, and the firmware device ID, and must not already be in use. credential_source selects what you provide (default generate): generate — the server mints a 16-character root secret, derives the 32-character MQTT broker password from it (HKDF-SHA256), and returns the root secret exactly once as password (the eFuse root secret, NOT the MQTT password); root_secret — you supply the device's existing eFuse secret and the server derives from it, which is how you RE-ENROLL a device whose eFuse is already burned (minting a new secret would derive a password the device can never produce); mqtt_password — you supply the already-derived 32-character lowercase-hex broker password verbatim and the server performs no derivation. A supplied credential is never echoed back, and the root secret is never stored server-side in any mode — it lives only in the device eFuse and the provisioning helper's own record. org placement: a partner with access_all_devices may optionally name an existing ROOT organization (omitted, the device lands in the root Staging / Inventory node); an org-scoped partner (no access_all_devices) MUST supply org naming a node INSIDE its own granted subtrees — omitted is 400 validation_error, a name that is unknown or outside scope is 400 "unknown organization '<org>'" (the two are indistinguishable, so the response never confirms an organization outside the partner's sight), and a name shared by two or more in-scope nodes is 400 "organization '<org>' is ambiguous: N organizations in your scope share that name".
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| device_id required | path | string | |
| Idempotency-Key | header | string | A value of your choosing (a UUID, for example) that makes a retry safe. A repeat of the same request with the same key within 24 hours gets the first response back, status and body, without the work being done again; it carries Idempotent-Replayed: true. Scoped to your account and this route. The same key with a different request is a 422 idempotency_key_reused; a repeat while the first is still running is a 409 idempotency_in_progress. Only a successful (2xx) response is kept: after a failure the same key runs the request again. Not taken by the multipart file upload, which never overwrites anyway. |
Request body (JSON)
| Field | Type | Description |
|---|---|---|
| credential_format required | string: derived-v1 | |
| credential_source | string: generate, root_secret, mqtt_password | What you are providing. Only the field matching this value may be sent; supplying the others is rejected. |
| root_secret | string | Required when credential_source is 'root_secret': the device's existing eFuse secret, 16 printable-ASCII characters with no whitespace. |
| mqtt_password | string | Required when credential_source is 'mqtt_password': the already-derived broker password, 32 lowercase hexadecimal characters. |
| product | string | Which device product this is. Currently 'blunotify' or 'bluem'. It selects the HKDF label the MQTT password is derived with ('<product>/mqtt/v1'), the MQTT topic root the device uses ('<product>/<DEV_ID>/...'), and the broker ACL subtree its credential is granted. Omit it and you get 'blunotify', so older tooling keeps working; an unrecognised value is rejected rather than defaulted, because a device burned against one label can never produce the password derived from another. Echoed back in the response — check it before burning an eFuse. |
| name | string | Optional display name. |
| org | string | Where to place the device; provisioning never creates org nodes. Optional for an access_all_devices partner (must then name an existing ROOT organization; omitted lands the device in Staging / Inventory). REQUIRED for an org-scoped partner (no access_all_devices), and must name a node inside that partner's own granted subtrees — see the operation description for the exact 400s this produces. |
Example request
curl -X POST "https://api.blunotify.com/v1/devices/DEVICE_ID" \
-H "Authorization: Bearer $KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"credential_format": "derived-v1", "credential_source": "generate", "root_secret": "string", "mqtt_password": "string", "product": "bluem", "name": "string", "org": "string"}'
Responses
201 Provisioned. device_id, credential_format, product, password (the root secret, only when credential_source was generate), and name and org…
Provisioned. device_id, credential_format, product, password (the root secret, only when credential_source was generate), and name and org when given. Location names the device.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Idempotent-Replayed | true on a response replayed for a repeated Idempotency-Key. |
| Location | The resource created, or the one to poll for an outcome. |
| Field | Type | Description |
|---|---|---|
| device_id always | string | |
| credential_format always | string: derived-v1 | |
| product always | string | |
| password | string | The 16-character root secret; only when the server generated it. |
| name | string | |
| org | string |
{
"device_id": "string",
"credential_format": "derived-v1",
"product": "string",
"password": "string",
"name": "string",
"org": "string"
}
400 Invalid device_id, missing/unsupported credential_format, unknown product, invalid credential_source or supplied credential material,…
Invalid device_id, missing/unsupported credential_format, unknown product, invalid credential_source or supplied credential material, invalid name/org, or unknown organization. Nothing is created on any of these. Or a body that is not valid JSON, or not a JSON object.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
403 Device provisioning not permitted.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
409 device_id is already in use (existing devices are never modified, rotated, or disclosed). Or code idempotency_in_progress: a request with…
device_id is already in use (existing devices are never modified, rotated, or disclosed). Or code idempotency_in_progress: a request with the same Idempotency-Key is still running; retry after Retry-After.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
415 Code unsupported_media_type: the body was not sent as application/json.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
422 Code idempotency_key_reused: this Idempotency-Key was used with a different request.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the…
A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
503 Broker provisioning or DB commit failed; nothing was persisted and any partial broker credential was rolled back.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
Remove a device
Cuts the device's broker credential, then deletes its record (cascades automation suspensions and sensor attachments). Requires the device-removal permission. Applies to any device in the partner's scope, not only ones it provisioned. DESTRUCTIVE TIER: confirm must equal the device id (as a query parameter or in a JSON body) or the call is refused with 409, and each removal spends a slot from the same per-partner per-hour destructive-operation ceiling as factory_reset.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| device_id required | path | string | |
| confirm | query | string | Must equal the device id. Required unless the same field is supplied in a JSON request body. |
Request body (JSON)
| Field | Type | Description |
|---|---|---|
| confirm | string | Must equal the device id. |
Example request
curl -X DELETE "https://api.blunotify.com/v1/devices/DEVICE_ID" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"confirm": "string"}'
Responses
204 Removed. No body.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
400 A body that is not valid JSON, or not a JSON object.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
403 Device removal not permitted.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
404 Outside scope or not found.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
409 Missing or mismatched confirm (code confirmation_required); nothing was deleted.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
415 Code unsupported_media_type: the body was not sent as application/json.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 Destructive-operation ceiling reached (Retry-After header). A rate limit was reached: the per-partner request limit (RateLimit-Remaining is…
Destructive-operation ceiling reached (Retry-After header). A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
503 Broker removal failed; the device record was not deleted (retryable).
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
Current device state
Requires the state-read permission. Returns the latest command per permitted channel with a decoded label.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| device_id required | path | string |
Example request
curl -X GET "https://api.blunotify.com/v1/devices/DEVICE_ID/state" \ -H "Authorization: Bearer $KEY"
Responses
200 Per-channel state.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
403 State access not permitted.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
404 Device outside scope.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the…
A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
Device health
Requires the health-read permission.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| device_id required | path | string | |
| hours | query | integer | |
| limit | query | integer |
Example request
curl -X GET "https://api.blunotify.com/v1/devices/DEVICE_ID/health" \ -H "Authorization: Bearer $KEY"
Responses
200 The connectivity fields and recent health telemetry.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Field | Type | Description |
|---|---|---|
| device_id always | string | |
| status | string or null: connected, disconnected, None | |
| status_changed_at | string (date-time) or null | When status last changed. |
| last_seen_at | string (date-time) or null | When the device last sent anything itself. |
| telemetry always | array of object | |
| telemetry[].created_at | string (date-time) | |
| telemetry[].topic | string | |
| telemetry[].payload | string |
{
"device_id": "string",
"status": "connected",
"status_changed_at": "2026-09-30T14:00:00+00:00",
"last_seen_at": "2026-09-30T14:00:00+00:00",
"telemetry": [
{
"created_at": "2026-09-30T14:00:00+00:00",
"topic": "string",
"payload": "string"
}
]
}
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
403 Health access not permitted.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
404 Device outside scope.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the…
A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
BluEM energy monitors
A meter's interval records, summary, consumption and live reading, and its two commands.
Interval records over a time range (BluEM)
Requires the energy-read permission (energy_read). Two ways of asking. BY START TIME (start/end): records come back oldest first, ordered by (start, stream id, sequence number) and cursor-paginated by that key; a cursor carries its window, so the next page is cut from the window the first page resolved, and start, end and unknown_time may be left out beside it (sent, each must equal the first request's value, or the call is a 400); pass unknown_time=true to list records with no determinable start instead. BY ARRIVAL (received_since, then cursor): every record of the meter in the order the server stored it, late records included; next_cursor is always present and is the bookmark to keep; with a cursor, received_since is ignored; start, end and unknown_time cannot be combined with it (400). A bookmark answered 400 invalid 'cursor' belongs to a database history from before a restore: start again from received_since and de-duplicate by sid and seq. Meters are tested before they ship, and the records a meter stored during the test may be cleared: cleared records are no longer listed in either mode, records from before the clear are not stored if they arrive later, and a bookmark stays valid. Each record's current_a, power_w and energy_wh are corrected for the meter's CT turns (divided by its ct_turns); voltage_v, frequency_hz and power_factor are as measured.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| device_id required | path | string | The BluEM device id. |
| start | query | string | ISO-8601 or epoch seconds; defaults to 24h before end. Beside a range-mode cursor: may be left out, and must otherwise equal the first request's. |
| end | query | string | ISO-8601 or epoch seconds; defaults to now. Beside a range-mode cursor: may be left out, and must otherwise equal the first request's. |
| received_since | query | string | ISO-8601 or epoch seconds. Selects the arrival feed: records the server received at or after this instant, in the order it stored them. Ignored when a cursor is sent. |
| limit | query | integer | Default 200, max 1000. |
| cursor | query | string | A previous answer's next_cursor, exactly as received. Cursors are URL-safe ([A-Za-z0-9_-]) and need no encoding. |
| unknown_time | query | boolean | By start time only: list the records that have no start time. |
Example request
curl -X GET "https://api.blunotify.com/v1/bluem/devices/DEVICE_ID/intervals" \ -H "Authorization: Bearer $KEY"
Responses
200 A page of interval records. By start time: device_id, unknown_time, window, records, has_more, next_cursor (null on the last page). By…
A page of interval records. By start time: device_id, unknown_time, window, records, has_more, next_cursor (null on the last page). By arrival: device_id, mode, received_since, records, has_more, next_cursor (never null).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Field | Type | Description |
|---|---|---|
| device_id always | string | |
| unknown_time | boolean | |
| window | object | The half-open window [start, end) the answer covers. |
| window.start always | string (date-time) | |
| window.end always | string (date-time) | |
| records always | array of object | |
| records[].seq | integer | |
| records[].sid | string | Stream id, 8 hex characters. |
| records[].start_at | string (date-time) or null | When the interval began; null when the meter could not know (time_quality unknown). |
| records[].time_quality | string: device, derived, unknown | |
| records[].duration_s | integer or null | |
| records[].reads | object | |
| records[].reads.n | integer or null | |
| records[].reads.err | integer or null | |
| records[].voltage_v | object or null | Voltage, in volts (V), as measured. |
| records[].voltage_v.avg | number | |
| records[].voltage_v.min | number | |
| records[].voltage_v.max | number | |
| records[].current_a | object or null | Current, in amperes (A): the real load, already divided by ct_turns. |
| records[].current_a.avg | number | |
| records[].current_a.min | number | |
| records[].current_a.max | number | |
| records[].power_w | object or null | Real (active) power, in watts (W): the real load, already divided by ct_turns. |
| records[].power_w.avg | number | |
| records[].power_w.min | number | |
| records[].power_w.max | number | |
| records[].frequency_hz | object or null | Mains frequency, in hertz (Hz), as measured. |
| records[].frequency_hz.avg | number | |
| records[].frequency_hz.min | number | |
| records[].frequency_hz.max | number | |
| records[].power_factor | object or null | Power factor, without unit (0 to 1), as measured. |
| records[].power_factor.avg | number | |
| records[].power_factor.min | number | |
| records[].power_factor.max | number | |
| records[].energy_wh | object or null | The meter's lifetime energy counter, in watt-hours (Wh), at the record's first and last good read, divided by ct_turns (so fractional when ct_turns > 1). |
| records[].energy_wh.first | number or null | |
| records[].energy_wh.last | number or null | |
| records[].ct_turns | integer or null | CT turns the record was measured under (firmware emCtTurns; 1 when the record omitted it). a, w and energy_wh are already divided by it. null on a quarantined record whose ct was unusable. |
| records[].alarm_reads | integer or null | |
| records[].flags | object | |
| records[].flags.partial | boolean | |
| records[].flags.energy_reset | boolean | |
| records[].flags.meter_reconfigured | boolean | |
| records[].quarantined | boolean | |
| records[].quarantine_reason | string or null | |
| records[].received_at | string (date-time) | When the server took the record off the network. Much later than start for a record the meter stored through an outage. |
| next_cursor always | string or null | URL-safe. By start time: null on the last page. By arrival: never null; the bookmark to keep. |
| mode | string: received | Arrival feed only. |
| received_since | string (date-time) or null | Arrival feed only: the instant applied, or null on a cursor call. |
| has_more always | boolean | Both modes. By start time: true exactly when next_cursor is set. By arrival: call again at once with next_cursor. |
{
"device_id": "EM123",
"unknown_time": false,
"window": {
"start": "2026-09-24T12:00:00+00:00",
"end": "2026-09-25T12:00:00+00:00"
},
"records": [
{
"seq": 118,
"sid": "1a2b3c4d",
"start_at": "2026-09-25T11:45:00+00:00",
"time_quality": "device",
"duration_s": 900,
"reads": {
"n": 900,
"err": 0
},
"voltage_v": {
"avg": 229.83,
"min": 228.1,
"max": 231.4
},
"current_a": {
"avg": 1.203,
"min": 0.9,
"max": 1.6
},
"power_w": {
"avg": 276.1,
"min": 205.0,
"max": 370.0
},
"frequency_hz": {
"avg": 50.01,
"min": 49.9,
"max": 50.1
},
"power_factor": {
"avg": 0.947,
"min": 0.9,
"max": 0.99
},
"energy_wh": {
"first": 100020.0,
"last": 100089.0
},
"ct_turns": 1,
"alarm_reads": 0,
"flags": {
"partial": false,
"energy_reset": false,
"meter_reconfigured": false
},
"quarantined": false,
"quarantine_reason": null,
"received_at": "2026-09-25T12:00:01.532+00:00"
}
],
"has_more": false,
"next_cursor": null
}
400 Invalid start/end/received_since/cursor, the two ways of asking mixed, or the range exceeds the cap.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
403 Energy data access not permitted (required_permission energy_read).
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
404 Device outside scope, unknown, or not a BluEM device.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the…
A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
Statistics and consumption over a window (BluEM)
Requires the energy-read permission (energy_read). Adds up exactly the records GET .../intervals lists by start time for the same start and end (records with no start time are in none). For voltage_v, current_a, power_w, frequency_hz and power_factor, over the records that carry the quantity (not quarantined, reads.n > 0, an average, a positive duration_s): avg is duration-weighted, min and max are the records' own extremes, rounded to 6 places; null, never zeros, when no record carries it. power_factor counts only records with a load (see its schema). energy_wh is the total GET .../energy gives for the same window, or null with no counter reading. coverage counts the window's records, the quarantined ones, and the seconds the rest cover. A summary of a past window can change when late records arrive.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| device_id required | path | string | The BluEM device id. |
| start | query | string | ISO-8601 or epoch seconds; defaults to 24h before end. |
| end | query | string | ISO-8601 or epoch seconds; defaults to now. |
Example request
curl -X GET "https://api.blunotify.com/v1/bluem/devices/DEVICE_ID/summary" \ -H "Authorization: Bearer $KEY"
Responses
200 The window's statistics.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Field | Type | Description |
|---|---|---|
| device_id always | string | |
| window always | object | The half-open window [start, end) the answer covers. |
| window.start always | string (date-time) | |
| window.end always | string (date-time) | |
| coverage always | object | |
| coverage.records always | integer | Records with a start in the window. |
| coverage.quarantined always | integer | Of those, quarantined. |
| coverage.seconds always | integer | Summed duration_s of the ones not quarantined. |
| voltage_v always | object or null | Voltage, in volts (V), as measured. |
| voltage_v.avg always | number | Duration-weighted: sum(avg x duration_s) / sum(duration_s). |
| voltage_v.min always | number | The lowest record min. |
| voltage_v.max always | number | The highest record max. |
| voltage_v.intervals always | integer | Records that carry the quantity. |
| voltage_v.seconds always | integer | Their summed duration_s. |
| current_a always | object or null | Current, in amperes (A): the real load, already divided by ct_turns. |
| current_a.avg always | number | Duration-weighted: sum(avg x duration_s) / sum(duration_s). |
| current_a.min always | number | The lowest record min. |
| current_a.max always | number | The highest record max. |
| current_a.intervals always | integer | Records that carry the quantity. |
| current_a.seconds always | integer | Their summed duration_s. |
| power_w always | object or null | Real (active) power, in watts (W): the real load, already divided by ct_turns. |
| power_w.avg always | number | Duration-weighted: sum(avg x duration_s) / sum(duration_s). |
| power_w.min always | number | The lowest record min. |
| power_w.max always | number | The highest record max. |
| power_w.intervals always | integer | Records that carry the quantity. |
| power_w.seconds always | integer | Their summed duration_s. |
| frequency_hz always | object or null | Mains frequency, in hertz (Hz), as measured. |
| frequency_hz.avg always | number | Duration-weighted: sum(avg x duration_s) / sum(duration_s). |
| frequency_hz.min always | number | The lowest record min. |
| frequency_hz.max always | number | The highest record max. |
| frequency_hz.intervals always | integer | Records that carry the quantity. |
| frequency_hz.seconds always | integer | Their summed duration_s. |
| power_factor always | object or null | Power factor, without unit (0 to 1), as measured. Over the records with a load only: those whose highest power before CT correction (power_w.max x ct_turns) reached 5 W. With nothing drawing, the meter reports a placeholder, not a ratio. A duration-weighted average of the records' own averages, not an energy-weighted power factor. |
| power_factor.avg always | number | Duration-weighted: sum(avg x duration_s) / sum(duration_s). |
| power_factor.min always | number | The lowest record min. |
| power_factor.max always | number | The highest record max. |
| power_factor.intervals always | integer | Records that carry the quantity. |
| power_factor.seconds always | integer | Their summed duration_s. |
| energy_wh always | object or null | Null when no record carried a counter reading. |
| energy_wh.total always | number | Consumption over the window, in watt-hours (Wh): the total GET .../energy gives for the same window. |
| energy_wh.resets always | integer | Energy-counter resets seen in the window. |
| energy_wh.intervals always | integer | Records the consumption is summed over. |
{
"device_id": "7C9EBD91235C",
"window": {
"start": "2026-09-29T14:00:00+00:00",
"end": "2026-09-30T14:00:00+00:00"
},
"coverage": {
"records": 96,
"quarantined": 0,
"seconds": 86391
},
"voltage_v": {
"avg": 123.077838,
"min": 119.6,
"max": 125.3,
"intervals": 96,
"seconds": 86391
},
"current_a": {
"avg": 0.336163,
"min": 0.0,
"max": 6.9894,
"intervals": 96,
"seconds": 86391
},
"power_w": {
"avg": 31.99751,
"min": 0.0,
"max": 764.92,
"intervals": 96,
"seconds": 86391
},
"frequency_hz": {
"avg": 59.967292,
"min": 59.9,
"max": 60.0,
"intervals": 96,
"seconds": 86391
},
"power_factor": {
"avg": 0.693137,
"min": 0.0,
"max": 0.96,
"intervals": 55,
"seconds": 49493
},
"energy_wh": {
"total": 769.0,
"resets": 0,
"intervals": 96
}
}
400 Invalid start/end, or the range exceeds a year.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
403 Energy data access not permitted (required_permission energy_read).
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
404 Device outside scope, unknown, or not a BluEM device.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the…
A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
Consumption per bucket and total (BluEM)
Requires the energy-read permission (energy_read). bucket is 'hour' or 'day' (default 'hour'). Buckets are always exactly the requested width, never silently coarser: a window the bucket cannot cover in under 2000 buckets is a 400 validation_error, so an 'hour'-bucketed range must span fewer than 2000 hours (83 days) — use bucket=day beyond that. total is null, not zero, when no record in the window carried a counter reading. Consumption is in watt-hours (Wh), corrected for CT turns like a record's energy_wh; a change of emCtTurns is never reported as a reset (the interval after it counts its own first-to-last reading). With tz the buckets are local days or hours of that zone; an unknown zone is a 400.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| device_id required | path | string | The BluEM device id. |
| bucket | query | string: hour, day | |
| start | query | string | |
| end | query | string | |
| tz | query | string | An IANA zone name, e.g. America/New_York. Buckets are then that zone's local days or hours (a day at a daylight-saving change is 23 or 25 hours), each point's t is written in that zone with its offset, and the answer echoes tz. Absent: UTC buckets, exactly as before. Nothing is stored. |
Example request
curl -X GET "https://api.blunotify.com/v1/bluem/devices/DEVICE_ID/energy" \ -H "Authorization: Bearer $KEY"
Responses
200 Bucketed consumption points plus the window total; tz is echoed when it was sent.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Field | Type | Description |
|---|---|---|
| device_id always | string | |
| bucket always | string: hour, day | |
| window always | object | The half-open window [start, end) the answer covers. |
| window.start always | string (date-time) | |
| window.end always | string (date-time) | |
| points always | array of object | |
| points[].start_at always | string (date-time) | The bucket's first instant (with tz, in that zone). |
| points[].energy_wh always | number | Consumption in the bucket, in watt-hours (Wh). |
| points[].reset always | boolean | The counter was reset in this bucket. |
| total always | object or null | |
| total.energy_wh always | number | Consumption over the window, in watt-hours (Wh). |
| total.resets always | integer | Energy-counter resets seen in the window. |
| total.intervals always | integer | Records the consumption is summed over. |
| total.buckets always | integer | Buckets with a reading. |
| tz | string | Echoed when sent. |
{
"device_id": "EM123",
"bucket": "day",
"window": {
"start": "2026-09-28T00:00:00+00:00",
"end": "2026-09-30T00:00:00+00:00"
},
"points": [
{
"start_at": "2026-09-28T00:00:00+00:00",
"energy_wh": 812.4,
"reset": false
},
{
"start_at": "2026-09-29T00:00:00+00:00",
"energy_wh": 744.1,
"reset": false
}
],
"total": {
"energy_wh": 1556.5,
"resets": 0,
"intervals": 192,
"buckets": 2
}
}
400 Invalid bucket or start/end, the range exceeds the cap, or the bucket cannot cover the window in under 2000 buckets (an 'hour'-bucketed…
Invalid bucket or start/end, the range exceeds the cap, or the bucket cannot cover the window in under 2000 buckets (an 'hour'-bucketed range of 2000 hours / ~83 days and up), or tz is not a known zone.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
403 Energy data access not permitted (required_permission energy_read).
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
404 Device outside scope, unknown, or not a BluEM device.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the…
A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
Close the interval in progress now (cmd/report)
Requires system control. Fire-and-forget (202); the record carries a partial flag and reaches GET .../intervals a few seconds later. Not a refresh: every call closes the interval in progress and stores a short record. For what the meter reads now, use POST .../sample, which stores nothing.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| device_id required | path | string | The BluEM device id. |
| Idempotency-Key | header | string | A value of your choosing (a UUID, for example) that makes a retry safe. A repeat of the same request with the same key within 24 hours gets the first response back, status and body, without the work being done again; it carries Idempotent-Replayed: true. Scoped to your account and this route. The same key with a different request is a 422 idempotency_key_reused; a repeat while the first is still running is a 409 idempotency_in_progress. Only a successful (2xx) response is kept: after a failure the same key runs the request again. Not taken by the multipart file upload, which never overwrites anyway. |
Example request
curl -X POST "https://api.blunotify.com/v1/bluem/devices/DEVICE_ID/report" \ -H "Authorization: Bearer $KEY" \ -H "Idempotency-Key: $(uuidgen)"
Responses
202 Accepted
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Idempotent-Replayed | true on a response replayed for a repeated Idempotency-Key. |
| Field | Type | Description |
|---|---|---|
| topic | string | |
| payload | string | |
| sent_at | string (date-time) | |
| command | string | |
| key | string |
{
"topic": "string",
"payload": "string",
"sent_at": "2026-09-30T14:00:00+00:00",
"command": "string",
"key": "string"
}
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
403 System control not permitted.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
404 Device outside scope, unknown, or not a BluEM device.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
409 Code idempotency_in_progress: a request with the same Idempotency-Key is still running; retry after Retry-After.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
422 Code idempotency_key_reused: this Idempotency-Key was used with a different request.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 Rate limited. A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation…
Rate limited. A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
503 Publish failed.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
Ask for a live reading (BluEM)
Requires the energy-read permission (energy_read). Asks the meter what it reads now. Closes no interval and stores no record. The server does not wait for the meter: the answer is a token to poll with GET .../sample/{token}. At most one request per meter every 10 seconds, whoever asks.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| device_id required | path | string | The BluEM device id. |
| Idempotency-Key | header | string | A value of your choosing (a UUID, for example) that makes a retry safe. A repeat of the same request with the same key within 24 hours gets the first response back, status and body, without the work being done again; it carries Idempotent-Replayed: true. Scoped to your account and this route. The same key with a different request is a 422 idempotency_key_reused; a repeat while the first is still running is a 409 idempotency_in_progress. Only a successful (2xx) response is kept: after a failure the same key runs the request again. Not taken by the multipart file upload, which never overwrites anyway. |
Example request
curl -X POST "https://api.blunotify.com/v1/bluem/devices/DEVICE_ID/sample" \ -H "Authorization: Bearer $KEY" \ -H "Idempotency-Key: $(uuidgen)"
Responses
202 Sent to the meter. Location names the resource to poll.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Location | The resource created, or the one to poll for an outcome. |
| Idempotent-Replayed | true on a response replayed for a repeated Idempotency-Key. |
| Field | Type | Description |
|---|---|---|
| device_id | string | |
| token | string | |
| state | string: pending | |
| requested_at | string (date-time) | |
| timeout_s | integer | After this the outcome is final. |
{
"device_id": "string",
"token": "5f3a9c1e",
"state": "pending",
"requested_at": "2026-09-30T14:00:00+00:00",
"timeout_s": 30
}
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
403 Energy data access not permitted (required_permission energy_read).
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
404 Device outside scope, unknown, or not a BluEM device.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
409 code device_offline: the meter is not connected and nothing was sent. Carries status, status_changed_at and last_seen_at. Or code…
code device_offline: the meter is not connected and nothing was sent. Carries status, status_changed_at and last_seen_at. Or code idempotency_in_progress: a request with the same Idempotency-Key is still running; retry after Retry-After.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
| status | string or null | |
| status_changed_at | string (date-time) or null | |
| last_seen_at | string (date-time) or null |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string",
"status": "string",
"status_changed_at": "2026-09-30T14:00:00+00:00",
"last_seen_at": "2026-09-30T14:00:00+00:00"
}
422 Code idempotency_key_reused: this Idempotency-Key was used with a different request.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 This meter was asked less than 10 seconds ago. Retry-After says how long to wait. A rate limit was reached: the per-partner request limit…
This meter was asked less than 10 seconds ago. Retry-After says how long to wait. A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
503 code publish_failed: the broker could not be reached.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
Outcome of a live reading (BluEM)
Requires the energy-read permission (energy_read). Poll every second or two until state is no longer pending; it is final within 30 seconds. The reading is a fixed set of electrical quantities and nothing of the device's own status.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| device_id required | path | string | The BluEM device id. |
| token required | path | string | The token a POST .../sample returned. |
Example request
curl -X GET "https://api.blunotify.com/v1/bluem/devices/DEVICE_ID/sample/TOKEN" \ -H "Authorization: Bearer $KEY"
Responses
200 The state of the request.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Field | Type | Description |
|---|---|---|
| device_id | string | |
| token | string | |
| state | string: pending, done, failed, no_reply | |
| reason | string or null | Set when state is failed: busy, config_mode, unsupported, rejected or no_snapshot. |
| requested_at | string (date-time) | |
| reading | object | Set when state is done. |
| reading.link | string: ok, timeout, crc, exception, badreply, notstarted, unknown | The unit's link to its meter at its last read. |
| reading.answered | boolean | True only when link is ok. |
| reading.voltage_v | number or null | Voltage, in volts (V), as measured. |
| reading.current_a | number or null | Current, in amperes (A): the real load, already divided by ct_turns. |
| reading.power_w | number or null | Real (active) power, in watts (W): the real load, already divided by ct_turns. |
| reading.power_factor | number or null | Power factor, without unit (0 to 1), as measured. Null when nothing is drawing power. |
| reading.frequency_hz | number or null | Mains frequency, in hertz (Hz), as measured. |
| reading.ct_turns | integer | The CT turns the unit is set to. |
| reading.age_s | number or null | Seconds between the meter reading these values and its answer. |
| reading.taken_at | string (date-time) | When the answer reached the server. |
| reading.took_s | number | Seconds from the request to the answer, to the millisecond (for example 1.278). |
{
"device_id": "EM123",
"token": "0a1b2c3d",
"state": "done",
"reason": null,
"requested_at": "2026-09-30T14:02:09.926+00:00",
"reading": {
"link": "ok",
"answered": true,
"voltage_v": 122.0,
"current_a": 0.6268,
"power_w": 58.54,
"power_factor": 0.77,
"frequency_hz": 60.0,
"ct_turns": 5,
"age_s": 0,
"taken_at": "2026-09-30T14:02:11.204+00:00",
"took_s": 1.278
}
}
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
403 Energy data access not permitted (required_permission energy_read).
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
404 Device outside scope, unknown, or not a BluEM device; or a token that was not issued for this device.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the…
A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
Zero a meter's lifetime energy counter (cmd/resetenergy)
DESTRUCTIVE AND IRREVERSIBLE. Requires the energy-reset permission (off by default, independent of every other flag) plus confirm equal to the device id exactly. Fire-and-forget (202) with the minted token; poll GET .../resetenergy/{token} for the outcome.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| device_id required | path | string | The BluEM device id. |
| Idempotency-Key | header | string | A value of your choosing (a UUID, for example) that makes a retry safe. A repeat of the same request with the same key within 24 hours gets the first response back, status and body, without the work being done again; it carries Idempotent-Replayed: true. Scoped to your account and this route. The same key with a different request is a 422 idempotency_key_reused; a repeat while the first is still running is a 409 idempotency_in_progress. Only a successful (2xx) response is kept: after a failure the same key runs the request again. Not taken by the multipart file upload, which never overwrites anyway. |
Request body (JSON)
| Field | Type | Description |
|---|---|---|
| confirm required | string | Must equal the device id exactly. |
Example request
curl -X POST "https://api.blunotify.com/v1/bluem/devices/DEVICE_ID/resetenergy" \
-H "Authorization: Bearer $KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"confirm": "string"}'
Responses
202 Accepted. Location names the resource to poll.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Location | The resource created, or the one to poll for an outcome. |
| Idempotent-Replayed | true on a response replayed for a repeated Idempotency-Key. |
| Field | Type | Description |
|---|---|---|
| topic | string | |
| payload | string | |
| sent_at | string (date-time) | |
| command | string | |
| key | string |
{
"topic": "string",
"payload": "string",
"sent_at": "2026-09-30T14:00:00+00:00",
"command": "string",
"key": "string"
}
400 A body that is not valid JSON, or not a JSON object.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
403 Energy reset permission not granted.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
404 Device outside scope, unknown, or not a BluEM device.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
409 Missing or mismatched 'confirm'. Or code idempotency_in_progress: a request with the same Idempotency-Key is still running; retry after…
Missing or mismatched 'confirm'. Or code idempotency_in_progress: a request with the same Idempotency-Key is still running; retry after Retry-After.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
415 Code unsupported_media_type: the body was not sent as application/json.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
422 Code idempotency_key_reused: this Idempotency-Key was used with a different request.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 Rate limited (destructive ceiling included). A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a…
Rate limited (destructive ceiling included). A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
503 Publish failed.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
The outcome of one energy-reset request
Requires the energy-reset permission (same flag as the request). state is pending, done, duplicate or failed. A token not issued for this device is 404, like an unknown or out-of-scope device.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| device_id required | path | string | The BluEM device id. |
| token required | path | string | The token returned by the POST above. |
Example request
curl -X GET "https://api.blunotify.com/v1/bluem/devices/DEVICE_ID/resetenergy/TOKEN" \ -H "Authorization: Bearer $KEY"
Responses
200 The current outcome. pending carries requested_at and unmatched_rejection; the final states carry result, reason and at.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Field | Type | Description |
|---|---|---|
| state always | string: pending, done, duplicate, failed | |
| result | string or null | The meter's answer, when final. |
| reason | string or null | Why it failed, when it did. |
| at | string (date-time) | When the meter answered, when final. |
| requested_at | string (date-time) | Pending only. |
| unmatched_rejection | boolean | Pending only: the meter has logged a rejection that names no token since the request, which may or may not be this one's answer. |
{
"state": "pending",
"result": "string",
"reason": "string",
"at": "2026-09-30T14:00:00+00:00",
"requested_at": "2026-09-30T14:00:00+00:00",
"unmatched_rejection": true
}
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
403 Energy reset permission not granted.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
404 Device or token not found.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the…
A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
BluNotify lights
Commands and effects on a light's channels, and its temperature automation.
List predefined commands available to the partner
Requires the predefined-commands permission. Includes each parameter's effective min/max/default.
Example request
curl -X GET "https://api.blunotify.com/v1/commands" \ -H "Authorization: Bearer $KEY"
Responses
200 Command list with parameter bounds.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
403 Predefined commands not permitted.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the…
A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
Send a raw command
Requires the raw-command permission and channel access. Availability caps apply unless the partner also has the relaxed-caps permission.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| device_id required | path | string | |
| Idempotency-Key | header | string | A value of your choosing (a UUID, for example) that makes a retry safe. A repeat of the same request with the same key within 24 hours gets the first response back, status and body, without the work being done again; it carries Idempotent-Replayed: true. Scoped to your account and this route. The same key with a different request is a 422 idempotency_key_reused; a repeat while the first is still running is a 409 idempotency_in_progress. Only a successful (2xx) response is kept: after a failure the same key runs the request again. Not taken by the multipart file upload, which never overwrites anyway. |
Request body (JSON)
| Field | Type | Description |
|---|---|---|
| channel required | string: light1, light2, light3, sound, contact | |
| payload required | string |
Example request
curl -X POST "https://api.blunotify.com/v1/devices/DEVICE_ID/commands" \
-H "Authorization: Bearer $KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"channel": "light1", "payload": "sldff0000"}'
Responses
202 Published to the broker: the device takes it from there.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Idempotent-Replayed | true on a response replayed for a repeated Idempotency-Key. |
| Field | Type | Description |
|---|---|---|
| channel | string | |
| payload | string | |
| command | string |
{
"channel": "string",
"payload": "string",
"command": "string"
}
400 Invalid payload or channel. Or a body that is not valid JSON, or not a JSON object.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
403 Not permitted
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
404 Device outside scope
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
409 Code idempotency_in_progress: a request with the same Idempotency-Key is still running; retry after Retry-After.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
415 Code unsupported_media_type: the body was not sent as application/json.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
422 Code idempotency_key_reused: this Idempotency-Key was used with a different request.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 Rate limited A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation…
Rate limited A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
503 Publish failed
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
Trigger a predefined effect
Requires the predefined-commands permission, channel access, and that the command is granted. light2/light3 effects are auto-wrapped as repeating overlays (duration_s / interval_s).
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| device_id required | path | string | |
| channel required | path | string: light1, light2, light3, sound, contact | |
| Idempotency-Key | header | string | A value of your choosing (a UUID, for example) that makes a retry safe. A repeat of the same request with the same key within 24 hours gets the first response back, status and body, without the work being done again; it carries Idempotent-Replayed: true. Scoped to your account and this route. The same key with a different request is a 422 idempotency_key_reused; a repeat while the first is still running is a 409 idempotency_in_progress. Only a successful (2xx) response is kept: after a failure the same key runs the request again. Not taken by the multipart file upload, which never overwrites anyway. |
Request body (JSON)
| Field | Type | Description |
|---|---|---|
| command required | string | |
| params | object |
Example request
curl -X POST "https://api.blunotify.com/v1/devices/DEVICE_ID/channels/CHANNEL/effects" \
-H "Authorization: Bearer $KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"command": "flash_white", "params": {}}'
Responses
202 Published to the broker: the device takes it from there.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Idempotent-Replayed | true on a response replayed for a repeated Idempotency-Key. |
| Field | Type | Description |
|---|---|---|
| channel | string | |
| payload | string | |
| command | string |
{
"channel": "string",
"payload": "string",
"command": "string"
}
400 Invalid command or parameter. Or a body that is not valid JSON, or not a JSON object.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
403 Not permitted
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
404 Device outside scope
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
409 Code idempotency_in_progress: a request with the same Idempotency-Key is still running; retry after Retry-After.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
415 Code unsupported_media_type: the body was not sent as application/json.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
422 Code idempotency_key_reused: this Idempotency-Key was used with a different request.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 Rate limited A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation…
Rate limited A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
503 Publish failed
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
Enable/disable temperature-sensor automation
Requires the automation-control permission (an independent add-on permission). Disabling stops automation from overwriting light1.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| device_id required | path | string |
Request body (JSON)
| Field | Type | Description |
|---|---|---|
| enabled required | boolean |
Example request
curl -X PUT "https://api.blunotify.com/v1/devices/DEVICE_ID/automation" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"enabled": true}'
Responses
200 Updated: device_id and automation_enabled.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
400 Invalid body. Or a body that is not valid JSON, or not a JSON object.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
403 Automation-control permission not granted.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
404 Device outside scope.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
415 Code unsupported_media_type: the body was not sent as application/json.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the…
A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
Configuration
Device names and organization placement, sensors and alert profiles.
Read the organization tree (config surface)
Requires allow_config_read. Returns the granted subtrees as a forest (a granted subtree-root's parent_id is nulled); the whole tree with access_all_devices; an empty forest (not 403) when no nodes are granted.
Example request
curl -X GET "https://api.blunotify.com/v1/org-tree" \ -H "Authorization: Bearer $KEY"
Responses
200 { nodes: [{ id, parent_id, name }] }
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
403 config read permission not granted
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the…
A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
Read a device's configuration
Requires allow_config_read. Out-of-scope devices return 404 indistinguishable from nonexistent.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| device_id required | path | string |
Example request
curl -X GET "https://api.blunotify.com/v1/devices/DEVICE_ID/config" \ -H "Authorization: Bearer $KEY"
Responses
200 { device_id, name, org_node, sensors:[{tdl_id,label,profile}], automation_enabled, alert_signal_id }
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
403 config read permission not granted
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
404 Not in scope or not found
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the…
A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
Rename a device
Requires allow_config_write. Sets the device's canonical name.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| device_id required | path | string |
Request body (JSON)
| Field | Type | Description |
|---|---|---|
| name required | string |
Example request
curl -X PATCH "https://api.blunotify.com/v1/devices/DEVICE_ID/config" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"name": "string"}'
Responses
204 Renamed. No body.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
400 A body that is not valid JSON, or not a JSON object.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
403 denied
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
404 Not found
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
415 Code unsupported_media_type: the body was not sent as application/json.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the…
A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
Move a device to an org node
Requires allow_config_write. The target node must be within the partner's own scope (else 403 node_out_of_scope). For an org-scoped partner (no access_all_devices), org_node_id: null is ALWAYS 409 would_lose_access, since a granted subtree is the partner's only reach to the device; only a partner with access_all_devices may detach a device back to Staging / Inventory this way.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| device_id required | path | string |
Request body (JSON)
| Field | Type | Description |
|---|---|---|
| org_node_id required | integer or null |
Example request
curl -X PUT "https://api.blunotify.com/v1/devices/DEVICE_ID/org" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"org_node_id": 0}'
Responses
204 Moved. No body.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
400 A body that is not valid JSON, or not a JSON object.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
403 node_out_of_scope
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
404 Not found
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
409 would_lose_access
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
415 Code unsupported_media_type: the body was not sent as application/json.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the…
A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
Attach a sensor to a device
Requires allow_config_write. Incremental attach; 409 if already attached. profile_id must be global or partner-owned (else 404).
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| device_id required | path | string | |
| Idempotency-Key | header | string | A value of your choosing (a UUID, for example) that makes a retry safe. A repeat of the same request with the same key within 24 hours gets the first response back, status and body, without the work being done again; it carries Idempotent-Replayed: true. Scoped to your account and this route. The same key with a different request is a 422 idempotency_key_reused; a repeat while the first is still running is a 409 idempotency_in_progress. Only a successful (2xx) response is kept: after a failure the same key runs the request again. Not taken by the multipart file upload, which never overwrites anyway. |
Request body (JSON)
| Field | Type | Description |
|---|---|---|
| tdl_id required | string | |
| profile_id | integer or null |
Example request
curl -X POST "https://api.blunotify.com/v1/devices/DEVICE_ID/sensors" \
-H "Authorization: Bearer $KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"tdl_id": "string", "profile_id": 0}'
Responses
201 Attached. Location names the attachment (PUT and DELETE address it).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Idempotent-Replayed | true on a response replayed for a repeated Idempotency-Key. |
| Location | The resource created, or the one to poll for an outcome. |
| Field | Type | Description |
|---|---|---|
| device_id | string | |
| tdl_id | string | |
| profile_id | integer or null |
{
"device_id": "string",
"tdl_id": "string",
"profile_id": 0
}
400 A body that is not valid JSON, or not a JSON object.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
403 denied
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
404 Device or profile not found
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
409 already_attached. Or code idempotency_in_progress: a request with the same Idempotency-Key is still running; retry after Retry-After.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
415 Code unsupported_media_type: the body was not sent as application/json.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
422 Code idempotency_key_reused: this Idempotency-Key was used with a different request.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the…
A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
Set a sensor's alert profile
Requires allow_config_write. profile_id null clears to Default; a non-visible profile returns 404.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| device_id required | path | string | |
| tdl_id required | path | string |
Request body (JSON)
| Field | Type | Description |
|---|---|---|
| profile_id | integer or null |
Example request
curl -X PUT "https://api.blunotify.com/v1/devices/DEVICE_ID/sensors/TDL_ID" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"profile_id": 0}'
Responses
204 Updated. No body.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
400 A body that is not valid JSON, or not a JSON object.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
403 denied
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
404 Not attached / profile not found
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
415 Code unsupported_media_type: the body was not sent as application/json.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the…
A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
Detach a sensor from a device
Requires allow_config_write. 404 if not attached.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| device_id required | path | string | |
| tdl_id required | path | string |
Example request
curl -X DELETE "https://api.blunotify.com/v1/devices/DEVICE_ID/sensors/TDL_ID" \ -H "Authorization: Bearer $KEY"
Responses
204 Detached. No body.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
400 A body that is not valid JSON, or not a JSON object.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
403 denied
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
404 Not attached
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
415 Code unsupported_media_type: the body was not sent as application/json.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the…
A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
List alert profiles
Requires allow_config_read. Returns global + own partner-owned profiles.
Example request
curl -X GET "https://api.blunotify.com/v1/profiles" \ -H "Authorization: Bearer $KEY"
Responses
200 { profiles: [{ id, name, scope, version, description, rules }] }
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
403 denied
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the…
A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
Create a partner-owned alert profile
Requires allow_profile_manage. Body is validated by the rule engine. Cap 50 profiles per partner (403 limit_reached).
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| Idempotency-Key | header | string | A value of your choosing (a UUID, for example) that makes a retry safe. A repeat of the same request with the same key within 24 hours gets the first response back, status and body, without the work being done again; it carries Idempotent-Replayed: true. Scoped to your account and this route. The same key with a different request is a 422 idempotency_key_reused; a repeat while the first is still running is a 409 idempotency_in_progress. Only a successful (2xx) response is kept: after a failure the same key runs the request again. Not taken by the multipart file upload, which never overwrites anyway. |
Request body (JSON)
| Field | Type | Description |
|---|---|---|
| name required | string | |
| rules required | array of object | |
| default | string |
Example request
curl -X POST "https://api.blunotify.com/v1/profiles" \
-H "Authorization: Bearer $KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"name": "string", "rules": [{}], "default": "string"}'
Responses
201 Created. Location names the profile.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Idempotent-Replayed | true on a response replayed for a repeated Idempotency-Key. |
| Location | The resource created, or the one to poll for an outcome. |
| Field | Type | Description |
|---|---|---|
| id always | integer |
{
"id": 0
}
400 validation_error. Or a body that is not valid JSON, or not a JSON object.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
403 denied / limit_reached
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
409 name_taken. Or code idempotency_in_progress: a request with the same Idempotency-Key is still running; retry after Retry-After.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
415 Code unsupported_media_type: the body was not sent as application/json.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
422 Code idempotency_key_reused: this Idempotency-Key was used with a different request.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the…
A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
Update a partner-owned alert profile
Requires allow_profile_manage. Own rows only (global/system/other → 404). Bumps version + records a revision.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| profile_id required | path | integer |
Request body (JSON)
| Field | Type | Description |
|---|---|---|
| name | string | |
| rules | array of object | |
| default | string |
Example request
curl -X PUT "https://api.blunotify.com/v1/profiles/PROFILE_ID" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"name": "string", "rules": [{}], "default": "string"}'
Responses
200 Updated: the profile id and its new version.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Field | Type | Description |
|---|---|---|
| id always | integer | |
| version always | integer |
{
"id": 0,
"version": 0
}
400 validation_error. Or a body that is not valid JSON, or not a JSON object.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
403 denied
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
404 Not found
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
409 name_taken
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
415 Code unsupported_media_type: the body was not sent as application/json.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the…
A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
Delete a partner-owned alert profile
Requires allow_profile_manage. Own rows only; 409 in_use with a count when still referenced (no force).
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| profile_id required | path | integer |
Example request
curl -X DELETE "https://api.blunotify.com/v1/profiles/PROFILE_ID" \ -H "Authorization: Bearer $KEY"
Responses
204 Deleted. No body.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
400 A body that is not valid JSON, or not a JSON object.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
403 denied
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
404 Not found
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
409 in_use
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
415 Code unsupported_media_type: the body was not sent as application/json.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the…
A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
List alert signals
Requires allow_config_read. Returns the global signals (including the default) and your own partner-owned signals.
Example request
curl -X GET "https://api.blunotify.com/v1/alert-signals" \ -H "Authorization: Bearer $KEY"
Responses
200 { alert_signals: [AlertSignal] }
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Field | Type | Description |
|---|---|---|
| alert_signals always | array of object | |
| alert_signals[].id always | integer | |
| alert_signals[].name always | string | |
| alert_signals[].scope always | string: global, partner | |
| alert_signals[].is_default always | boolean | The default signal: what a light with no signal of its own shows. |
| alert_signals[].version always | integer | |
| alert_signals[].description | string or null | |
| alert_signals[].entries always | object | One entry per alert state group. NO_DATA covers NO_SENSORS, INSUFFICENT_DATA, STALE_DATA and UNKNOWN. |
| alert_signals[].entries.CLEAR always | object | |
| alert_signals[].entries.CLEAR.light always | object | A light effect: {type, ...fields}. type is Solid, Pulse, PulseThenSolid, Chase, ChaseThenSolid, Toggle, ToggleThenSolid or BluChase; the fields are the ones the Effect Designer posts (see the advanced docs). |
| alert_signals[].entries.CLEAR.light.type always | string: Solid, Pulse, PulseThenSolid, Chase, ChaseThenSolid, Toggle, ToggleThenSolid, BluChase | |
| alert_signals[].entries.ADVISORY always | object | |
| alert_signals[].entries.ADVISORY.light always | object | A light effect: {type, ...fields}. type is Solid, Pulse, PulseThenSolid, Chase, ChaseThenSolid, Toggle, ToggleThenSolid or BluChase; the fields are the ones the Effect Designer posts (see the advanced docs). |
| alert_signals[].entries.ADVISORY.light.type always | string: Solid, Pulse, PulseThenSolid, Chase, ChaseThenSolid, Toggle, ToggleThenSolid, BluChase | |
| alert_signals[].entries.WARNING always | object | |
| alert_signals[].entries.WARNING.light always | object | A light effect: {type, ...fields}. type is Solid, Pulse, PulseThenSolid, Chase, ChaseThenSolid, Toggle, ToggleThenSolid or BluChase; the fields are the ones the Effect Designer posts (see the advanced docs). |
| alert_signals[].entries.WARNING.light.type always | string: Solid, Pulse, PulseThenSolid, Chase, ChaseThenSolid, Toggle, ToggleThenSolid, BluChase | |
| alert_signals[].entries.NO_DATA always | object | |
| alert_signals[].entries.NO_DATA.light always | object | A light effect: {type, ...fields}. type is Solid, Pulse, PulseThenSolid, Chase, ChaseThenSolid, Toggle, ToggleThenSolid or BluChase; the fields are the ones the Effect Designer posts (see the advanced docs). |
| alert_signals[].entries.NO_DATA.light.type always | string: Solid, Pulse, PulseThenSolid, Chase, ChaseThenSolid, Toggle, ToggleThenSolid, BluChase |
{
"alert_signals": [
{
"id": 0,
"name": "string",
"scope": "global",
"is_default": true,
"version": 0,
"description": "string",
"entries": {
"CLEAR": {
"light": {
"type": "Solid"
}
},
"ADVISORY": {
"light": {
"type": "Solid"
}
},
"WARNING": {
"light": {
"type": "Solid"
}
},
"NO_DATA": {
"light": {
"type": "Solid"
}
}
}
}
]
}
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
403 denied
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the…
A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
Create a partner-owned alert signal
Requires allow_signal_manage. Every entry is validated as the device would run it, with the availability caps (no light period under 100 ms). Cap 50 signals per partner (403 limit_reached).
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| Idempotency-Key | header | string | A value of your choosing (a UUID, for example) that makes a retry safe. A repeat of the same request with the same key within 24 hours gets the first response back, status and body, without the work being done again; it carries Idempotent-Replayed: true. Scoped to your account and this route. The same key with a different request is a 422 idempotency_key_reused; a repeat while the first is still running is a 409 idempotency_in_progress. Only a successful (2xx) response is kept: after a failure the same key runs the request again. Not taken by the multipart file upload, which never overwrites anyway. |
Request body (JSON)
| Field | Type | Description |
|---|---|---|
| name required | string | |
| description | string | |
| entries required | object | One entry per alert state group. NO_DATA covers NO_SENSORS, INSUFFICENT_DATA, STALE_DATA and UNKNOWN. |
| entries.CLEAR required | object | |
| entries.CLEAR.light required | object | A light effect: {type, ...fields}. type is Solid, Pulse, PulseThenSolid, Chase, ChaseThenSolid, Toggle, ToggleThenSolid or BluChase; the fields are the ones the Effect Designer posts (see the advanced docs). |
| entries.CLEAR.light.type required | string: Solid, Pulse, PulseThenSolid, Chase, ChaseThenSolid, Toggle, ToggleThenSolid, BluChase | |
| entries.ADVISORY required | object | |
| entries.ADVISORY.light required | object | A light effect: {type, ...fields}. type is Solid, Pulse, PulseThenSolid, Chase, ChaseThenSolid, Toggle, ToggleThenSolid or BluChase; the fields are the ones the Effect Designer posts (see the advanced docs). |
| entries.ADVISORY.light.type required | string: Solid, Pulse, PulseThenSolid, Chase, ChaseThenSolid, Toggle, ToggleThenSolid, BluChase | |
| entries.WARNING required | object | |
| entries.WARNING.light required | object | A light effect: {type, ...fields}. type is Solid, Pulse, PulseThenSolid, Chase, ChaseThenSolid, Toggle, ToggleThenSolid or BluChase; the fields are the ones the Effect Designer posts (see the advanced docs). |
| entries.WARNING.light.type required | string: Solid, Pulse, PulseThenSolid, Chase, ChaseThenSolid, Toggle, ToggleThenSolid, BluChase | |
| entries.NO_DATA required | object | |
| entries.NO_DATA.light required | object | A light effect: {type, ...fields}. type is Solid, Pulse, PulseThenSolid, Chase, ChaseThenSolid, Toggle, ToggleThenSolid or BluChase; the fields are the ones the Effect Designer posts (see the advanced docs). |
| entries.NO_DATA.light.type required | string: Solid, Pulse, PulseThenSolid, Chase, ChaseThenSolid, Toggle, ToggleThenSolid, BluChase |
Example request
curl -X POST "https://api.blunotify.com/v1/alert-signals" \
-H "Authorization: Bearer $KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"name": "string", "description": "string", "entries": {"CLEAR": {"light": {"type": "Solid"}}, "ADVISORY": {"light": {"type": "Solid"}}, "WARNING": {"light": {"type": "Solid"}}, "NO_DATA": {"light": {"type": "Solid"}}}}'
Responses
201 Created. Location names the signal.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Idempotent-Replayed | true on a response replayed for a repeated Idempotency-Key. |
| Location | The resource created, or the one to poll for an outcome. |
| Field | Type | Description |
|---|---|---|
| id always | integer |
{
"id": 0
}
400 validation_error. Or a body that is not valid JSON, or not a JSON object.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
403 denied / limit_reached
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
409 name_taken. Or code idempotency_in_progress: a request with the same Idempotency-Key is still running; retry after Retry-After.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
415 Code unsupported_media_type: the body was not sent as application/json.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
422 Code idempotency_key_reused: this Idempotency-Key was used with a different request.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the…
A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
Update a partner-owned alert signal
Requires allow_signal_manage. Own rows only (global/default/other → 404). Send any of name, description, entries; bumps version. Lights showing it change at their next state change or within the hour.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| signal_id required | path | integer |
Request body (JSON)
| Field | Type | Description |
|---|---|---|
| name | string | |
| description | string | |
| entries | object | One entry per alert state group. NO_DATA covers NO_SENSORS, INSUFFICENT_DATA, STALE_DATA and UNKNOWN. |
| entries.CLEAR required | object | |
| entries.CLEAR.light required | object | A light effect: {type, ...fields}. type is Solid, Pulse, PulseThenSolid, Chase, ChaseThenSolid, Toggle, ToggleThenSolid or BluChase; the fields are the ones the Effect Designer posts (see the advanced docs). |
| entries.CLEAR.light.type required | string: Solid, Pulse, PulseThenSolid, Chase, ChaseThenSolid, Toggle, ToggleThenSolid, BluChase | |
| entries.ADVISORY required | object | |
| entries.ADVISORY.light required | object | A light effect: {type, ...fields}. type is Solid, Pulse, PulseThenSolid, Chase, ChaseThenSolid, Toggle, ToggleThenSolid or BluChase; the fields are the ones the Effect Designer posts (see the advanced docs). |
| entries.ADVISORY.light.type required | string: Solid, Pulse, PulseThenSolid, Chase, ChaseThenSolid, Toggle, ToggleThenSolid, BluChase | |
| entries.WARNING required | object | |
| entries.WARNING.light required | object | A light effect: {type, ...fields}. type is Solid, Pulse, PulseThenSolid, Chase, ChaseThenSolid, Toggle, ToggleThenSolid or BluChase; the fields are the ones the Effect Designer posts (see the advanced docs). |
| entries.WARNING.light.type required | string: Solid, Pulse, PulseThenSolid, Chase, ChaseThenSolid, Toggle, ToggleThenSolid, BluChase | |
| entries.NO_DATA required | object | |
| entries.NO_DATA.light required | object | A light effect: {type, ...fields}. type is Solid, Pulse, PulseThenSolid, Chase, ChaseThenSolid, Toggle, ToggleThenSolid or BluChase; the fields are the ones the Effect Designer posts (see the advanced docs). |
| entries.NO_DATA.light.type required | string: Solid, Pulse, PulseThenSolid, Chase, ChaseThenSolid, Toggle, ToggleThenSolid, BluChase |
Example request
curl -X PUT "https://api.blunotify.com/v1/alert-signals/SIGNAL_ID" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"name": "string", "description": "string", "entries": {"CLEAR": {"light": {"type": "Solid"}}, "ADVISORY": {"light": {"type": "Solid"}}, "WARNING": {"light": {"type": "Solid"}}, "NO_DATA": {"light": {"type": "Solid"}}}}'
Responses
200 Updated: the profile id and its new version.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Field | Type | Description |
|---|---|---|
| id always | integer | |
| version always | integer |
{
"id": 0,
"version": 0
}
400 validation_error. Or a body that is not valid JSON, or not a JSON object.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
403 denied
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
404 Not found
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
409 name_taken
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
415 Code unsupported_media_type: the body was not sent as application/json.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the…
A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
Delete a partner-owned alert signal
Requires allow_signal_manage. Own rows only. Allowed while in use: the lights showing it go back to the default signal.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| signal_id required | path | integer |
Example request
curl -X DELETE "https://api.blunotify.com/v1/alert-signals/SIGNAL_ID" \ -H "Authorization: Bearer $KEY"
Responses
204 Deleted. No body.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
400 A body that is not valid JSON, or not a JSON object.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
403 denied
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
404 Not found
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
415 Code unsupported_media_type: the body was not sent as application/json.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the…
A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
Choose a light's alert signal
Requires allow_config_write. alert_signal_id is a global or your own partner-owned signal (404 otherwise); null, or the default signal's id, means the default.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| device_id required | path | string |
Request body (JSON)
| Field | Type | Description |
|---|---|---|
| alert_signal_id required | integer or null |
Example request
curl -X PUT "https://api.blunotify.com/v1/devices/DEVICE_ID/alert-signal" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"alert_signal_id": 0}'
Responses
200 The device and the signal it now shows (null = default).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Field | Type | Description |
|---|---|---|
| device_id always | string | |
| alert_signal_id always | integer or null |
{
"device_id": "string",
"alert_signal_id": 0
}
400 A body that is not valid JSON, or not a JSON object.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
403 denied
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
404 Not attached / profile not found
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
415 Code unsupported_media_type: the body was not sent as application/json.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the…
A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
System
The device's own MQTT interface: settings, status, structured commands and the platform file server.
System command registry (System API)
Every system verb, its tier, parameters, whether it needs confirm, and whether the caller holds that tier. Also lists the destructive settings keys. Requires system read.
Example request
curl -X GET "https://api.blunotify.com/v1/system/commands" \ -H "Authorization: Bearer $KEY"
Responses
200 Verb registry + dangerous settings keys.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
403 System read not permitted.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the…
A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
List platform files (System API)
Files devices download (files/ + firmware/). Requires system read.
Example request
curl -X GET "https://api.blunotify.com/v1/system/files" \ -H "Authorization: Bearer $KEY"
Responses
200 File and firmware listings.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
403 System read not permitted.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the…
A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
Upload a platform file (System API)
Requires the file-upload permission. Extension allowlist; firmware/ accepts .bin only; size-capped. Uploads never overwrite (409 on name clash). An .mp3 uploaded into files/ is checked against what a BluNotify device can play: one that holds no MP3 (MPEG Layer III) audio is refused, and any other comes back with an audio assessment whose warnings say what would make it better (mono, 16 kHz, 48 kbps, no tags). Warnings never block.
Example request
curl -X POST "https://api.blunotify.com/v1/system/files" \ -H "Authorization: Bearer $KEY" \ -H "Idempotency-Key: $(uuidgen)"
Responses
201 Stored: dir, name, size, sha256, and `audio` for an .mp3 in files/.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Field | Type | Description |
|---|---|---|
| dir always | string: files, firmware | |
| name always | string | |
| size always | integer | |
| sha256 always | string | |
| audio | object | Present for an .mp3 uploaded into files/. |
| audio.playable always | boolean | The file holds MP3 (MPEG Layer III) audio a device can play. |
| audio.ideal always | boolean | No warnings: the file is ready for the device as it is. |
| audio.fixable always | boolean | Converting the file to the recommended format would fix at least one warning. |
| audio.summary always | object | |
| audio.summary.text | string | One line for people, e.g. "44.1 kHz, stereo, 192 kbps, 2 min 38 s, 3.63 MB, 37.2% of device storage". |
| audio.summary.format | string or null | e.g. "MPEG-1 Layer III", or the real format of a file that is not MP3 ("WAV"). |
| audio.summary.sample_rate_hz | integer or null | |
| audio.summary.channels | integer or null | |
| audio.summary.channel_mode | string or null: stereo, joint_stereo, dual_channel, mono, None | |
| audio.summary.bitrate_kbps | integer or null | Average over the audio. |
| audio.summary.vbr | boolean | |
| audio.summary.duration_s | number | |
| audio.summary.size_bytes | integer | |
| audio.summary.device_storage_pct | number | Share of a device's sound-file storage the file takes. |
| audio.summary.tag_bytes | integer | ID3 tags, cover art included. |
| audio.summary.cover_art | boolean | |
| audio.summary.header_frame | string or null: Xing, Info, VBRI, None | |
| audio.issues always | array of object | Most serious first. |
| audio.issues[].code always | string | e.g. not_mp3, not_layer3, too_big, large_file, stereo, high_sample_rate, high_bitrate, large_tags, uppercase_extension, low_sample_rate, long_clip. |
| audio.issues[].severity always | string: error, warn, info | |
| audio.issues[].message always | string | Plain-language explanation, fit to show a user. |
{
"dir": "files",
"name": "string",
"size": 0,
"sha256": "string",
"audio": {
"playable": true,
"ideal": true,
"fixable": true,
"summary": {
"text": "string",
"format": "string",
"sample_rate_hz": 0,
"channels": 0,
"channel_mode": "stereo",
"bitrate_kbps": 0,
"vbr": true,
"duration_s": 0.0,
"size_bytes": 0,
"device_storage_pct": 0.0,
"tag_bytes": 0,
"cover_art": true,
"header_frame": "Xing"
},
"issues": [
{
"code": "string",
"severity": "error",
"message": "string"
}
]
}
}
400 Invalid directory, name, or extension, or an .mp3 that holds no MP3 (MPEG Layer III) audio a BluNotify device can play.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
403 File upload not permitted.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
409 A file with that name already exists.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the…
A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
Delete a platform file (System API)
Requires the file-delete permission. Protected names are refused.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| dir | query | string: files, firmware | |
| name required | query | string |
Example request
curl -X DELETE "https://api.blunotify.com/v1/system/files?name=NAME" \ -H "Authorization: Bearer $KEY"
Responses
204 Deleted. No body.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
400 Invalid directory or name (or protected). Or a body that is not valid JSON, or not a JSON object.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
403 File delete not permitted.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
404 File not found.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
415 Code unsupported_media_type: the body was not sent as application/json.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the…
A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
Cached device settings (System API)
Latest NVS settings, fw_ver, and file_list. Requires system read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| device_id required | path | string |
Example request
curl -X GET "https://api.blunotify.com/v1/devices/DEVICE_ID/system/settings" \ -H "Authorization: Bearer $KEY"
Responses
200 Settings, firmware version, and file list.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
403 System read not permitted.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
404 Device outside scope.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the…
A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
Device status snapshot & health (System API)
Status, last connection string, on-demand status snapshots, and the health heartbeat. Requires system read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| device_id required | path | string |
Example request
curl -X GET "https://api.blunotify.com/v1/devices/DEVICE_ID/system/status" \ -H "Authorization: Bearer $KEY"
Responses
200 The connectivity fields (status, status_changed_at, last_seen_at), last_conn, status_snapshots and health.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
403 System read not permitted.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
404 Device outside scope.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the…
A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
Command response feed (System API)
Recent command acknowledgements and device log lines. Page with since/limit; use server_now as the next cursor. Requires system read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| device_id required | path | string | |
| cursor | query | string | A previous answer's next_cursor: the rows after it, oldest first. Wins over since. |
| limit | query | integer | Default 200, at most 1000. |
| since | query | string (date-time) | The older way to page: rows at or after this instant (percent-encode the '+' of its offset). Prefer cursor. |
Example request
curl -X GET "https://api.blunotify.com/v1/devices/DEVICE_ID/system/responses" \ -H "Authorization: Bearer $KEY"
Responses
200 A page of rows: oldest first with a cursor. Without one, the last ten minutes (or since).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Field | Type | Description |
|---|---|---|
| device_id | string | |
| rows always | array of object | |
| rows[].id | integer | |
| rows[].created_at | string (date-time) | |
| rows[].topic | string | |
| rows[].payload | string | |
| has_more always | boolean | More rows are waiting now: ask again with next_cursor at once. |
| next_cursor always | string | Always set: pass it back as cursor. With no rows it is the cursor sent, or a position at the present. |
| server_now | string (date-time) | For the older since paging. |
{
"device_id": "string",
"rows": [
{
"id": 0,
"created_at": "2026-09-30T14:00:00+00:00",
"topic": "string",
"payload": "string"
}
],
"has_more": true,
"next_cursor": "string",
"server_now": "2026-09-30T14:00:00+00:00"
}
400 An invalid cursor or since.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
403 System read not permitted.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
404 Device outside scope.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the…
A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
Run a system verb (System API)
Control verbs need system control; destructive verbs need system destructive + confirm == device_id. Fire-and-forget (202). Call GET /v1/system/commands for the verb list and parameters.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| device_id required | path | string | |
| Idempotency-Key | header | string | A value of your choosing (a UUID, for example) that makes a retry safe. A repeat of the same request with the same key within 24 hours gets the first response back, status and body, without the work being done again; it carries Idempotent-Replayed: true. Scoped to your account and this route. The same key with a different request is a 422 idempotency_key_reused; a repeat while the first is still running is a 409 idempotency_in_progress. Only a successful (2xx) response is kept: after a failure the same key runs the request again. Not taken by the multipart file upload, which never overwrites anyway. |
Request body (JSON)
| Field | Type | Description |
|---|---|---|
| command required | string | |
| params | object | |
| confirm | string | Required for destructive verbs; must equal the device id. |
Example request
curl -X POST "https://api.blunotify.com/v1/devices/DEVICE_ID/system/commands" \
-H "Authorization: Bearer $KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"command": "query", "params": {}, "confirm": "string"}'
Responses
202 Accepted
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Idempotent-Replayed | true on a response replayed for a repeated Idempotency-Key. |
| Field | Type | Description |
|---|---|---|
| topic | string | |
| payload | string | |
| sent_at | string (date-time) | |
| command | string | |
| key | string |
{
"topic": "string",
"payload": "string",
"sent_at": "2026-09-30T14:00:00+00:00",
"command": "string",
"key": "string"
}
400 Unknown verb or invalid params. Or a body that is not valid JSON, or not a JSON object.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
403 Tier not permitted.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
404 Device outside scope.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
409 Destructive verb missing/mismatched confirm. Or code idempotency_in_progress: a request with the same Idempotency-Key is still running;…
Destructive verb missing/mismatched confirm. Or code idempotency_in_progress: a request with the same Idempotency-Key is still running; retry after Retry-After.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
415 Code unsupported_media_type: the body was not sent as application/json.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
422 Code idempotency_key_reused: this Idempotency-Key was used with a different request.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 Rate limited. A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation…
Rate limited. A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
503 Publish failed.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
Write an NVS settings key (System API)
Ordinary keys need system control; connection/identity keys (mqttUser, mqttPass, devId, topicRoot, host, tlsMode, mqttPort, caCertFile) need system destructive + confirm == device_id. Fire-and-forget (202). On a BluEM device the keys emAck, pmBaud, pmSlave and emCtTurns are fixed by the meter hardware and its wiring and cannot be written: 403 forbidden, whatever the caller's permissions.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| device_id required | path | string | |
| key required | path | string |
Request body (JSON)
| Field | Type | Description |
|---|---|---|
| value required | string | |
| confirm | string |
Example request
curl -X PUT "https://api.blunotify.com/v1/devices/DEVICE_ID/system/settings/KEY" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"value": "string", "confirm": "string"}'
Responses
202 Accepted
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Field | Type | Description |
|---|---|---|
| topic | string | |
| payload | string | |
| sent_at | string (date-time) | |
| command | string | |
| key | string |
{
"topic": "string",
"payload": "string",
"sent_at": "2026-09-30T14:00:00+00:00",
"command": "string",
"key": "string"
}
400 Invalid key or value. Or a body that is not valid JSON, or not a JSON object.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
403 Tier not permitted, or the key is locked for this device's product.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
404 Device outside scope.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
409 Dangerous key missing/mismatched confirm.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
415 Code unsupported_media_type: the body was not sent as application/json.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 Rate limited. A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation…
Rate limited. A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
503 Publish failed.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
File transfers
Pushing a platform file to a device as a job to poll.
Push a platform file to a device, as a transfer to poll
Requires system control (system_control); polling the transfer needs system read (system_read). Sends addfile> with a correlation token and records a durable transfer. An Idempotency-Key header makes a retry return the original transfer (200) instead of downloading twice.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| device_id required | path | string | |
| Idempotency-Key | header | string | A value of your choosing (a UUID, for example) that makes a retry safe. A repeat of the same request with the same key within 24 hours gets the first response back, status and body, without the work being done again; it carries Idempotent-Replayed: true. Scoped to your account and this route. The same key with a different request is a 422 idempotency_key_reused; a repeat while the first is still running is a 409 idempotency_in_progress. Only a successful (2xx) response is kept: after a failure the same key runs the request again. Not taken by the multipart file upload, which never overwrites anyway. |
Request body (JSON)
| Field | Type | Description |
|---|---|---|
| name required | string | A file in the platform files/ directory. |
| dest | string | Its name on the device; defaults to name. |
Example request
curl -X POST "https://api.blunotify.com/v1/devices/DEVICE_ID/files" \
-H "Authorization: Bearer $KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"name": "string", "dest": "string"}'
Responses
200 The transfer an earlier request with the same Idempotency-Key created.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Idempotent-Replayed | true on a response replayed for a repeated Idempotency-Key. |
| Field | Type | Description |
|---|---|---|
| transfer_id always | string | |
| device_id always | string | |
| state always | string: queued, sent, succeeded, failed, timed_out | |
| terminal always | boolean | Final: stop polling. |
| src always | string | The platform file pushed. |
| dest always | string | Its name on the device. |
| sha256 always | string | |
| corr_mode | string | |
| attempts always | integer | |
| detail | string or null | The device's reply, or why it timed out. |
| timeline always | object | |
| timeline.created_at | string (date-time) or null | |
| timeline.sent_at | string (date-time) or null | |
| timeline.ack_deadline | string (date-time) or null | |
| timeline.settled_at | string (date-time) or null | |
| note | string | While sent: the device drops its MQTT session while it downloads. |
| device_online | boolean | POST only: whether the device was connected when the push was sent. |
| poll_after_s always | number or null | Seconds to wait before polling again; null once terminal. |
{
"transfer_id": "string",
"device_id": "string",
"state": "queued",
"terminal": true,
"src": "string",
"dest": "string",
"sha256": "string",
"corr_mode": "string",
"attempts": 0,
"detail": "string",
"timeline": {
"created_at": "2026-09-30T14:00:00+00:00",
"sent_at": "2026-09-30T14:00:00+00:00",
"ack_deadline": "2026-09-30T14:00:00+00:00",
"settled_at": "2026-09-30T14:00:00+00:00"
},
"note": "string",
"device_online": true,
"poll_after_s": 0.0
}
202 Sent; poll the transfer. Location names the resource to poll.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Location | The resource created, or the one to poll for an outcome. |
| Idempotent-Replayed | true on a response replayed for a repeated Idempotency-Key. |
| Field | Type | Description |
|---|---|---|
| transfer_id always | string | |
| device_id always | string | |
| state always | string: queued, sent, succeeded, failed, timed_out | |
| terminal always | boolean | Final: stop polling. |
| src always | string | The platform file pushed. |
| dest always | string | Its name on the device. |
| sha256 always | string | |
| corr_mode | string | |
| attempts always | integer | |
| detail | string or null | The device's reply, or why it timed out. |
| timeline always | object | |
| timeline.created_at | string (date-time) or null | |
| timeline.sent_at | string (date-time) or null | |
| timeline.ack_deadline | string (date-time) or null | |
| timeline.settled_at | string (date-time) or null | |
| note | string | While sent: the device drops its MQTT session while it downloads. |
| device_online | boolean | POST only: whether the device was connected when the push was sent. |
| poll_after_s always | number or null | Seconds to wait before polling again; null once terminal. |
{
"transfer_id": "string",
"device_id": "string",
"state": "queued",
"terminal": true,
"src": "string",
"dest": "string",
"sha256": "string",
"corr_mode": "string",
"attempts": 0,
"detail": "string",
"timeline": {
"created_at": "2026-09-30T14:00:00+00:00",
"sent_at": "2026-09-30T14:00:00+00:00",
"ack_deadline": "2026-09-30T14:00:00+00:00",
"settled_at": "2026-09-30T14:00:00+00:00"
},
"note": "string",
"device_online": true,
"poll_after_s": 0.0
}
400 Missing or invalid name, unknown file, or a type this device does not accept. Or a body that is not valid JSON, or not a JSON object.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
403 System control not permitted.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
404 Device outside scope.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
409 The device has no registered product. Or code idempotency_in_progress: a request with the same Idempotency-Key is still running; retry…
The device has no registered product. Or code idempotency_in_progress: a request with the same Idempotency-Key is still running; retry after Retry-After.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
415 Code unsupported_media_type: the body was not sent as application/json.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
422 Code idempotency_key_reused: this Idempotency-Key was used with a different request.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 Per-device system-command limit. A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device…
Per-device system-command limit. A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
503 Publish failed.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
This partner's transfers for a device, newest first
Requires system read (system_read). ?state= and ?since= filter; the reconciliation read after an outage.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| device_id required | path | string | |
| state | query | string | |
| since | query | string (date-time) | |
| limit | query | integer |
Example request
curl -X GET "https://api.blunotify.com/v1/devices/DEVICE_ID/files/transfers" \ -H "Authorization: Bearer $KEY"
Responses
200 Transfers.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Field | Type | Description |
|---|---|---|
| device_id always | string | |
| transfers always | array of object | |
| transfers[].transfer_id always | string | |
| transfers[].device_id always | string | |
| transfers[].state always | string: queued, sent, succeeded, failed, timed_out | |
| transfers[].terminal always | boolean | Final: stop polling. |
| transfers[].src always | string | The platform file pushed. |
| transfers[].dest always | string | Its name on the device. |
| transfers[].sha256 always | string | |
| transfers[].corr_mode | string | |
| transfers[].attempts always | integer | |
| transfers[].detail | string or null | The device's reply, or why it timed out. |
| transfers[].timeline always | object | |
| transfers[].timeline.created_at | string (date-time) or null | |
| transfers[].timeline.sent_at | string (date-time) or null | |
| transfers[].timeline.ack_deadline | string (date-time) or null | |
| transfers[].timeline.settled_at | string (date-time) or null | |
| transfers[].note | string | While sent: the device drops its MQTT session while it downloads. |
| transfers[].device_online | boolean | POST only: whether the device was connected when the push was sent. |
| transfers[].poll_after_s always | number or null | Seconds to wait before polling again; null once terminal. |
{
"device_id": "string",
"transfers": [
{
"transfer_id": "string",
"device_id": "string",
"state": "queued",
"terminal": true,
"src": "string",
"dest": "string",
"sha256": "string",
"corr_mode": "string",
"attempts": 0,
"detail": "string",
"timeline": {
"created_at": "2026-09-30T14:00:00+00:00",
"sent_at": "2026-09-30T14:00:00+00:00",
"ack_deadline": "2026-09-30T14:00:00+00:00",
"settled_at": "2026-09-30T14:00:00+00:00"
},
"note": "string",
"device_online": true,
"poll_after_s": 0.0
}
]
}
400 Invalid since.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
403 System read not permitted.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
404 Device outside scope.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the…
A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
Poll one transfer
Requires system read (system_read). Terminal when terminal is true; until then wait poll_after_s seconds. A transfer id that is not this partner's, or not for this device, is the same 404 as an unknown one.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| device_id required | path | string | |
| transfer_id required | path | string |
Example request
curl -X GET "https://api.blunotify.com/v1/devices/DEVICE_ID/files/transfers/TRANSFER_ID" \ -H "Authorization: Bearer $KEY"
Responses
200 The transfer.
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Field | Type | Description |
|---|---|---|
| transfer_id always | string | |
| device_id always | string | |
| state always | string: queued, sent, succeeded, failed, timed_out | |
| terminal always | boolean | Final: stop polling. |
| src always | string | The platform file pushed. |
| dest always | string | Its name on the device. |
| sha256 always | string | |
| corr_mode | string | |
| attempts always | integer | |
| detail | string or null | The device's reply, or why it timed out. |
| timeline always | object | |
| timeline.created_at | string (date-time) or null | |
| timeline.sent_at | string (date-time) or null | |
| timeline.ack_deadline | string (date-time) or null | |
| timeline.settled_at | string (date-time) or null | |
| note | string | While sent: the device drops its MQTT session while it downloads. |
| device_online | boolean | POST only: whether the device was connected when the push was sent. |
| poll_after_s always | number or null | Seconds to wait before polling again; null once terminal. |
{
"transfer_id": "string",
"device_id": "string",
"state": "queued",
"terminal": true,
"src": "string",
"dest": "string",
"sha256": "string",
"corr_mode": "string",
"attempts": 0,
"detail": "string",
"timeline": {
"created_at": "2026-09-30T14:00:00+00:00",
"sent_at": "2026-09-30T14:00:00+00:00",
"ack_deadline": "2026-09-30T14:00:00+00:00",
"settled_at": "2026-09-30T14:00:00+00:00"
},
"note": "string",
"device_online": true,
"poll_after_s": 0.0
}
401 Missing, malformed, unknown, revoked or disabled API key (one generic answer for all of them).
| Header | Description |
|---|---|
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| WWW-Authenticate | Bearer realm="BluNotify Partner API" (RFC 6750), with error="invalid_token" when a key was sent. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
403 System read not permitted.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
404 Device outside scope, or transfer not found.
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}
429 A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the…
A rate limit was reached: the per-partner request limit (RateLimit-Remaining is then 0), or a per-device or per-operation limit of the route. Retry-After says when to retry.
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying. |
| X-Request-Id | The id of this request, on every response. Error bodies carry the same value as request_id; quote it to BluNotify support to find the call. |
| RateLimit-Limit | The per-partner request limit: requests per minute. |
| RateLimit-Remaining | Requests the current minute still admits. |
| RateLimit-Reset | Seconds until the oldest request counted in the current minute leaves it, i.e. until RateLimit-Remaining next grows. |
| Cache-Control | Always no-store on /v1: answers are one partner's, and some carry secrets. |
| Field | Type | Description |
|---|---|---|
| error always | string | |
| code always | string | |
| request_id always | string | The same value as the X-Request-Id header. |
| required_permission | string | On a 403 refused because the key lacks a permission the request needs: its name, as GET /v1/account lists it (for example energy_read). Permissions are checked first, so with it granted the request can still be refused for something else. Absent on a 403 no permission would lift, and never on a 404. |
{
"error": "string",
"code": "string",
"request_id": "string",
"required_permission": "string"
}