Curtailment
Read, schedule, apply, and release curtailment — and why the grid permit is never exceeded.
Curtailment caps a plant's output. The API lets you read active and scheduled curtailment (curtailment:read) and command it — schedule a window, curtail immediately, or release a group (curtailment:write, a control scope).
The grid permit is never exceeded. Curtailment can only ever reduce output. Your plant's TSO/DSO grid-connection permit is a hard ceiling that no API call — curtail, release, or "restore to full" — can raise. The edge clamps every apply and every restore to the permit across multiple independent layers, and the setting that holds the permit is deliberately not part of this API. A release restores power, but that restore is itself clamped to the permit. Treat control keys like passwords and revoke immediately if exposed.
The model
- A curtail command creates a group of schedule rows (one per affected plant) and returns a
group_id. - You release by
group_id— never by individual row. Releasing restores clamped power first, then cancels the group. GET …/curtailmentreturns both active and scheduled rows; each row carriesgroup_id,source,applied,restored, andis_active. There is no separate "active" endpoint — read active curtailments here.
Shared body semantics
Both write endpoints share these fields:
| Field | Type | Required | Meaning |
|---|---|---|---|
scope | "site" | "logger" | yes | site caps the whole site; logger caps one plant. |
logger_id | integer | only when scope="logger" | Which plant to cap. |
limit_mode | "percent" | "kw" | yes | percent = % of rated power; kw = an absolute ceiling in kilowatts. |
limit_value | number > 0 | yes | The limit, interpreted per limit_mode. |
Percent vs kW. percent caps each affected plant at that fraction of its rated power. kw sets an absolute ceiling — and for scope="site" with kw, limit_value is the total site kW, split across plants proportional to their rated power.
Read active & scheduled curtailment
curl "https://api.owner.dynvolt.com/v1/sites/pv-ljubas/curtailment" \
-H "Authorization: Bearer $DYNVOLT_API_KEY"rows = client.get("/v1/sites/pv-ljubas/curtailment").json()["data"]
active = [r for r in rows if r["is_active"]]Schedule a future window
POST …/curtailment/window adds start_at / end_at (RFC3339; end_at must be after start_at) to the shared fields.
curl -X POST "https://api.owner.dynvolt.com/v1/sites/pv-ljubas/curtailment/window" \
-H "Authorization: Bearer $DYNVOLT_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"scope": "logger",
"logger_id": 4,
"start_at": "2026-08-12T10:00:00Z",
"end_at": "2026-08-12T14:00:00Z",
"limit_mode": "percent",
"limit_value": 60
}'import uuid
resp = client.post(
"/v1/sites/pv-ljubas/curtailment/window",
headers={"Idempotency-Key": str(uuid.uuid4())},
json={
"scope": "logger",
"logger_id": 4,
"start_at": "2026-08-12T10:00:00Z",
"end_at": "2026-08-12T14:00:00Z",
"limit_mode": "percent",
"limit_value": 60,
},
).json()
group_id = resp["group_id"]Response 201:
{
"group_id": "grp_8a1f",
"schedules": [
{ "id": 501, "logger_id": 4, "start_at": "2026-08-12T10:00:00Z",
"end_at": "2026-08-12T14:00:00Z", "max_power_kw": 792.0 }
]
}Curtail now
POST …/curtailment/immediate applies within ~10 seconds. Instead of a window it takes an optional duration_minutes:
| Field | Type | Required | Meaning |
|---|---|---|---|
duration_minutes | integer > 0, ≤ 10080 | no | How long to hold (max 7 days). Omit or null = hold until released — still bounded to 7 days. |
curl -X POST "https://api.owner.dynvolt.com/v1/sites/pv-ljubas/curtailment/immediate" \
-H "Authorization: Bearer $DYNVOLT_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"scope": "site",
"limit_mode": "kw",
"limit_value": 3500,
"duration_minutes": 120
}'The example caps the whole site at a 3,500 kW total, split proportionally across plants, for two hours.
Release a group
DELETE …/curtailment/group/{group_id} — no body. It restores clamped power first (re-clamped to the permit), then cancels every row in the group.
curl -X DELETE "https://api.owner.dynvolt.com/v1/sites/pv-ljubas/curtailment/group/grp_8a1f" \
-H "Authorization: Bearer $DYNVOLT_API_KEY" \
-H "Idempotency-Key: $(uuidgen)"Suggest a limit from forecast
GET …/curtailment/suggest returns a suggested power limit derived from the production forecast for a plant and hour window. All four parameters are required:
| Query parameter | Type | Meaning |
|---|---|---|
logger_id | integer | Plant to suggest for. |
date | YYYY-MM-DD | Target day (UTC). |
start_hour | 0–23 | Window start hour. |
end_hour | 1–24 | Window end hour (must exceed start_hour). |
curl "https://api.owner.dynvolt.com/v1/sites/pv-ljubas/curtailment/suggest?logger_id=4&date=2026-08-12&start_hour=10&end_hour=14" \
-H "Authorization: Bearer $DYNVOLT_API_KEY"Safe mode
While a site is in commissioning safe mode, all writes — curtailment included — return 409 conflict_safe_mode. Reads keep working. Retry once commissioning completes.

