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.
Alert shape
Section titled “Alert shape”{ "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.
system.offline
Section titled “system.offline”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.
soc.low
Section titled “soc.low”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.
system.fault
Section titled “system.fault”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.
health.critical
Section titled “health.critical”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.
Lifecycle
Section titled “Lifecycle” raise resolve (no alert) ────────► status: "open" ────────────► status: "resolved" ▲ │ └──── new alert, new id ◄────────┘- At most one open alert per system and kind. While a
soc.lowalert is open for a system, the evaluator will not raise a second one. - Resolving is final for that alert.
resolvedAtis set andstatusbecomesresolved. If the condition returns, a new alert with a newidis 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
socAlertsEnabledand any opensoc.lowalert for that system is resolved, because an alert you can no longer see resolve would otherwise stay open forever.
Hysteresis on soc.low
Section titled “Hysteresis on soc.low”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 + 5or 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.
Listing alerts
Section titled “Listing alerts”GET /api/v1/external/alerts?status=open&page=1&pageSize=50 scope alerts:readGET /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 |
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.
Settings
Section titled “Settings”Thresholds are per system.
GET /api/v1/external/systems/{serialNumber}/alert-settings scope alerts:readPUT /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.
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.