Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions documentation/api/change_log.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,16 @@ API change log

.. note:: The FlexMeasures API follows its own versioning scheme. This is also reflected in the URL (e.g. `/api/v3_0`), allowing developers to upgrade at their own pace.

v3.0-31 | 2026-06-01
v3.0-32 | 2026-06-18
""""""""""""""""""""
- Support filtering assets and sensors by ID prefix in the ``filter`` query parameter of ``GET /assets``, ``GET /assets/<id>/sensors`` and ``GET /sensors``.
- **Field canonicalization** for background job tracking:
* The ``job_id`` field is now the canonical way to identify background jobs returned by `/sensors/<id>/schedules/trigger`, `/assets/<id>/schedules/trigger`, and `/sensors/<id>/forecasts/trigger` endpoints. If applicable, the triggered response now also returns a ``job_results_url`` pointing to the sensor-specific results endpoint, alongside the generic ``job_monitor_url``.
* Legacy `schedule` field (in scheduling endpoints) and `forecast` field (in forecasting endpoints) are deprecated but remain in responses for backward compatibility. All responses include a ``_deprecated_fields`` object with machine-readable migration guidance for each deprecated field, including the recommended replacement and removal date.


- Support filtering assets and sensors by ID prefix in the ``filter`` query parameter of ``GET /assets``, ``GET /assets/<id>/sensors`` and ``GET /sensors``.
v3.0-31 | 2026-06-01
""""""""""""""""""""
- Added a unified job status endpoint ``GET /api/v3_0/jobs/<uuid>`` that returns the current execution status and a human-readable result message for any background job (scheduling, forecasting, etc.) identified by its UUID.
- Switched from ``force_new_job_creation`` to ``force-new-job-creation`` (maintaining backwards compatibility) and added the field to `/assets/(id)/schedules/trigger` (POST) endpoint, too.
- Support both snake_case and kebab-case fields in `/sensors/<id>/data` (GET), while only documenting the kebab-case ones.
Expand Down
117 changes: 117 additions & 0 deletions documentation/api/introduction.rst
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,91 @@ Here is a client-side code example in Python for handling 303 redirects (this me
print(f"Failed to fetch fallback schedule: {response.status_code} {response.text}")
return response

.. _api_background_jobs:

Background job monitoring
--------------------------

Several API endpoints queue background jobs for asynchronous processing (scheduling, forecasting, data ingestion) and return a ``202 Accepted`` response.
These responses include a ``job_id`` field (the canonical identifier) that clients can use to monitor job progress and retrieve results.
They also include both ``job_monitor_url`` for generic status monitoring and (if applicable) ``job_results_url`` for the sensor-specific results endpoint.

**Example 202 Accepted response from a scheduling endpoint:**

.. code-block:: json

{
"status": "ACCEPTED",
"job_id": "364bfd06-c1fa-430b-8d25-8f5a547651fb",
"job_monitor_url": "/api/v3_0/jobs/364bfd06-c1fa-430b-8d25-8f5a547651fb",
"job_results_url": "/api/v3_0/sensors/3/schedules/364bfd06-c1fa-430b-8d25-8f5a547651fb",
"message": "Request has been accepted for processing."
}

**Monitoring job status:**

Clients should use the ``job_id`` to query the unified job status endpoint:

.. code-block:: bash

GET /api/v3_0/jobs/<job_id>

This returns the current execution status and a human-readable result message. For example:

.. code-block:: python

import requests
import time

def wait_for_job(job_id, job_monitor_url, timeout=300, poll_interval=5):
"""Wait for a background job to complete and return the result.

Parameters
----------
job_id : str
The UUID of the background job.
job_monitor_url : str
The URL to query for job status (e.g., "/api/v3_0/jobs/<uuid>").
timeout : int
Maximum seconds to wait for job completion.
poll_interval : int
Seconds between status checks.
"""
start_time = time.time()
while time.time() - start_time < timeout:
response = requests.get(job_monitor_url)
if not response.ok:
raise RuntimeError(f"Failed to query job status: {response.status_code} {response.text}")

job_data = response.json()
status = job_data.get("status")

if status in ("PENDING", "RUNNING"):
print(f"Job {job_id} is still {status.lower()}...")
time.sleep(poll_interval)
elif status == "SUCCEEDED":
return job_data.get("result")
else: # Failed, error, etc.
raise RuntimeError(f"Job failed with status {status}: {job_data.get('message')}")

raise TimeoutError(f"Job {job_id} did not complete within {timeout} seconds")

.. note::

For **schedules**, after the job completes successfully, use the job ID (same value as the legacy ``schedule`` field) to retrieve the actual schedule or follow the returned ``job_results_url``:

.. code-block:: bash

GET /api/v3_0/sensors/<sensor_id>/schedules/<job_id>

For **forecasts**, after the job completes successfully, use the job ID to retrieve the forecast or follow the returned ``job_results_url``:

.. code-block:: bash

GET /api/v3_0/sensors/<sensor_id>/forecasts/<job_id>

Both of these endpoints will also return `202 Accepted` if the job is still being computed, so clients can continue to poll them directly if they prefer.

.. _api_deprecation:

Deprecation and sunset
Expand All @@ -166,6 +251,38 @@ For more information on our multi-stage deprecation approach and available optio

.. _api_deprecation_clients:

Deprecated response fields
^^^^^^^^^^^^^^^^^^^^^^^^^^^

In addition to deprecating entire endpoints, we sometimes deprecate individual fields in API responses while maintaining backward compatibility by including both the legacy and canonical fields.
When this happens, responses include a ``_deprecated_fields`` object containing machine-readable information about deprecated fields.

For example, when scheduling endpoints switched from ``schedule`` to ``job_id`` as the canonical field identifier for background jobs:

.. code-block:: json

{
"status": "ACCEPTED",
"job_id": "364bfd06-c1fa-430b-8d25-8f5a547651fb",
"schedule": "364bfd06-c1fa-430b-8d25-8f5a547651fb",
"job_monitor_url": "/api/v3_0/jobs/364bfd06-c1fa-430b-8d25-8f5a547651fb",
"job_results_url": "/api/v3_0/sensors/3/schedules/364bfd06-c1fa-430b-8d25-8f5a547651fb",
"_deprecated_fields": {
"schedule": {
"use": "job_id",
"deprecated_since": "1.0.0",
"removal_date": "2.0.0",
"note": "The 'schedule' response field is deprecated; use 'job_id' instead"
}
}
}

Clients should:

- Use the ``_deprecated_fields`` object to identify which fields are deprecated in their version.
- Migrate to use the canonical field names (indicated by the ``"use"`` field).
- Plan upgrades before the ``"removal_date"`` to avoid breakage when the deprecated fields are removed in a future API version.

Clients
^^^^^^^

Expand Down
5 changes: 5 additions & 0 deletions documentation/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ FlexMeasures Changelog
v1.0.0 | July XX, 2026
============================

.. warning:: As of this release we standardize asynchronous job responses to use the `job_id` field and return HTTP `202 Accepted` when a background job is queued. Legacy response fields such as ``schedule`` and ``forecast`` will be deprecated; clients should migrate to `job_id` (see the Infrastructure / Support section below for migration details).


New features
-------------
* In the UI, asset and sensor lists can be filtered by ID prefix through API-backed search fields [see `PR #2231 <https://www.github.com/FlexMeasures/flexmeasures/pull/2231>`_]
Expand All @@ -18,6 +21,8 @@ New features

Infrastructure / Support
----------------------
* Standardize job-trigger API responses to return `202 Accepted` and a canonical `job_id` field; legacy response fields such as ``schedule`` and ``forecast`` are preserved for backward-compatibility but marked deprecated with a migration guide in the API docs (see issue #2171).

* Upgraded dependencies [see `PR #1485 <https://www.github.com/FlexMeasures/flexmeasures/pull/1485>`_ and `PR #2215 <https://www.github.com/FlexMeasures/flexmeasures/pull/2215>`_]
* Prepare the ``device_scheduler`` to deal with commitments per device group [see `PR #1934 <https://www.github.com/FlexMeasures/flexmeasures/pull/1934>`_]
* Documentation section on the modelling choice for recording measurements, forecasts and schedules under one or multiple sensors [see `PR #2217 <https://www.github.com/FlexMeasures/flexmeasures/pull/2217>`_]
Expand Down
4 changes: 4 additions & 0 deletions documentation/tut/toy-example-expanded.rst
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ This will have an effect on the available headroom for the battery, given the ``
}
}

