Skip to main content

Balancing Services Python Client

Python client library for the Balancing Services REST API. Access European electricity balancing market data with a modern, type-safe Python interface.

Features

  • Type-safe: Full type hints for better IDE support and code quality
  • Modern Python: Built with httpx and attrs for async support and clean data models
  • Auto-generated: Generated from the official OpenAPI specification
  • Comprehensive: Access to all API endpoints and data models
  • Well-documented: Inline documentation and usage examples

Installation

uv pip install balancing-services

Or with pip:

pip install balancing-services

For development, install from source:

cd clients/python
uv pip install -e .

Quick Start

from datetime import datetime

from balancing_services import AuthenticatedClient
from balancing_services.api.default import get_imbalance_prices

# Create an authenticated client
client = AuthenticatedClient(
    base_url="https://api.balancing.services/v2",
    token="YOUR_API_TOKEN"
)

# Get imbalance prices for Estonia. Enum-typed parameters take plain string
# values (e.g. area="EE") — see "Data Models" below.
response = get_imbalance_prices.sync_detailed(
    client=client,
    area="EE",
    period_start_at=datetime.fromisoformat("2025-01-01T00:00:00Z"),
    period_end_at=datetime.fromisoformat("2025-01-02T00:00:00Z")
)

if response.status_code == 200:
    data = response.parsed
    print(f"Query period: {data.queried_period.start_at} to {data.queried_period.end_at}")
    for imbalance_prices in data.data:
        print(f"Area: {imbalance_prices.area}, Direction: {imbalance_prices.direction}")
        for price in imbalance_prices.prices:
            print(f"  {price.period.start_at}: {price.price_per_mwh} {imbalance_prices.currency}")

Authentication

To obtain an API token:

Include your token when creating the client:

from balancing_services import AuthenticatedClient

client = AuthenticatedClient(
    base_url="https://api.balancing.services/v2",
    token="YOUR_API_TOKEN"
)

Usage Examples

Get Balancing Energy Bids with Pagination

from datetime import datetime

from balancing_services import AuthenticatedClient
from balancing_services.api.default import get_balancing_energy_bids

client = AuthenticatedClient(base_url="https://api.balancing.services/v2", token="YOUR_TOKEN")

# First page
response = get_balancing_energy_bids.sync_detailed(
    client=client,
    area="EE",
    period_start_at=datetime.fromisoformat("2025-01-01T00:00:00Z"),
    period_end_at=datetime.fromisoformat("2025-01-02T00:00:00Z"),
    reserve_type="aFRR",
    limit=100
)

if response.status_code == 200:
    data = response.parsed
    print(f"Has more: {data.has_more}")

    # Process first page. Each group carries one entry per delivery period.
    for bid_group in data.data:
        for period in bid_group.periods:
            for bid in period.bids:
                print(f"Bid: {bid.volume_in_mw} MW @ {bid.price_per_mwh} {bid_group.currency}")

    # Get next page if available
    if data.has_more and data.next_cursor:
        next_response = get_balancing_energy_bids.sync_detailed(
            client=client,
            area="EE",
            period_start_at=datetime.fromisoformat("2025-01-01T00:00:00Z"),
            period_end_at=datetime.fromisoformat("2025-01-02T00:00:00Z"),
            reserve_type="aFRR",
            cursor=data.next_cursor,
            limit=100
        )

Async Usage

import asyncio
from datetime import datetime

from balancing_services import AuthenticatedClient
from balancing_services.api.default import get_imbalance_prices

async def fetch_prices():
    client = AuthenticatedClient(
        base_url="https://api.balancing.services/v2",
        token="YOUR_TOKEN"
    )

    response = await get_imbalance_prices.asyncio_detailed(
        client=client,
        area="EE",
        period_start_at=datetime.fromisoformat("2025-01-01T00:00:00Z"),
        period_end_at=datetime.fromisoformat("2025-01-02T00:00:00Z")
    )

    if response.status_code == 200:
        return response.parsed
    return None

# Run async function
prices = asyncio.run(fetch_prices())

Error Handling

from datetime import datetime

