# Validation of bids & dispatches

> How the EBX API checks bids and dispatch requests against the asset's declared reality, and what the response tells you.

Knowledge hub

9 min

How the EBX API checks bids and dispatch requests against the asset's declared reality, and what the response tells you.

## In short

Every write to the EBX API (a wholesale schedule, an immediate dispatch, an FCR bid, an aFRR capacity or energy bid) is checked against what EBX knows about the asset: its configured properties, the restrictions you have declared, and the obligations already committed for the same delivery window.

Checks run per 15-minute delivery block and per direction. Discharge and export are positive; charge and import are negative. A request is stored completely or not at all, and a rejection names every delivery window that caused it.

* * *

## Who this is for

**Optimisers** integrating against the EBX v1 API who need to know which requests will be accepted, which will be refused, and what the response contains.

## How validation works

### What EBX validates against

Validation uses **declared** data, not live telemetry, so that the result for a future delivery window is deterministic and reproducible:

-   **Asset properties**: usable power and energy capacity, grid connection power, whether grid import is allowed, and the supported control modes.
-   **Prequalified capacity**: the approved FCR power and the approved aFRR power per direction. A value of `0.0` is a real ceiling, not an unset field.
-   **Enablement**: `fcr_enabled`, `afrr_enabled`, `wholesale_enabled`, `control_enabled`.
-   **Restrictions you declared**: unavailability, power limits (MW, signed) and energy limits (MWh), submitted through the restriction endpoints.
-   **Committed obligations**: FCR and aFRR bids and awards, aFRR energy bids, the wholesale schedule, and an active immediate dispatch.

Live asset state (available charge and discharge power, state of charge) is used for execution, not for validating future windows.

### Rules that apply to every write path

-   **First committed wins.** A request is validated against everything committed before it. Nothing already committed is displaced by a later request. Because ancillary services outrank wholesale, a wholesale schedule has to fit around bids and awards. That is enforced at submission once an asset has `strict_validation` enabled. Until then a conflict comes back as a warning, the schedule is accepted, and the target is clipped during execution.
-   **Pending results count as committed.** An offered bid counts at its offered power until a result is known. Once the result arrives, the award replaces the offer. For a partial clearance, only the cleared power stays committed. Capacity is released only when a bid is rejected or fails.
-   **Committed statuses** are `PENDING`, `SUBMITTED`, `SUCCESSFUL` and `FALLBACK`. `FALLBACK` (the auction did not clear and the TSO replacement price applies) is a terminal state that still counts against your ceilings.
-   **Restrictions are conservative at the edges.** A restriction that touches any part of a delivery window applies to the whole window, because an award binds its entire block. An open-ended restriction covers everything after its start.
-   **Multiple bids per block are allowed.** You may place several bids for the same block and direction at different prices; ceilings apply to their sum.
-   **Timestamps** are ISO 8601 with an explicit UTC offset. Bid intervals and query windows are aligned to 15 minutes, and all windows are half-open.
-   **Idempotency.** A duplicate `idempotency_key` for the same interval within one request is rejected with 422. For aFRR the scope is the same interval **and** product. Reusing one key across different intervals or products is allowed. A retry with a known key returns the original result.

## Market windows and product shapes

| Write path | Delivery unit | Market gate (Europe/Berlin) | EBX recommended gate |
| --- | --- | --- | --- |
| FCR capacity bids | 4-hour block, symmetric (`NEGPOS`) | D-1 08:00 | 07:30 |
| aFRR capacity bids | 4-hour block, directional (`POS` / `NEG`) | D-1 09:00 | 08:30 |
| aFRR energy bids | One 15-minute MTU | 25 min before the MTU | 30 min before |
| Scheduled dispatch | 15-minute block | 6 min before the block starts | n/a |
| Immediate dispatch | From now until `end_time` | n/a | n/a |

The market gate is the enforced gate: after it, the request is refused. Between the EBX recommended gate and the market gate, changes are accepted on a best-effort basis, and EBX cannot guarantee that they are forwarded to the market or resynchronised.