.. note::
The API returns a **202 Accepted** response containing a ``job_id`` field.
Use this UUID to monitor the job status via :ref:`api_background_jobs`.

.. tab:: FlexMeasures Client

Using the `FlexMeasures Client <https://pypi.org/project/flexmeasures-client/>`_:
Expand Down
14 changes: 14 additions & 0 deletions documentation/tut/toy-example-from-scratch.rst
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,20 @@ There is more information being used by the scheduler, such as the battery's cap
}

.. note:: You can try this right in Swagger UI, too! You should find it at `http://localhost:5000/api/v3_0/docs <http://localhost:5000/api/v3_0/docs>`_ after starting FlexMeasures locally.

The API returns a **202 Accepted** response with a ``job_id`` field to track the scheduling job:

.. code-block:: json

{
"status": "ACCEPTED",
"job_id": "364bfd06-c1fa-430b-8d25-8f5a547651fb",
"schedule": "364bfd06-c1fa-430b-8d25-8f5a547651fb",
"job_monitor_url": "/api/v3_0/jobs/364bfd06-c1fa-430b-8d25-8f5a547651fb",
"job_results_url": "/api/v3_0/sensors/2/schedules/364bfd06-c1fa-430b-8d25-8f5a547651fb"
}

**Important:** Use the ``job_id`` field to reference the scheduling job. You can also follow the returned ``job_results_url`` to fetch the schedule results once they are ready. For more details, see :ref:`api_background_jobs`.