from balancing_services import AuthenticatedClient
from balancing_services.api.default import get_imbalance_prices

client = AuthenticatedClient(base_url="https://api.balancing.services/v2", token="YOUR_TOKEN")

response = get_imbalance_prices.sync_detailed(
    client=client,
    area="EE",
    period_start_at=datetime.fromisoformat("2025-01-01T00:00:00Z"),
    period_end_at=datetime.fromisoformat("2025-01-02T00:00:00Z")
)

if response.status_code == 200:
    data = response.parsed
    # Process successful response
elif response.status_code == 400:
    error = response.parsed
    print(f"Bad request: {error.detail}")
elif response.status_code == 401:
    print("Authentication failed - check your API token")
elif response.status_code == 429:
    print("Rate limited - please retry later")
else:
    print(f"Error {response.status_code}: {response.content}")

Data Models

All response and request models are fully typed using attrs. Key models include:

  • ImbalancePricesResponse, ImbalanceTotalVolumesResponse
  • BalancingEnergyVolumesResponse, BalancingEnergyPricesResponse, BalancingEnergyBidsResponse
  • BalancingCapacityBidsResponse, BalancingCapacityPricesResponse, BalancingCapacityVolumesResponse
  • Enum types: Area, ReserveType, Direction, Currency, ActivationType, BidStatus

Enum-typed fields and parameters are plain strings. Each enum is a Literal string type (e.g. Area = Literal["EE", "FI", ...]) with a companion *_VALUES set of accepted values — there is no Area.EE member access or .value accessor. Pass the bare wire string (area="EE", reserve_type="aFRR") and read fields back as strings.

Development

Regenerating the Client

The client is generated from the OpenAPI specification. To regenerate:

cd clients/python
./generate.sh

Running Tests

# Install development dependencies
uv pip install -e ".[dev]"

# Run tests
pytest

# Run tests with coverage
pytest --cov=balancing_services --cov-report=html

Code Quality

# Format code
ruff format .

# Lint code
ruff check .

# Type checking
mypy balancing_services

Automation Scripts

The Python client includes automation scripts for development and publishing:

generate-pyproject.sh

Generates pyproject.toml from the template with the current version from openapi.yaml.

Note: The version is stored in openapi.yaml (single source of truth). The pyproject.toml.draft template contains invalid TOML (version = __VERSION__ without quotes) to prevent accidental use without proper generation.

./generate-pyproject.sh

This script is automatically called by check.sh and test-and-publish.sh.

check.sh

Runs all quality checks in one command:

./check.sh

Performs:

  • Generates pyproject.toml from draft
  • Runs tests with pytest
  • Runs linting with ruff
  • Runs type checking with mypy
  • Verifies the package builds

test-and-publish.sh

Complete publishing workflow with TestPyPI validation:

Setup:

  1. Copy .env.sample to .env and fill in your credentials:

    cp .env.sample .env
    # Edit .env and add:
    #   BALANCING_SERVICES_API_KEY (required - for testing against live API)
    #   UV_PUBLISH_TOKEN_TESTPYPI (required)
    #   UV_PUBLISH_TOKEN_PYPI (required)
    
  2. Run the publish script:

    ./test-and-publish.sh
    

Emergency option - skip API tests if the server is down:

./test-and-publish.sh --skip-api-tests

⚠️ This skips testing against the live API. Use only in emergencies!

Alternatively, export variables manually:

export BALANCING_SERVICES_API_KEY="your-api-key"
export UV_PUBLISH_TOKEN_TESTPYPI="your-testpypi-token"
export UV_PUBLISH_TOKEN_PYPI="your-pypi-token"
./test-and-publish.sh

What it does:

  1. Loads credentials from .env (if present)
  2. Verifies all required environment variables are set
  3. Runs quality checks
  4. Publishes to TestPyPI using UV_PUBLISH_TOKEN_TESTPYPI
  5. Creates isolated test sandbox
  6. Installs package from TestPyPI
  7. Runs all example scripts against live API using BALANCING_SERVICES_API_KEY
  8. Prompts to publish to PyPI using UV_PUBLISH_TOKEN_PYPI if tests pass
  9. Verifies production PyPI package by installing and running examples again

