# Segments and Flex Shape Segments partition your locations into groups based on the combination of asset types present at each household. The flex shape endpoint returns a rolling 24-hour forecast of flexible load, broken down by segment. ## Segments A segment defines a household profile — which asset types must be present and which must be absent. All assets at a matching location belong to that segment. Segments apply across all zones. They are not tied to a specific one. ### Creating a Segment ```http POST /flex/segments Content-Type: application/json { "name": "ev-only", "household": { "includes": ["vehicle"], "excludes": ["solar", "battery"] } } ``` ```json { "id": "seg_abc123", "name": "ev-only", "household": { "includes": ["vehicle"], "excludes": ["solar", "battery"] }, "createdAt": "2026-03-17T10:00:00Z", "updatedAt": "2026-03-17T10:00:00Z" } ``` | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | Human-readable name. Must be unique. | | `household.includes` | string[] | yes | Asset types the household must have. All listed types must be present. | | `household.excludes` | string[] | no | Asset types the household must not have. None of the listed types may be present. | ### The Filter Language A segment filter describes what asset types must (or must not) be present at a household: - `includes` — the household must have at least one asset of each listed type (AND logic) - `excludes` — the household must not have any asset of the listed types - When `excludes` is omitted, there is no restriction on additional asset types ### Disjointness Segments are validated to be mutually exclusive on creation. If a new segment would overlap with an existing one, the request is rejected. This means every household matches at most one segment — no priority ordering is needed. A typical set of segments for a portfolio with EVs, solar, and batteries: ```json // POST /flex/segments { "name": "ev-only", "household": { "includes": ["vehicle"], "excludes": ["solar", "battery"] } } // POST /flex/segments { "name": "ev-solar", "household": { "includes": ["vehicle", "solar"], "excludes": ["battery"] } } // POST /flex/segments { "name": "ev-battery", "household": { "includes": ["vehicle", "battery"] } } ``` These are disjoint because: - **ev-only** requires vehicle, forbids solar and battery - **ev-solar** requires vehicle and solar, forbids battery - **ev-battery** requires vehicle and battery (solar allowed or not) A household with `[vehicle, solar, battery]` matches **ev-battery** (which does not exclude solar). A household with just `[vehicle]` matches **ev-only**. No ambiguity. ### Listing Segments ```http GET /flex/segments ``` Returns all segment definitions. ### Updating a Segment ```http PUT /flex/segments/seg_abc123 Content-Type: application/json { "name": "ev-only", "household": { "includes": ["vehicle"], "excludes": ["solar", "battery", "heatPump"] } } ``` The disjointness check runs against all other existing segments. If the update would create an overlap, it is rejected. Changes take effect on the next shape computation. ### Deleting a Segment ```http DELETE /flex/segments/seg_abc123 ``` Assets at households previously in this segment will appear in `unassigned` on the next shape computation. ## Flex Shape The flex shape endpoint returns a rolling 24-hour forecast of flexible load for a zone, broken down by segment. ### Retrieving the Shape ```http GET /flex/shape/NO1 ``` To filter by area, pass the `area` query parameter: ```http GET /flex/shape/NO1?area=FA_12 GET /flex/shape/NO1?area=FA_12,FA_13 ``` **Response:** ```json { "zoneId": "NO1", "computedAt": "2026-03-17T10:00:00Z", "chunkSizeMinutes": 15, "revisionId": "abc-def-123", "total": { "assetCount": 60, "startAt": "2026-03-17T10:00:00Z", "endAt": "2026-03-18T10:00:00Z", "chunks": [ { "timestamp": "2026-03-17T10:00:00Z", "forecast": { "expectedKw": 75000, "minimumKw": 55000, "maximumKw": 101000, "confidence": 0.95 } } ] }, "segments": [ { "segmentId": "seg_abc123", "name": "ev-only", "assetCount": 42, "chunks": [ { "timestamp": "2026-03-17T10:00:00Z", "forecast": { "expectedKw": 50000, "minimumKw": 38000, "maximumKw": 68000, "confidence": 0.96 } } ] }, { "segmentId": "seg_def456", "name": "ev-solar", "assetCount": 18, "chunks": [ { "timestamp": "2026-03-17T10:00:00Z", "forecast": { "expectedKw": 25000, "minimumKw": 17000, "maximumKw": 33000, "confidence": 0.93 } } ] } ], "unassigned": { "assetCount": 0, "chunks": [] } } ``` The response contains three parts: - **`total`** — aggregates all assets in the zone regardless of segment assignment - **`segments`** — one entry per defined segment that has matched households - **`unassigned`** — assets at households that matched no segment Each chunk contains a `timestamp` and a `forecast` with: | Field | Description | | --- | --- | | `expectedKw` | The expected flexible load at this point in time. | | `minimumKw` | The lower bound of the flexibility range. | | `maximumKw` | The upper bound of the flexibility range. | | `confidence` | Confidence level for the forecast interval. | ### Additivity Segments sum to total. For every chunk: ```text total.expectedKw == sum(segments[*].expectedKw) + unassigned.expectedKw total.minimumKw == sum(segments[*].minimumKw) + unassigned.minimumKw total.maximumKw == sum(segments[*].maximumKw) + unassigned.maximumKw ``` This holds because segments are provably disjoint and each household contributes to exactly one bucket. ### Single Segment Access To retrieve the shape for a single segment: ```http GET /flex/shape/NO1/segments/seg_abc123 GET /flex/shape/NO1/segments/seg_abc123?area=FA_12 ``` ```json { "startAt": "2026-03-17T10:00:00Z", "endAt": "2026-03-18T10:00:00Z", "chunkSizeMinutes": 15, "computedAt": "2026-03-17T10:00:00Z", "assetCount": 42, "revisionId": "abc-def-123", "chunks": [ { "timestamp": "2026-03-17T10:00:00Z", "forecast": { "expectedKw": 50000, "minimumKw": 38000, "maximumKw": 68000, "confidence": 0.96 } } ] } ``` ### Status Health indicators for a zone, with a per-segment breakdown: ```http GET /flex/shape/NO1/status ``` ```json { "zoneId": "NO1", "computedAt": "2026-03-17T10:00:00Z", "isForecastStable": true, "isSolvent": true, "isControlNominal": true, "segments": [ { "segmentId": "seg_abc123", "name": "ev-only", "isForecastStable": true, "isSolvent": true, "isControlNominal": true }, { "segmentId": "seg_def456", "name": "ev-solar", "isForecastStable": false, "isSolvent": true, "isControlNominal": true } ] } ``` ### Zero-Config Behavior If no segments are defined, the flex shape endpoint still works. All assets appear in `total` and `unassigned`, and the `segments` array is empty: ```json { "zoneId": "NO1", "computedAt": "2026-03-17T10:00:00Z", "chunkSizeMinutes": 15, "revisionId": "abc-def-123", "total": { "assetCount": 60, "startAt": "2026-03-17T10:00:00Z", "endAt": "2026-03-18T10:00:00Z", "chunks": ["..."] }, "segments": [], "unassigned": { "assetCount": 60, "chunks": ["..."] } } ``` You can start using the flex shape endpoint immediately and add segments later when you need per-segment breakdowns. ## Endpoint Reference | Task | Method | Endpoint | | --- | --- | --- | | List segments | GET | `/flex/segments` | | Create segment | POST | `/flex/segments` | | Get segment | GET | `/flex/segments/{segmentId}` | | Update segment | PUT | `/flex/segments/{segmentId}` | | Delete segment | DELETE | `/flex/segments/{segmentId}` | | Get shape (all segments) | GET | `/flex/shape/{zoneId}` | | Get shape (single segment) | GET | `/flex/shape/{zoneId}/segments/{segmentId}` | | Get shape status | GET | `/flex/shape/{zoneId}/status` |