Skip to content

Alerts

An alert is a persistent record that something is wrong with one of your systems. It is raised once, stays open while the condition holds, and moves to resolved when it stops. It is not a log line: the same problem does not produce a new alert every minute.

Alerts are evaluated once a minute for every active system. You can read them with GET /alerts (scope alerts:read), or have them pushed to you as webhooks.

{
"id": "6b1f5c8e-3a7d-4f21-9c0b-2d5e8a41f7b3",
"kind": "soc.low",
"severity": "warning",
"status": "open",
"serialNumber": "1234567890",
"title": "Low state of charge",
"message": "Battery on 1234567890 is at 14%, at or below the 20% alert level.",
"data": {
"socPercent": 14.0,
"thresholdPercent": 20,
"source": "device",
"measuredAt": "2026-09-17T09:41:00Z"
},
"raisedAt": "2026-09-17T09:41:30Z",
"resolvedAt": null
}

title and message are English and written for a human to read. Build your logic on kind, severity and data, never on the wording of message - the wording can change in a non-breaking release. data is an object whose keys depend on kind.

Raised when a system has not reported for longer than its offlineAfterMinutes setting. Resolved when it reports again.

Severity Always critical
Title System offline
Message No data received from {serialNumber} for {n} minutes.
data lastSeenAt, offlineForMinutes, thresholdMinutes
{ "lastSeenAt": "2026-09-17T07:10:00Z", "offlineForMinutes": 94, "thresholdMinutes": 30 }

A system that has never reported at all does not raise this alert. “Never seen” is not the same as “went silent”, and there is nothing to resolve either.

Raised when state of charge is at or below the socLowPercent setting.

Severity critical when SoC is at or below half the threshold, otherwise warning
Title Low state of charge
Message Battery on {serialNumber} is at {percent}%, at or below the {threshold}% alert level.
data socPercent, thresholdPercent, source, measuredAt
{ "socPercent": 14.0, "thresholdPercent": 20, "source": "device", "measuredAt": "2026-09-17T09:41:00Z" }

source is device for the minute-fresh reading from the unit, or hourly when that was unavailable and the latest hourly bucket was used instead. measuredAt is absent when the reading carries no timestamp. socPercent is rounded to one decimal and is the same number that appears in message.

Severity is not escalated while the alert is open. A battery that was at 18 percent when the alert was raised and later falls to 4 percent keeps the warning it was raised with. Resolving and re-raising would send you an alert.resolved webhook for a battery that got worse, which is the more misleading of the two options. If you need the current value, read /telemetry/latest.

Raised when the unit reports a failure code.

Severity critical when the code’s level is 3 or higher, otherwise warning
Title System fault
Message {serialNumber} reports error code {code}., or {serialNumber} reports error code {code}: {description}. when the code is in the catalogue
data code, and description, solution, level when the code is in the catalogue
{ "code": "27", "description": "Inverter over-temperature", "solution": "Check ventilation around the unit and restart it.", "level": 3 }

The code 0 means “no active error” and does not raise an alert.

A different code is a different fault. If a system moves from code 27 to code 41, the open alert is resolved and a new one is raised, so your history shows both rather than one alert whose message quietly changed.

Raised when the latest daily health snapshot is critical.

Severity Always critical
Title Health check critical
Message The latest health check for {serialNumber} is critical (score {n} of 100).
data status, overallScore
{ "status": "Critical", "overallScore": 31 }

Health snapshots are produced daily, so this alert changes state at most once a day. A system with no snapshot yet raises nothing, and the absence of a snapshot is never read as a recovery.

raise resolve
(no alert) ────────► status: "open" ────────────► status: "resolved"
▲ │
└──── new alert, new id ◄────────┘
  • At most one open alert per system and kind. While a soc.low alert is open for a system, the evaluator will not raise a second one.
  • Resolving is final for that alert. resolvedAt is set and status becomes resolved. If the condition returns, a new alert with a new id is raised. Never assume an id will be reused.
  • A missing signal changes nothing. If the platform has no reading for a system, the evaluator leaves the current state alone rather than resolving on a gap.
  • Disabling a kind closes what it left open. Turn off socAlertsEnabled and any open soc.low alert for that system is resolved, because an alert you can no longer see resolve would otherwise stay open forever.

A battery sitting exactly on the threshold would otherwise raise and resolve every minute. So the two edges are not the same number:

  • Raise when SoC is at or below socLowPercent.
  • Resolve only when SoC reaches socLowPercent + 5 or higher.

Between the threshold and the threshold plus 5, nothing changes. With the default threshold of 20 percent, the alert is raised at 20 percent and resolved at 25 percent.

GET /api/v1/external/alerts?status=open&page=1&pageSize=50 scope alerts:read
GET /api/v1/external/alerts/{id} scope alerts:read
Parameter Values
status open or resolved. Omit for both
kind system.offline, soc.low, system.fault, health.critical
serialNumber One of your serials
since ISO 8601 UTC; alerts raised at or after this moment
page, pageSize Standard pagination
Terminal window
curl -sS -G https://api.claytonpower.com/api/v1/external/alerts \
-H "X-API-Key: $CP_API_KEY" \
--data-urlencode "status=open" \
--data-urlencode "kind=system.offline"

Only your company’s alerts are returned. GET /alerts/{id} for an alert belonging to another company returns 404.

A polling integration should ask for status=open on a schedule and use since to pick up what has changed. A push integration should use webhooks instead - alerts are exactly what they carry.

Thresholds are per system.

GET /api/v1/external/systems/{serialNumber}/alert-settings scope alerts:read
PUT /api/v1/external/systems/{serialNumber}/alert-settings scope alerts:manage
{
"serialNumber": "1234567890",
"socAlertsEnabled": true,
"socLowPercent": 20,
"offlineAlertsEnabled": true,
"offlineAfterMinutes": 30,
"faultAlertsEnabled": true,
"healthAlertsEnabled": true,
"isDefault": true,
"updatedAt": null
}
Field Type Default Range
socAlertsEnabled boolean true
socLowPercent integer 20 1 to 99
offlineAlertsEnabled boolean true
offlineAfterMinutes integer 30 5 to 1440
faultAlertsEnabled boolean true
healthAlertsEnabled boolean true

isDefault is true when nothing has been stored for this system and every value is a platform default. updatedAt is null until the first change.

PUT is a partial update. Fields you omit keep their current effective value, so sending one field does not silently reset the others to their defaults.

Terminal window
curl -sS -X PUT \
https://api.claytonpower.com/api/v1/external/systems/1234567890/alert-settings \
-H "X-API-Key: $CP_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "socLowPercent": 30 }'

A value outside its range is rejected with 400 and errorCode = ValidationFailed; the detail of the response names the field and its bounds. An unknown or foreign serial returns 404.

Changed settings take effect on the next evaluation run, within a minute. Raising socLowPercent above a battery’s current level raises the alert on that run; lowering it below the level resolves an open alert once the hysteresis margin is met.