Capacity blocks are 4 hours in Berlin local time, which means 3 or 5 UTC hours on the two clock-change days. Energy products are single 15-minute MTUs, so a day has 92, 96 or 100 of them. The open tender blocks for a delivery day are listed by [`GET /v1/assets/{id}/markets/{product}/bids/intervals`](https://docs.ebx.energy/reference/fcr.md); a bid interval that does not match one of them is refused.

Bid power is an integer number of MW, at least 1 MW. Capacity prices are at least 0 with at most two decimals. Energy prices carry at most two decimals and may be negative, and the sign sets the payment direction towards the market.

## What is checked on each write path

### Scheduled dispatch

[`PUT /v1/assets/{id}/dispatch/scheduled`](/reference/dispatch/#put-assets-asset-id-dispatch-scheduled) carries per-second `bess_power_mw` and `poc_power_mw` targets.

Each 15-minute block your request touches is **replaced in full**. A block resets to 0 MW at its start unless a target is placed there, so a partial resubmission clears what it does not restate.

Refused at submission:

-   The asset does not have `wholesale_enabled = true`.
-   A target falls in a block that starts in fewer than 6 minutes, or beyond the end of the next UTC day.
-   The schedule is empty, two targets share the same second, or `pv_power_mw` is set.
-   The `control_mode` does not match the power fields supplied, or a point-of-connection mode is used on an asset without a controllable point of connection.
-   The magnitude of `poc_power_mw` exceeds `grid_connection_power_mw`; import is requested while `grid_import_allowed = false`; or, in `bess_and_poc`, the battery target exceeds the point-of-connection target.

### Immediate dispatch

[`POST /v1/assets/{id}/dispatch/immediate`](/reference/dispatch/#post-assets-asset-id-dispatch-immediate) sets one `set_power_mw` from now until `end_time`.

Immediate dispatch is a **break-glass override**. It is deliberately not gated by `wholesale_enabled`, operational status or control flags, and it deliberately bypasses restrictions, market commitments and live availability. EBX applies it best effort. Market obligations are not checked at submission, and the execution-time protection intended for them is not implemented today, so an immediate dispatch can currently displace an FCR or aFRR obligation.

Refused at submission:

-   `start_time`, if supplied, is not the current moment (1 second tolerance); `end_time` is not in the future or not after the start; the window is shorter than the minimum duration.
-   A separate immediate dispatch already covers part of the requested window: one scheduled to start later, for example. A dispatch that is merely **active** is not a refusal: posting a new one supersedes it automatically, and the old one is truncated at the handoff so its history is kept.

For anything other than an emergency intervention, use the scheduled dispatch endpoint.

### FCR and aFRR bids

`POST` / `PUT` / `DELETE` on [`/v1/assets/{id}/markets/fcr/bids`](https://docs.ebx.energy/reference/fcr.md), [`/markets/afrr/capacity/bids`](https://docs.ebx.energy/reference/afrr.md) and [`/markets/afrr/energy/bids`](https://docs.ebx.energy/reference/afrr.md).

Refused at submission:

-   The asset is not enabled for the market (`fcr_enabled`, `afrr_enabled`).
-   The gate for the delivery block has passed.
-   The interval is not an open tender block for that product.
-   `offered_power_mw` is not an integer of at least 1 MW, or a price breaks the format rules above.
-   A price sits outside the market’s legal range: a capacity price above **15,000 EUR/MW**, or a balancing-energy price outside **−15,000 to 15,000 EUR/MWh**. A capacity bid is checked on both its own price and the energy price it would activate at, so an impossible energy price cannot enter through the capacity endpoint.
-   The committed power for the asset exceeds its **prequalified capacity**: total FCR power for FCR, and per-direction aFRR power for aFRR. Today capacity and energy bids are metered against the aFRR prequalification independently of each other.
-   The bid is no longer editable: `start_time` and `end_time` are immutable once a bid has been submitted to the market, and only `PENDING` and `SUBMITTED` bids can be changed or deleted. Power and price stay editable until the gate.
-   A `REPLACE` batch does not carry a `replace_scope` and every bid inside it. The batch is stored as a whole or refused as a whole.

FCR bids carry no direction: the same power is reserved for charging and discharging. aFRR bids are directional (`pos` is discharge into the grid, `neg` is charge from the grid), and the direction cannot change after submission. An aFRR capacity bid carries a linked energy price, because an award creates a mandatory energy bid at that price.

An edit that arrives after the bid was submitted to the market is queued for resynchronisation and may end as `FAILED_LATE_EDIT`.

The price limits are the market’s own, not ours, so they are configuration rather than fixed schema bounds and can change without a release — the figures above are today’s defaults. A negative balancing-energy price is deliberately legal, because paying to absorb energy is a real offer; that is why only that field has a floor at all. Every price in the request that breaks its bound is reported together, so you are not fixing them one rejection at a time.

## Power and energy EBX reserves for your obligations

Balancing obligations need headroom beyond their nominal power. EBX reserves it at execution, and checks it at submission once an asset has <code>strict\_validation</code> enabled:

| Reservation | Amount |
| --- | --- |
| FCR power, both directions | 1.25 × contracted FCR power |
| FCR energy, each direction | ⅓ MWh per MW (20 minutes at full power) |
| aFRR capacity energy, awarded direction | 1 MWh per MW (60 minutes) |
| aFRR energy, per 15-minute MTU | 0.25 MWh per MW |
| Energy allocation order | FCR, then aFRR capacity, then aFRR energy |

Energy reservations are **not** checked when a bid is placed. Available energy changes over the 24 hours between a day-ahead bid and delivery, and recharge management is the optimiser's responsibility.

At execution, ancillary obligations are served first and the wholesale target is limited to what remains: the dispatcher gives wholesale the power left after 1.25 × the FCR reservation and the aFRR reservation for that block. Where that happens, the wholesale dispatch is reported as clipped in the dispatch report. Once an asset has `strict_validation` enabled, the same arithmetic runs at submission, so a request that would breach the reservation is refused up front instead of surfacing as a clip after the fact. Until the flag is on it comes back as a warning and the request is still accepted.

### Worked example

A 10 MW / 20 MWh asset, delivery block 12:00 to 16:00.

-   FCR is awarded 4 MW. That reserves 5 MW in each direction (1.25 × 4) and 1.33 MWh on each side.
-   aFRR capacity `pos` is awarded 3 MW. The discharge reservation is now 8 MW, with a 3 MWh discharge buffer on top.
-   2 MW of discharge power is left for wholesale in that block. A +5 MW wholesale target is 3 MW beyond the asset's discharge capability and can only be delivered at 2 MW.
-   On the charge side, 5 MW remains after the 5 MW FCR reservation, with no aFRR `neg` award to consume it.

## Restrictions you declare

Unavailability, power limits and energy limits are declared through [`/v1/assets/{id}/unavailability`](https://docs.ebx.energy/reference/unavailability.md), [`/power_limits`](https://docs.ebx.energy/reference/limits.md) and [`/energy_limits`](https://docs.ebx.energy/reference/limits.md).

-   A restriction is **always accepted**, even when it conflicts with an obligation already committed. Physical reality wins, and EBX needs to know about it in order to arrange backup for the affected obligation. The obligation itself is not cancelled by the restriction, and the contractual terms for that shortfall apply.
-   Shortening or deleting a restriction is always allowed, since it only adds capability.
-   Geometry: `start_time` cannot be in the past, the window is at least 15 minutes, windows of the same type may not overlap, `PATCH` can only shorten a window, and `DELETE` works only on a window that has not started. A power limit whose upper and lower bound are both zero (within 0.5 kW) means the asset is unavailable.
-   Bounds have to be physically possible: power-limit bounds stay within plus or minus `usable_power_capacity_mw`, and energy-limit bounds between 0 and `usable_energy_capacity_mwh`. If a bound is omitted, the asset's capacity must be configured so that the open side can be resolved.

DSO curtailment is not part of validation. It is applied at the asset by the EMS and shows up in the asset's live available power.

## Error responses

422 · The request is structurally invalid. Usually fixable without looking at the asset; the exception is `INVALID_INTERVAL`, which needs the open tender blocks.

404 · The asset does not exist for this caller.

409 · The request conflicts with the asset's state, its restrictions, or existing commitments.

Rejections carry a machine-readable `error` code. The codes in use today are:

GATE\_CLOSED

the gate for that delivery block has passed. The body carries `gate_time`.

MARKET\_DISABLED

the asset is not enabled for that market.

SETPOINT\_OUT\_OF\_RANGE

a power value is outside the range the asset can accept.

PRICE\_OUT\_OF\_RANGE

a bid price is outside the market’s configured range. Every offending price is listed, each with its `field_name`, the `value` you sent, and the `minimum` and `maximum` in force. Nothing was stored.

IMMEDIATE\_DISPATCH\_OVERLAP

another immediate dispatch overlaps the window. An **active** dispatch does not raise this, because it is superseded automatically.

FCR\_PREQUALIFIED\_CAPACITY\_EXCEEDED / AFRR\_PREQUALIFIED\_CAPACITY\_EXCEEDED

committed power would exceed the asset's prequalified capacity.

INVALID\_INTERVAL

the bid interval is not an open tender block. The body carries `suggested_intervals`.

A rejection that is specific to delivery windows lists them, with the committed value, the ceiling it breaches and the amount over. Adjacent violating blocks are merged into one window, so a 4-hour block whose sixteen quarter-hours all breach is reported once:

Rejection · 409

```json
{ · 
   · "detail" · : ·   · { · 
     · "error" · : ·   · "AFRR_PREQUALIFIED_CAPACITY_EXCEEDED" · , · 
     · "message" · : ·   · "Committed aFRR capacity-market power would exceed the asset's prequalified capacity in 1 delivery window(s). No bids were stored." · , · 
     · "market" · : ·   · "capacity" · , · 
     · "violations" · : ·   · [ · 
       · { · 
         · "start_time" · : ·   · "2026-09-01T12:00:00Z" · , · 
         · "end_time" · : ·   · "2026-09-01T16:00:00Z" · , · 
         · "product" · : ·   · "pos" · , · 
         · "committed_power_mw" · : ·   · 5.0 · , · 
         · "prequalified_capacity_mw" · : ·   · 4.0 · , · 
         · "exceeded_by_mw" · : ·   · 1.0 · 
       · } · 
     · ] · 
   · } · 
 · }
```

Every error body is wrapped in `detail`. `committed_power_mw` is what the asset would hold in that window with your request applied, not your request's own contribution, and `prequalified_capacity_mw` is the ceiling it breaches, so the comparison reads as "this is what you would be holding, and this is all you may hold". An aFRR body also carries the `market` the bids were for, and each violation the `product`; an FCR body carries neither, because FCR has one symmetric ceiling and no direction.

Checks run cheapest first (structure, then enablement, then the gate, then ceilings), but all violations of the failing check are reported, not only the first one.