Get your credentials:

For maintainers: See ../../scripts/README.md for the complete release workflow.

Troubleshooting

Authentication Errors (401)

Problem: Receiving 401 Unauthorized responses

Solutions:

  • Verify your API token is correct
  • Check that the token is being passed to AuthenticatedClient
  • Ensure your token hasn't expired (contact support if needed)

Bad Request Errors (400)

Problem: Receiving 400 Bad Request responses

Common causes:

  • Invalid date range (end date before start date)
  • Date range too large
  • Invalid area code or reserve type
  • Malformed datetime strings

Solution: Check the error detail in the response for specific information:

if response.status_code == 400:
    error = response.parsed
    print(f"Error detail: {error.detail}")

Empty Results

Problem: Receiving 200 OK but no data

Possible reasons:

  • No data available for the requested period
  • Data not yet published for recent periods
  • Requesting data for a period before data collection started

Solution: Try a different time period or check if data exists for your area

Timeout Issues

Problem: Requests timing out

Solutions:

  • Increase the client timeout:
client = AuthenticatedClient(
    base_url="https://api.balancing.services/v2",
    token="YOUR_TOKEN",
    timeout=30.0  # Increase from default
)
  • Reduce the date range in your request
  • Use pagination for large datasets

Type Errors with Enums

Problem: Type errors when passing area or reserve type

Solution: Pass the bare wire string value. Enum-typed parameters are Literal string types, so the string itself is the type-safe value — there is no enum member to import:

# Correct
area="EE"
reserve_type="aFRR"

The accepted values for each enum are available as a *_VALUES set if you need to validate or enumerate them:

from balancing_services.models.area import AREA_VALUES

assert "EE" in AREA_VALUES

Documentation

Support

License

MIT License - see LICENSE for details.

Changelog

See CHANGELOG.md for version history and changes.

Metadata

Release files for balancing-services 2.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for balancing-services 2.2.0
File Size Uploaded
balancing_services-2.2.0.tar.gz 54.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for balancing-services 2.2.0
File Interpreter ABI Platform
balancing_services-2.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 174.2 kB

Release files / balancing_services-2.2.0.tar.gz

Download URL balancing_services-2.2.0.tar.gz
Size 54.1 kB
Tags Source
SHA-256 checksum
How to use checksums
6c92bdec6c09553f3bc0f5f5cda4d03c6f56a98f5c701d5893b5111a94ca46f3
BLAKE2b-256 checksum
How to use checksums
9050eb9830c00432c59e7fa0c0e47baa79ed3053ea09e9f132764d00d39f5c71
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.0 {"installer":{"name":"uv","version":"0.12.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"openSUSE Tumbleweed","version":"20260731","id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / balancing_services-2.2.0-py3-none-any.whl

Download URL balancing_services-2.2.0-py3-none-any.whl
Size 120.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
9ec8b7bdfc8d0b43d773bee7ae1c12adc9756c9e3a066c047d485a86abeef2f1
BLAKE2b-256 checksum
How to use checksums
a8e2c0d09d385248f89658dd8ad4973a0c1787d97e90e1fcfa5e699d5c0a5c3a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.0 {"installer":{"name":"uv","version":"0.12.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"openSUSE Tumbleweed","version":"20260731","id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release history Release notifications | RSS feed

2.6.0

2 release files

2.5.0

2 release files

2.4.0

2 release files

2.3.0

2 release files

This release

2.2.0 This release

2 release files

2.1.0

2 release files

2.0.2

2 release files

1.20.0

2 release files

1.19.0

2 release files

1.16.0

2 release files

1.15.0

2 release files

1.13.0

2 release files

1.12.0

2 release files

1.11.0

2 release files

1.10.0

2 release files

1.9.0

2 release files

1.8.0

2 release files

1.7.1

2 release files

1.7.0

2 release files

1.6.0

2 release files

1.5.1

2 release files

1.5.0

2 release files

1.4.0

2 release files

1.3.0

2 release files

1.2.0

2 release files

1.1.1

2 release files

1.1.0

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page