DynVoltDYNVOLT · developers

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 …/curtailment returns both active and scheduled rows; each row carries group_id, source, applied, restored, and is_active. There is no separate "active" endpoint — read active curtailments here.

Shared body semantics

Both write endpoints share these fields:

FieldTypeRequiredMeaning
scope"site" | "logger"yessite caps the whole site; logger caps one plant.
logger_idintegeronly when scope="logger"Which plant to cap.
limit_mode"percent" | "kw"yespercent = % of rated power; kw = an absolute ceiling in kilowatts.
limit_valuenumber > 0yesThe 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:

FieldTypeRequiredMeaning
duration_minutesinteger > 0, ≤ 10080noHow 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 parameterTypeMeaning
logger_idintegerPlant to suggest for.
dateYYYY-MM-DDTarget day (UTC).
start_hour0–23Window start hour.
end_hour1–24Window 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.