.. tab:: FlexMeasures Client

Expand Down
46 changes: 38 additions & 8 deletions flexmeasures/api/common/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from flask import url_for

from flexmeasures.auth.error_handling import FORBIDDEN_MSG, FORBIDDEN_STATUS_CODE
from flexmeasures.utils.time_utils import server_now

p = inflect.engine()

Expand Down Expand Up @@ -383,16 +384,45 @@ def request_processed(message: str) -> ResponseTuple:
def request_accepted_for_processing(
job_id: str,
message: str = "Request has been accepted for processing.",
legacy_key: str | None = None,
deprecated_since: str | None = None,
removal_date: str | None = None,
job_results_url: str | None = None,
) -> ResponseTuple:
return (
dict(
status="ACCEPTED",
message=message,
job_monitor_url=url_for("JobAPI:get_job_status", uuid=job_id),
job_id=job_id,
),
202,
"""
Standard 202 response when a background job is accepted.

Optional backwards-compatibility: if `legacy_key` is provided the response
will include the job id under that legacy key (e.g. `schedule` or
`forecast`). The response will also include a `_deprecated_fields` object
with guidance for migrating to `job_id`.

Optional `job_results_url` may be supplied to provide a direct link to the
sensor-specific job results endpoint, e.g. `/api/v3_0/sensors/<id>/schedules/<uuid>`.
"""
resp: dict[str, object] = dict(
status="ACCEPTED",
message=message,
job_monitor_url=url_for("JobAPI:get_job_status", uuid=job_id),
job_id=job_id,
)
if job_results_url:
resp["job_results_url"] = job_results_url

if legacy_key:
# keep legacy key for backwards compatibility
resp[legacy_key] = job_id
# include machine-readable deprecation info so clients can detect it
resp["_deprecated_fields"] = {
legacy_key: dict(
use="job_id",
deprecated_since=deprecated_since or server_now().date().isoformat(),
removal_date=removal_date,
note=(f"The '{legacy_key}' response field is deprecated; use 'job_id'"),
)
}

return resp, 202


def request_too_large(message: str) -> ResponseTuple:
Expand Down
31 changes: 21 additions & 10 deletions flexmeasures/api/v3_0/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
)
from flexmeasures.api.common.responses import (
unprocessable_entity,
request_processed,
request_accepted_for_processing,
)
from flexmeasures.api.common.schemas.users import AccountIdField
from flexmeasures.api.common.schemas.assets import default_response_fields
Expand Down Expand Up @@ -1479,23 +1479,34 @@ def trigger_schedule(
site-consumption-capacity: {sensor: 32}

responses:
200:
description: PROCESSED
202:
description: ACCEPTED (Scheduling job queued for processing)
content:
application/json:
schema:
type: object
examples:
successful_response:
description: |
This message indicates that the scheduling request has been processed without any error.
This message indicates that the scheduling request has been accepted for processing (202 Accepted).
A scheduling job has been created with some Universally Unique Identifier (UUID),
which will be picked up by a worker.
The given UUID may be used to obtain the resulting schedule for each flexible device: see [/sensors/schedules/](#/Sensors/get_api_v3_0_sensors__id__schedules__uuid_).
The given UUID is returned in the canonical `job_id` field.
For backward-compatibility, the legacy `schedule` field is also included but is deprecated;
use `job_id` instead. The `_deprecated_fields` object provides migration guidance.
The given UUID may be used to obtain the resulting schedule for each flexible device via [/sensors/schedules/](#/Sensors/get_api_v3_0_sensors__id__schedules__uuid_).
value:
status: PROCESSED
status: ACCEPTED
job_id: "364bfd06-c1fa-430b-8d25-8f5a547651fb"
schedule: "364bfd06-c1fa-430b-8d25-8f5a547651fb"
message: "Request has been processed."
job_monitor_url: "/api/v3_0/jobs/364bfd06-c1fa-430b-8d25-8f5a547651fb"
message: "Request has been accepted for processing."
_deprecated_fields:
schedule:
use: "job_id"
deprecated_since: "1.0.0"
removal_date: "2.0.0"
note: "The 'schedule' response field is deprecated; use 'job_id'"
400:
description: INVALID_DATA
401:
Expand Down Expand Up @@ -1536,9 +1547,9 @@ def trigger_schedule(
except ValueError as err:
return unprocessable_entity(str(err))

response = dict(schedule=job.id)
d, s = request_processed()
return dict(**response, **d), s
# Keep legacy `schedule` key for backward compatibility; prefer `job_id`.
d, s = request_accepted_for_processing(job.id, legacy_key="schedule")
return d, s

@route("/<id>/kpis", methods=["GET"])
@use_kwargs(
Expand Down
Loading
Loading