Skip to main content
API v1 · Stable

EnergyCode Enterprise API

Query GB Elec and gas network charge data programmatically. DUoS, TNUoS, BSUoS, policy levies, Gas LDZ transportation, Line Loss Factors and MDD reference data. Every rate returned raw with provenance and source document.

your first request
curl "https://www.energycode.ai/api/v1/reference/dnos" \
  -H "X-Api-Key: ec_live_..."

What is the EnergyCode API?

The EnergyCode API provides access to UK Elec and Gas network charges data, including:

  • DUoS (Distribution Use of System) - charges from all 14 DNO regions
  • TNUoS (Transmission Use of System) - NESO transmission charges
  • BSUoS (Balancing Services Use of System) - balancing costs
  • Capacity Market - capacity market levy
  • CfD (Contracts for Difference) - low-carbon generation levy (funds renewables and nuclear)
  • CCL (Climate Change Levy) - environmental tax rates (non-domestic supply only)
  • AAHEDC (Assistance for Areas with High Electricity Distribution Costs)
  • Nuclear RAB (Regulated Asset Base) levy - live from December 2025
  • Gas LDZ transportation - gas distribution Use of System charges (the gas analogue to DUoS), the five canonical UNC-governed components across the GB LDZs
  • LLF (Line Loss Factors) - seasonal network loss factors from Elexon ISD
  • Reference data - DNO regions, LLFC mappings, and MDD meter reference (MTCs, SSCs, TPRs, D0018 profile coefficients)

All data is validated against published charging statements and updated for each charging year.

Quick Start (5 Minutes)

1. Get Your API Key

Contact your account manager or email [email protected] to receive your API key. You'll receive a key in the format:

text
ec_live_a1b2c3d4e5f6...

For testing, you may also receive a test key prefixed with ec_test_.

2. Make Your First Request

List all DNO regions:

bash
curl -X GET "https://www.energycode.ai/api/v1/reference/dnos" \
  -H "X-Api-Key: YOUR_API_KEY_HERE"

Expected Response:

json
{
  "success": true,
  "data": [
    {
      "id": 10,
      "name": "Eastern Power Networks",
      "operator": "UKPN"
    },
    {
      "id": 11,
      "name": "East Midlands",
      "operator": "NGED"
    }
    // ... all 14 DNO regions
  ]
}

3. Fetch DUoS Rates

Now retrieve DUoS rates for a specific DNO:

bash
curl -X GET "https://www.energycode.ai/api/v1/rates/duos?dno_id=10&charging_year=2025-26" \
  -H "X-Api-Key: YOUR_API_KEY_HERE"

Expected Response:

json
{
  "success": true,
  "data": {
    "dno": {
      "id": 10,
      "name": "Eastern Power Networks"
    },
    "charging_year": "2025-26",
    "rates": [
      {
        "tariff_category": "DOM_AGG",
        "red_rate_pkwh": 12.34,
        "amber_rate_pkwh": 5.67,
        "green_rate_pkwh": 1.23,
        "fixed_rate_pmpan_day": 45.67,
        "capacity_rate_pkva_day": null,
        "exceeded_capacity_rate_pkva_day": null,
        "reactive_rate_pkvarh": null
      }
      // ... more tariff categories
    ]
  }
}

You've made your first API calls.

MPANs are strings. Keep the leading zeros.

An MPAN Core begins with 0 (e.g. 008450831071094911008). Spreadsheets and JSON number types silently strip leading zeros, turning a valid 21-digit MPAN into a 19- or 20-digit value the API rejects with 400 UNEXPECTED_LENGTH, whose message names dropped leading zeros as the likely cause. A 6-10 digit value (a gas MPRN pasted by mistake) is rejected with 400 LIKELY_MPRN. Always send mpan as a quoted string, zero-padded to 21 (legacy) or 22 (MHHS) digits.

Authentication

All API requests require an API key passed via the X-Api-Key header.

How to Authenticate

Include your API key in every request:

bash
-H "X-Api-Key: ec_live_abc123..."

API Key Types

Live

Format
ec_live_*
Purpose
Production use with real data

Test

Format
ec_test_*
Purpose
Testing and development (same data, separate quota)

Plan Scopes

Your plan may restrict your API key to specific datasets. A key with no scopes set has full access to every endpoint. When your key is scoped, only the matching datasets are reachable:

duos

Datasets
Red, amber and green unit rates, fixed, capacity, exceeded-capacity and reactive charges, Line Loss Factors, DNO regions, LLFC mappings

tnuos

Datasets
Locational (zonal) demand tariffs, TDR fixed-band charges, embedded export credits

balancing

Datasets
BSUoS fixed six-monthly tariffs

policy

Datasets
CCL, CfD, Capacity Market, AAHEDC, Nuclear RAB rates

gas

Datasets
Gas LDZ transportation rate card by MPRN (ZCA, ZCO, CCA, ECN, CFI, SOLR)

metering

Datasets
SSC, TPR and MTC meter-configuration reference, D0018 profile coefficients
  • /usage is always accessible, regardless of scopes.
  • A request to a dataset outside your key's scopes returns 403 with error code SCOPE_DENIED. Email [email protected] to upgrade your plan.

Security Best Practices

  1. Never expose API keys in client-side code (JavaScript, mobile apps)
  2. Use environment variables to store keys securely
  3. Rotate keys periodically (contact support to generate new keys)
  4. Use test keys for development - keep live keys for production only
  5. Monitor usage - contact support if you suspect unauthorized use

Rate Limits and Quotas

Usage is governed by three limits, agreed individually as part of your contract:

  1. Per-minute rate limit - short-burst throttle (requests per minute)
  2. Monthly call quota - billable calls consumed per calendar month
  3. Daily reference-row quota - rows returned per day from the reference endpoints (/reference/*), which protects the bulk MDD and mapping datasets from wholesale extraction

Your specific allowances are agreed individually as part of your contract. Your current allowance and remaining balance are returned in the response headers on every call.

Billable Calls

A call is the billable unit of quota: every successful request costs one call, whatever the endpoint and however much data it returns. Failed requests (4xx/5xx) and the free /usage meta call cost nothing. Each response reports the cost of that call in the X-Request-Cost header.

ResponseCost
Any successful (2xx) data request1 call
/usage0
Any error (4xx / 5xx)0

Reference endpoints (/reference/*) also count the rows they return against a separate daily reference-row quota, on top of the one-call cost.

Billing Mode

Every account is either capped or uncapped. A capped account stops with 429 QUOTA_EXCEEDED once the monthly call allowance is reached; an uncapped account keeps serving and meters calls beyond the allowance as overage, billed per your contract. The active mode is returned on every response in X-Quota-Mode, with any overage in X-Overage-Calls.

Response Headers

Every successful response carries the per-minute rate limit state, the monthly call quota state, and the cost of the call:

http
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 73
X-RateLimit-Reset: 1738348800
X-Request-Cost: 8
X-Quota-Limit: 15000
X-Quota-Remaining: 13760
X-Quota-Mode: capped
HeaderDescription
X-RateLimit-LimitMaximum requests per minute
X-RateLimit-RemainingRemaining requests in the current per-minute window
X-RateLimit-ResetUnix timestamp when the per-minute window resets
X-Request-CostBillable calls this request consumed (1 for a successful call, 0 otherwise)
X-Quota-LimitMonthly call allowance for your account
X-Quota-RemainingCalls remaining in the current calendar month
X-Quota-ModeBilling mode for your account: capped or uncapped
X-Overage-CallsCalls beyond the allowance this month (uncapped accounts only)

Handling 429 Responses

When you exceed your per-minute rate limit, the API returns 429 Too Many Requests with code RATE_LIMIT_EXCEEDED:

json
{
  "success": false,
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Too many requests. Please try again later."
  }
}

When a capped account reaches its monthly call quota or its daily reference-row quota, the API returns 429 with code QUOTA_EXCEEDED:

json
{
  "success": false,
  "error": {
    "code": "QUOTA_EXCEEDED",
    "message": "Monthly call allowance reached for your account."
  }
}

Response Headers on 429:

http
Retry-After: 42

The Retry-After header tells you how many seconds to wait before retrying. Check your own current consumption and remaining quota at any time via GET /usage.

Best Practices for Avoiding Rate Limits

  1. Implement exponential backoff - wait longer between retries
  2. Cache responses - store frequently accessed data locally
  3. Batch requests - fetch data in bulk rather than making many small requests
  4. Monitor X-RateLimit-Remaining - proactively slow down before hitting the limit
  5. Contact support if you need higher limits

Examples

1. DUoS rates for a DNO region

All DUoS tariff categories and rates for a DNO region and charging year - Eastern Power Networks (DNO ID 10), 2025-26:

bash
curl -X GET "https://www.energycode.ai/api/v1/rates/duos?dno_id=10&charging_year=2025-26" \
  -H "X-Api-Key: YOUR_API_KEY_HERE"

Filter by tariff category:

bash
curl -X GET "https://www.energycode.ai/api/v1/rates/duos?dno_id=10&charging_year=2025-26&tariff_category=ND_AGG_B1" \
  -H "X-Api-Key: YOUR_API_KEY_HERE"

2. TNUoS rates

bash
curl -X GET "https://www.energycode.ai/api/v1/rates/tnuos?charging_year=2025-26" \
  -H "X-Api-Key: YOUR_API_KEY_HERE"

3. CCL rates

bash
curl -X GET "https://www.energycode.ai/api/v1/rates/ccl?charging_year=2025-26" \
  -H "X-Api-Key: YOUR_API_KEY_HERE"

Endpoint Reference

All endpoints are served from https://www.energycode.ai/api/v1 and require the X-Api-Keyheader. Query parameters shown as "optional" can be omitted; reference and rate list endpoints support limit and offset for pagination.

Rates

MethodEndpointDescriptionKey params
GET/rates/duosDUoS rates for a DNO regiondno_id (required), charging_year, tariff_category, llfc_code
GET/rates/tnuosTNUoS zonal and TDR ratescharging_year, zone_id, gsp_group, rate_type, voltage, band
GET/rates/bsuosBSUoS ratescharging_year, period, date
GET/rates/cclClimate Change Levy ratescharging_year
GET/rates/cfdCfD Levy ratescharging_year, quarter, date
GET/rates/capacity-marketCapacity Market published auction clearing prices per delivery yeardelivery_year
GET/rates/aahedcAAHEDC rates (p/kWh and £/MWh)charging_year, date
GET/rates/nuclear-rabNuclear RAB Levy rates (live from Dec 2025)charging_year, date
GET/rates/llfLine Loss Factors from Elexon ISDdno_id (required), charging_year (required), llfc_code
GET/rates/by-mpanFull applicable non-commodity rate card for a single MPAN, demand or generationmpan (required), charging_year, override_distributor_id, postcode (for IDNO MPANs), charges
GET/rates/by-mprnFull Gas LDZ transportation rate card for a single MPRNmprn (required), postcode (required), charging_year, charges

Reference Data

MethodEndpointDescriptionKey params
GET/reference/dnosList all 14 DNO regionsnone
GET/reference/llfcsLLFC-to-tariff-category mappings for a DNOdno_id (required), llfc_code, market_segment, is_import
GET/reference/mtcsMeter Timeswitch Classes (MDD)mtc_id, mtc_type, is_common_code
GET/reference/sscsStandard Settlement Configurations (MDD)ssc_code, ssc_type, is_active, include (tprs, clock_intervals)
GET/reference/tprsTime Pattern Regimes (MDD)tpr_id, tpr_type, include (clock_intervals)
GET/reference/profile-coefficientsD0018 profile coefficients (NHH, PC 01-04)profile_class (required), gsp_group (required), date (required), period_index

Account

MethodEndpointDescriptionKey params
GET/usageYour own usage and current quota stateperiod (day/week/month/all), start_date, end_date

Looking Up DUoS Rates by LLFC

The 3-character LLFC code from an MPAN can be resolved to its tariff category(ies) in a single call. Pass llfc_code to /rates/duos and the matching rate tables come back directly, no separate mapping lookup needed. llfc_code takes precedence over tariff_category.

bash
curl -X GET "https://www.energycode.ai/api/v1/rates/duos?dno_id=10&charging_year=2025-26&llfc_code=001" \
  -H "X-Api-Key: YOUR_API_KEY_HERE"

MPANs are strings. Keep the leading zeros.

An MPAN Core begins with 0 (e.g. 008450831071094911008). Spreadsheets and JSON number types silently strip leading zeros, turning a valid 21-digit MPAN into a 19- or 20-digit value the API rejects with 400 UNEXPECTED_LENGTH, whose message names dropped leading zeros as the likely cause. A 6-10 digit value (a gas MPRN pasted by mistake) is rejected with 400 LIKELY_MPRN. Always send mpan as a quoted string, zero-padded to 21 (legacy) or 22 (MHHS) digits. The API does not auto-pad, substitute or truncate, because it cannot know how the value was altered. It does forgive presentation noise: surrounding or internal spaces, hyphens, zero-width characters and full-width digits are normalized away before validation, so 0084 5083 1071 0949 11008 and 0084-5083-1071-0949-11008 are both accepted.

Rate Card by MPAN

Send a single MPAN to /rates/by-mpan and it returns the full applicable non-commodity rate card for that supply point - raw published rates only.

Charges do not share a calendar, so data.requested_charging_year is an echo of your request used to select which rows to return, never the period of a returned rate. Each block carries its own basis (a plain-words period, e.g. "charging year (Apr–Mar)"), and each rate line carries its own period, a provenance label and a source reference. provenance is official_actual for a settled or final published figure, or official_forecast for an official forward figure (a NESO Draft/5YV tariff, an LCCC set-ahead Interim Levy Rate, a future Capacity Market auction result). Both are official data from the responsible body; a forecast is given and labelled as such. Our own derived estimates are never returned as a rate: where the only figure held for a year is an estimate, the block returns rates: null, provenance: null and an explanatory note, with published_through marking the coverage boundary.

For an ordinary DNO MPAN (distributor id 10-23) you send nothing but the MPAN. The endpoint decodes the distributor id, resolves the tariff category from the LLFC and returns the rate card directly:

bash
curl -X GET "https://www.energycode.ai/api/v1/rates/by-mpan?mpan=008450831071094911008&charging_year=2025-26" \
  -H "X-Api-Key: YOUR_API_KEY_HERE"

The response bundles every non-commodity charge that applies to the meter in one payload:

json
{
  "success": true,
  "data": {
    "mpan": {
      "distributor_id": 10,
      "dno_name": "Eastern Power Networks",
      "profile_class": "00",
      "gsp_group": "_A"
    },
    "requested_charging_year": "2026-27",
    "resolved_host_distributor_id": 10,
    "host_dno_resolution": "native",
    "charges_available": ["cdcm", "tnuos", "bsuos", "ccl", "cfd", "capacity_market", "aahedc", "nuclear_rab"],
    "charges_returned": ["cdcm", "tnuos", "bsuos", "ccl", "cfd", "capacity_market", "aahedc", "nuclear_rab"],
    "charges_requested": null,
    "cdcm": {
      "charging_year": "2026-27",
      "tariff_category": "LVS_SS_B3",
      "dno_name": "Eastern Power Networks",
      "basis": "charging year (Apr–Mar)",
      "provenance": "official_actual",
      "source_document": "EPN DUoS Charging Statement 2026-27",
      "tariff_category_source": "llfc",
      "published_through": "2026-27",
      "rates": {
        "red_rate_pkwh": 12.34,
        "amber_rate_pkwh": 5.67,
        "green_rate_pkwh": 1.23,
        "fixed_rate_pmpan_day": 45.67,
        "capacity_rate_pkva_day": 6.89,
        "exceeded_capacity_rate_pkva_day": 13.78,
        "reactive_rate_pkvarh": 0.42
      }
    },
    "tnuos": {
      "charging_year": "2026-27",
      "basis": "TNUoS charging year (Apr–Mar)",
      "locational": {
        "hh_tariff_gbp_kw": 30.12,
        "provenance": "official_actual",
        "source_document": "NESO TNUoS Tariffs 2026-27 (Final)"
      },
      "tdr_bands": [
        { "band_code": "LV2", "rate_gbp_day": 1.23, "provenance": "official_actual" }
      ]
    },
    "bsuos": {
      "requested_charging_year": "2026-27",
      "basis": "BSUoS fixed 6-month period (date-windowed; no charging year)",
      "published_through": "2027-03-31",
      "periods": [
        {
          "period_start": "2026-04-01",
          "period_end": "2026-09-30",
          "rate_pkwh": 0.412,
          "rate_type": "fixed",
          "provenance": "official_forecast"
        }
      ]
    },
    "policy": {
      "ccl": {
        "charging_year": "2026-27",
        "basis": "charging year (Apr–Mar)",
        "rate_pkwh": 0.775,
        "provenance": "official_actual",
        "source": "HMRC Climate Change Levy rates"
      },
      "cfd": {
        "requested_charging_year": "2026-27",
        "basis": "CfD calendar quarter (LCCC Interim Levy Rate, set-ahead)",
        "published_through": "2026-12-31",
        "quarters": [
          {
            "delivery_year": "2026-27",
            "quarter": "Q2",
            "period_start": "2026-04-01",
            "period_end": "2026-06-30",
            "interim_levy_rate_gbp_mwh": 10.26,
            "provenance": "official_forecast",
            "source": "LCCC CfD Interim Levy Rate"
          }
        ]
      },
      "capacity_market": {
        "requested_charging_year": "2026-27",
        "basis": "Capacity Market delivery year (Oct–Sep), published auction clearing prices",
        "published_through": "2028-09-30",
        "delivery_years": [
          {
            "delivery_year": "2026-27",
            "period_start": "2026-10-01",
            "period_end": "2027-09-30",
            "t4_auction_price_gbp_kw": 63.0,
            "t3_auction_price_gbp_kw": null,
            "t1_auction_price_gbp_kw": null,
            "settlement_costs_levy_gbp_mwh": 0.05,
            "provenance": "official_forecast",
            "source": "NESO EMR Capacity Market auction results"
          }
        ]
      },
      "aahedc": { "...": "..." },
      "nuclear_rab": { "...": "..." }
    }
  }
}
  • cdcm.rates carries the DUoS unit rates (red / amber / green), the fixed charge, and the capacity, exceeded-capacity and reactive rates - the per-kVA and per-kVArh figures, not just totals.
  • tnuos is the transmission network charge and bsuosis NESO's balancing charge, returned as separate top-level blocks; policy holds CCL, CfD, Capacity Market, AAHEDC and Nuclear RAB. Together they are the complete non-commodity stack for the supply point.
  • policy.capacity_market returns published auction clearing prices (£/kW/year) per delivery year (Oct–Sep). A charging-year window spans two delivery years, so both are returned. policy.cfd returns one entry per calendar quarter, each carrying its own delivery_year and period.
  • host_dno_resolution is nativefor an ordinary DNO MPAN - the MPAN's own distributor id was used, so no postcode is needed.
  • An EHV (EDCM) site returns an edcm block of site-specific import/export rates (with a direction indicator) in place of cdcm.

One MPAN in, the whole rate card out

/rates/by-mpan is a pure rates lookup: it returns published rates and never touches consumption. The only case where an MPAN alone is not enough is an IDNO MPAN (distributor id 24+), which needs a postcode - see below.

Selecting individual charges

The full card is the default. Add the charges query parameter with a comma-separated list to return only the blocks you name. The endpoint runs the same decode and resolution either way, and each returned block is identical to its full-card form.

  • distribution (or duos) returns the DUoS block for the meter: cdcm, or edcm for an EHV site.
  • transmission (or tnuos) returns TNUoS. BSUoS is separate: request it with bsuos.
  • policy returns all five policy charges, or name one: ccl, cfd, capacity_market, aahedc, nuclear_rab.

Every response carries three lists:

  • charges_available - every charge that applies to this MPAN.
  • charges_returned - the charges in this response.
  • charges_requested - the charges you asked for, or null for the full card.
bash
curl -X GET "https://www.energycode.ai/api/v1/rates/by-mpan?mpan=008450831071094911008&charging_year=2025-26&charges=duos,cfd" \
  -H "X-Api-Key: YOUR_API_KEY_HERE"
json
{
  "success": true,
  "data": {
    "mpan": { "distributor_id": 10, "dno_name": "Eastern Power Networks", "profile_class": "00", "gsp_group": "_A" },
    "requested_charging_year": "2026-27",
    "resolved_host_distributor_id": 10,
    "host_dno_resolution": "native",
    "charges_available": ["cdcm", "tnuos", "bsuos", "ccl", "cfd", "capacity_market", "aahedc", "nuclear_rab"],
    "charges_returned": ["cdcm", "cfd"],
    "charges_requested": ["cdcm", "cfd"],
    "cdcm": { "...": "..." },
    "policy": {
      "cfd": { "...": "..." }
    }
  }
}

An unknown token, an empty value, or more than 50 tokens returns 400 VALIDATION_ERROR with the accepted list. The MPAN and generation checks run first, so a generation MPAN or a gas MPRN returns its own error whatever you pass in charges.

Generation and export MPANs

Generation and export is auto-detected from the LLFC (an export LLFC on a Profile Class 00 supply point). You send nothing extra: the response carries a connection_type of generation and returns the charges that apply to an exporter.

bash
curl -X GET "https://www.energycode.ai/api/v1/rates/by-mpan?mpan=008457911000107350005&charging_year=2026-27" \
  -H "X-Api-Key: YOUR_API_KEY_HERE"

Generation rates are shown signed: a credit is negative (a payment to the generator), a charge positive.

json
{
  "success": true,
  "data": {
    "mpan": {
      "mpan": "008457911000107350005",
      "formatted": "00 845 791 10 00107350 005",
      "format": "legacy",
      "distributor_id": 10,
      "dno_name": "Eastern Power Networks",
      "llfc_code": "791",
      "profile_class": "00",
      "gsp_group": "_A"
    },
    "connection_type": "generation",
    "requested_charging_year": "2026-27",
    "resolved_host_distributor_id": 10,
    "host_dno_resolution": "native",
    "charges_available": ["cdcm", "tnuos"],
    "charges_returned": ["cdcm", "tnuos"],
    "charges_requested": null,
    "cdcm": {
      "charging_year": "2026-27",
      "tariff_category": "HV_GEN_SS",
      "tariff_name": "HV Generation Site Specific",
      "dno_name": "Eastern Power Networks",
      "basis": "charging year (Apr–Mar)",
      "provenance": "official_actual",
      "source_document": "EPN DUoS Charging Statement 2026-27",
      "tariff_category_source": "llfc",
      "published_through": "2026-27",
      "rates": {
        "red_rate_pkwh": -5.466,
        "amber_rate_pkwh": -0.559,
        "green_rate_pkwh": -0.052,
        "fixed_rate_pmpan_day": 12.88,
        "capacity_rate_pkva_day": 0.0,
        "exceeded_capacity_rate_pkva_day": 0.0,
        "reactive_rate_pkvarh": 0.277
      }
    },
    "tnuos": {
      "charging_year": "2026-27",
      "basis": "TNUoS charging year (Apr–Mar)",
      "wider_generation": {
        "basis": "Wider generation TNUoS (£/kW), published per NESO generation zone. All published zones are returned raw and signed (southern zones are typically NEGATIVE = a credit; components may be zero). The applicable zone cannot be resolved from the MPAN — the caller applies its generator classification and NESO generation zone to select the row. Each row carries its own provenance from its NESO publication (Final = official_actual; Draft/Forecast = official_forecast).",
        "zone_count": 27,
        "zones": [
          {
            "zone_id": 1,
            "system_peak_gbp_kw": 3.628060,
            "shared_year_round_gbp_kw": 26.532717,
            "not_shared_year_round_gbp_kw": 17.674559,
            "residual_gbp_kw": -2.476760,
            "small_gen_discount_gbp_kw": 0.0,
            "effective_from": "2026-04-01",
            "effective_to": "2027-03-31",
            "provenance": "official_actual",
            "source_document": "NESO TNUoS Tariffs 2026-27 (Final)"
          }
          // ... all 27 generation zones
        ]
      },
      "eet_credit": {
        "basis": "Eligibility for Embedded Export Tariff (EET) credit, resolved from the site DEMAND zone (14-zone table). Published as a positive £/kW; shown here as credit_gbp_kw (negative to the customer, following the published sign convention). A published zero is a real published zero.",
        "demand_zone_id": 9,
        "demand_zone_name": "Eastern",
        "gsp_group": "_A",
        "published_tariff_gbp_kw": 3.206484,
        "credit_gbp_kw": -3.206484,
        "effective_from": "2026-04-01",
        "effective_to": "2027-03-31",
        "source_document": "NESO TNUoS Tariffs 2026-27 (Final)",
        "provenance": "official_actual"
      }
    },
    "bsuos": {
      "applies": false,
      "rate": null,
      "basis": "Borne by demand only; generators exempt since 1 April 2023 (CMP308)."
    },
    "policy": {
      "ccl": { "applies": false, "rate": null, "basis": "Charged on energy consumed at non-domestic premises (Finance Act 2000 Sch 6); none on export." },
      "cfd": { "applies": false, "rate": null, "basis": "Per-MWh obligation on suppliers' supply volume (CfD ESO Regs 2014); none on generation/export." },
      "capacity_market": { "applies": false, "rate": null, "basis": "Supplier demand charge in peak windows (Electricity Capacity Regs 2014); none on generation/export." },
      "aahedc": { "applies": false, "rate": null, "basis": "No AAHEDC applies to generation/export — NESO levies AAHEDC on supplier demand only, with no published generation rate." },
      "nuclear_rab": { "applies": false, "rate": null, "basis": "Per-MWh levy on electricity suppliers (Nuclear Energy (Financing) Act 2022); none on generation/export." },
      "ro": { "applies": false, "rate": null, "basis": "Supplier per-MWh obligation (RO Order 2015); none on export. (Generator earns ROCs — scheme revenue, not a network rate.)" },
      "fit": { "applies": false, "rate": null, "basis": "Levy recovered from suppliers (Energy Act 2008 s.41); none on generation. (Accredited legacy gen receives FIT payments — scheme revenue, not a network rate.)" }
    },
    "dgnur": {
      "applies": true,
      "rate": null,
      "basis": "DGNUR (Distributed Generation Network Use rebate, DCUSA Sch 16): a credit under a site-specific connection agreement, with no published lookup rate. rate is null; resolve the amount from the site agreement."
    }
  }
}
  • cdcm.rates uses the same field names as the demand card (red_rate_pkwh, amber_rate_pkwh, green_rate_pkwh, fixed_rate_pmpan_day, capacity_rate_pkva_day, exceeded_capacity_rate_pkva_day, reactive_rate_pkvarh), with the unit rates typically negative credits.
  • tnuos.wider_generation is an object of { basis, zone_count, zones }. Each zone in zones carries five signed components: system_peak_gbp_kw, shared_year_round_gbp_kw, not_shared_year_round_gbp_kw, residual_gbp_kw and small_gen_discount_gbp_kw.
  • tnuos.eet_credit carries published_tariff_gbp_kw (the positive published magnitude) and credit_gbp_kw (its negative, the credit to the customer), resolved from the site demand zone.
  • bsuos is a top-level no-rate block (applies: false, rate: null) with a basis recording that generators are exempt since 1 April 2023 (CMP308).
  • policy carries a no-rate block for every demand-only levy: ccl, cfd, capacity_market, aahedc, nuclear_rab, ro and fit, each with applies: false, rate: null and a statutory basis. A top-level dgnur block (Distributed Generation Network Use rebate, DCUSA Sch 16) is present with applies: true and rate: null.
  • A charge that does not apply always carries an accurate basis; it is never silently dropped.

Rate Card by MPRN

Send a single gas MPRN to /rates/by-mprn and it returns the full Gas LDZ transportation rate card for that supply point - raw published charges only. This endpoint requires a key with the gas scope.

A gas MPRN identifies the meter point but does not itself carry a Local Distribution Zone. The LDZ is geographic and is resolved from the site postcode, so postcode is required. Each line carries its own period (the gas charging year runs 1 April to 31 March), a provenance label, a source_document reference and an applicability label. Values may be negative (for example a SOLR credit).

bash
curl -X GET "https://www.energycode.ai/api/v1/rates/by-mprn?mprn=1234567810&postcode=M1%201AA&charging_year=2026-27" \
  -H "X-Api-Key: YOUR_API_KEY_HERE"

The response resolves the zone identity and returns the six canonical UNC-governed components:

json
{
  "success": true,
  "data": {
    "mprn": {
      "mprn": "1234567810",
      "ldz": "NW",
      "exit_zone": "NW1",
      "gdn": "CAD"
    },
    "requested_charging_year": "2026-27",
    "basis": "Gas LDZ-transportation rate card. Each component's period is the gas charging year (1 April–31 March). Every rate line is raw published data with its own provenance and source document.",
    "charges_available": ["zca", "zco", "cca", "ecn", "cfi", "solr"],
    "charges_requested": null,
    "charges_returned": ["zca", "zco", "cca", "ecn", "cfi", "solr"],
    "transportation": {
      "zca": {
        "name": "LDZ Capacity (ZCA)",
        "applicability": "Applies to all sites (incl. CSEP). Full band table shown; no band selected (no AQ supplied).",
        "charging_year": "2026-27",
        "basis": "gas charging year (1 Apr–31 Mar)",
        "provenance": "official_actual",
        "source_document": "38509 - Charging Statement April 2026 North West.pdf",
        "rate_unit": "p_pkwh_day",
        "bands": {
          "small_band_rate": 0.3335,
          "medium_band_rate": 0.2784,
          "large_coefficient": 2.1945,
          "large_exponent": -0.2483,
          "large_minimum_rate": 0.0309
        }
      },
      "zco": {
        "name": "LDZ Commodity (ZCO)",
        "applicability": "Applies to all sites (incl. CSEP). Full band table shown; no band selected (no AQ supplied).",
        "charging_year": "2026-27",
        "basis": "gas charging year (1 Apr–31 Mar)",
        "provenance": "official_actual",
        "source_document": "38509 - Charging Statement April 2026 North West.pdf",
        "rate_unit": "p_kwh",
        "bands": {
          "small_band_rate": 0.0567,
          "medium_band_rate": 0.0477,
          "large_coefficient": 0.416,
          "large_exponent": -0.2586,
          "large_minimum_rate": 0.0049
        }
      },
      "cca": {
        "name": "Customer Capacity (CCA)",
        "applicability": "Applies to all direct-connect sites. A CSEP site carries only ZCA + ZCO + ECN. Full band table shown.",
        "charging_year": "2026-27",
        "basis": "gas charging year (1 Apr–31 Mar)",
        "provenance": "official_actual",
        "source_document": "38509 - Charging Statement April 2026 North West.pdf",
        "rate_unit": "p_pkwh_day",
        "bands": {
          "small_band_rate": 0.1574,
          "medium_band_rate": 0.0048,
          "large_coefficient": 0.1087,
          "large_exponent": -0.21,
          "large_minimum_rate": null
        }
      },
      "ecn": {
        "name": "Exit Capacity (ECN)",
        "applicability": "Applies to all sites at the exit zone level. Resolved per exit zone.",
        "charging_year": "2026-27",
        "basis": "gas charging year (1 Apr–31 Mar)",
        "provenance": "official_actual",
        "source_document": "38509 - Charging Statement April 2026 North West.pdf",
        "exit_zone": "NW1",
        "rate_p_pkwh_day": 0.0391,
        "indicative": false
      },
      "cfi": {
        "name": "Customer Fixed (CFI)",
        "applicability": "Applies to medium AQ band sites only (73,200–732,000 kWh/year); no CFI for small or large band. Both read-frequency rows shown.",
        "charging_year": "2026-27",
        "basis": "gas charging year (1 Apr–31 Mar)",
        "provenance": "official_actual",
        "source_document": "38509 - Charging Statement April 2026 North West.pdf",
        "rows": [
          { "read_frequency": "non_monthly", "rate_p_day": 44.8078 },
          { "read_frequency": "monthly",     "rate_p_day": 47.7111 }
        ]
      },
      "solr": {
        "name": "Supplier of Last Resort (SOLR)",
        "applicability": "Applies to domestic customers only. Values may be negative (a credit); the published sign is preserved. Both customer-type rows shown.",
        "charging_year": "2026-27",
        "basis": "gas charging year (1 Apr–31 Mar)",
        "provenance": "official_actual",
        "source_document": "38509 - Charging Statement April 2026 North West.pdf",
        "rows": [
          { "customer_type": "domestic", "rate_p_pkwh_day": -0.0047 },
          { "customer_type": "ic",       "rate_p_pkwh_day": 0 }
        ]
      }
    }
  }
}
  • The mprn identity resolves { mprn, ldz, exit_zone, gdn } for the supply point. gdn is the Gas Distribution Network code (CAD, NGN, SGN or WWU).
  • zca, zco and cca each carry a rate_unit and a bands object of small_band_rate, medium_band_rate, large_coefficient, large_exponent and large_minimum_rate. No band is selected: the request carries no annual quantity (AQ), so the small and medium rates are flat and the large band is the raw power function large_coefficient × SOQ^large_exponent, returned raw, never evaluated.
  • ecn (NTS exit) is a single block with an exit_zone, a rate_p_pkwh_day and an indicative flag. cfi returns both rows (read_frequency non_monthly and monthly, each with a rate_p_day), and solr returns both rows (customer_type domestic and ic, each with a rate_p_pkwh_day).

postcode is required for a gas MPRN

A missing postcode returns 400 GAS_POSTCODE_REQUIRED. If the outward code spans more than one gas network or LDZ, the request returns 400 GAS_POSTCODE_AMBIGUOUS: supply the full postcode to disambiguate. A valid MPRN and postcode that resolve to no zone on record return 422 LDZ_UNRESOLVABLE, and an MPRN that is not a valid 6-10 digit value returns 400 INVALID_MPRN. An MPRN served by an Independent Gas Transporter surfaces an igt_note.

IDNO MPANs

An IDNO (Independent DNO) MPAN carries a distributor id of 24 or above. That id identifies the IDNO, not the host DNO whose DUoS and network rates actually apply. The host DNO is geographic: it is determined by the site's postcode, and the MPAN alone cannot yield it.

So when you call /rates/by-mpan with an IDNO MPAN, supply the site postcodeand the endpoint resolves the host DNO for you, then returns the host DNO's rate card:

bash
curl -X GET "https://www.energycode.ai/api/v1/rates/by-mpan?mpan=008450922422524534007&postcode=SW1A%201AA&charging_year=2025-26" \
  -H "X-Api-Key: YOUR_API_KEY_HERE"

The response tells you which host DNO was used and how it was resolved:

json
{
  "success": true,
  "data": {
    "mpan": { "distributor_id": 24, "dno_name": "IDNO 24", "...": "..." },
    "requested_charging_year": "2026-27",
    "resolved_host_distributor_id": 12,
    "host_dno_resolution": "postcode",
    "cdcm": { "...": "..." }
  }
}
  • resolved_host_distributor_id is the DNO id (10-23) actually used to resolve the DUoS/DNO rates - the host DNO, not the IDNO id from the MPAN.
  • host_dno_resolution is postcode when the host DNO came from the postcode, override when you passed override_distributor_id, native for an ordinary DNO MPAN, or edcm for an EHV site.

If you already know the host DNO, you can skip the postcode and pass it directly with override_distributor_id (10-23). If you supply both, the override wins and a warnings entry flags any disagreement.

A postcode on an ordinary DNO MPAN (distributor id 10-23) is ignored. EDCM/EHV IDNO sites resolve their host DNO automatically from the site record - you do not need a postcode for them.

IDNO_HOST_REQUIRED

An IDNO MPAN (distributor 24+) sent to /rates/by-mpan with no postcode or override_distributor_id is rejected with 400 IDNO_HOST_REQUIRED. Send the site postcode, or override_distributor_id (host DNO 10-23).

Self-Service Usage and Quota Checks

Any key can inspect its own consumption and remaining quota via GET /usage, no admin grant required.

bash
curl -X GET "https://www.energycode.ai/api/v1/usage?period=month" \
  -H "X-Api-Key: YOUR_API_KEY_HERE"

Response (abridged):

json
{
  "success": true,
  "data": {
    "customer_id": "123e4567-e89b-12d3-a456-426614174000",
    "period": "month",
    "start_date": "2026-07-01",
    "end_date": "2026-07-31",
    "summary": {
      "total_requests": 1240,
      "successful_requests": 1198,
      "failed_requests": 42,
      "unique_endpoints": 7,
      "avg_latency_ms": 58
    },
    "quota": {
      "calls_this_month": 1240,
      "calls_per_month": 15000,
      "overage_calls": 0,
      "billing_mode": "capped",
      "requests_this_month": 812,
      "reference_rows_today": 3200,
      "reference_rows_per_day": 25000,
      "requests_per_minute": 120
    }
  }
}

Interactive Explorer and Machine-Readable Spec

Error Handling

All API responses follow a consistent format for errors.

Error Response Format

json
{
  "success": false,
  "error": {
    "code": "ERROR_CODE",
    "message": "Human-readable error description"
  }
}

Common Error Codes

HTTP StatusError CodeDescriptionAction
400VALIDATION_ERRORInvalid request parametersCheck request format and parameter values
400LIKELY_MPRNmpan is a 6-10 digit value, the shape of a gas MPRN, not an electricity MPANSend a full 21/22-digit electricity MPAN or a 13-digit MPAN Core; gas MPRN data is not served here
400UNEXPECTED_LENGTHmpan is a 19, 20 or 23 digit near-miss around the 21/22 full length (19 or 20 is usually dropped leading zeros; 23 is usually duplicated or appended characters)Resend the MPAN as a quoted, zero-padded string (the API never auto-pads)
400INVALID_MPANA full-length MPAN fails format or check-digit validationCheck the MPAN characters and check digit
400IDNO_HOST_REQUIREDIDNO MPAN (distributor 24+) with no postcode or override_distributor_idSend the site postcode, or override_distributor_id (host DNO 10-23)
401UNAUTHORIZEDMissing or invalid API keyVerify X-Api-Key header is set correctly
401INVALID_API_KEYAPI key format is incorrectCheck key format: ec_(live|test)_[64 hex chars]
403SCOPE_DENIEDEndpoint dataset not in your plan scopesUpgrade your plan or use a key with the right scope
404NOT_FOUNDResource not foundVerify DNO ID, charging year, tariff code exist
429RATE_LIMIT_EXCEEDEDPer-minute rate limit exceededWait for Retry-After seconds before retrying
429QUOTA_EXCEEDEDMonthly call quota or daily reference-row quota reached (capped accounts only)Wait for the quota window to reset, or contact us to raise your allowance
429TOO_MANY_AUTH_ATTEMPTSToo many failed auth attemptsWait 5 minutes before retrying
500INTERNAL_ERRORServer errorContact support if issue persists

Example: Handling Validation Errors

json
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid dno_id. Must be one of: 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23"
  }
}

Retry Strategy

For transient errors (rate limits, server errors), implement exponential backoff:

Python Example:

python
import time
import requests

def make_request_with_retry(url, headers, max_retries=3):
    for attempt in range(max_retries):
        response = requests.get(url, headers=headers)

        if response.status_code == 200:
            return response.json()

        if response.status_code == 429:
            retry_after = int(response.headers.get('Retry-After', 60))
            print(f"Rate limited. Waiting {retry_after} seconds...")
            time.sleep(retry_after)
            continue

        if response.status_code >= 500:
            wait_time = 2 ** attempt  # Exponential backoff: 1s, 2s, 4s
            print(f"Server error. Retrying in {wait_time}s...")
            time.sleep(wait_time)
            continue

        # Non-retryable error
        return response.json()

    raise Exception("Max retries exceeded")

Code Examples

Python

Install dependencies:

bash
pip install requests

Example: Fetch DNO regions and DUoS rates

python
import requests
import os

API_KEY = os.getenv("ENERGYCODE_API_KEY")
BASE_URL = "https://www.energycode.ai/api/v1"

headers = {
    "X-Api-Key": API_KEY,
    "Content-Type": "application/json"
}

# 1. List all DNO regions
def get_dno_regions():
    response = requests.get(f"{BASE_URL}/reference/dnos", headers=headers)
    response.raise_for_status()
    return response.json()

# 2. Get DUoS rates for a specific DNO
def get_duos_rates(dno_id, charging_year="2025-26"):
    params = {
        "dno_id": dno_id,
        "charging_year": charging_year
    }
    response = requests.get(f"{BASE_URL}/rates/duos", headers=headers, params=params)
    response.raise_for_status()
    return response.json()

# Usage
if __name__ == "__main__":
    # Fetch DNO regions
    dnos = get_dno_regions()
    print(f"Found {len(dnos['data'])} DNO regions")

    # Get rates for Eastern Power Networks (DNO 10)
    rates = get_duos_rates(dno_id=10)
    print(f"Retrieved {len(rates['data']['rates'])} tariff categories")

JavaScript / TypeScript (Node.js)

Install dependencies:

bash
npm install axios

Example:

typescript
import axios, { AxiosInstance } from 'axios';

const API_KEY = process.env.ENERGYCODE_API_KEY;
const BASE_URL = 'https://www.energycode.ai/api/v1';

const client: AxiosInstance = axios.create({
  baseURL: BASE_URL,
  headers: {
    'X-Api-Key': API_KEY,
    'Content-Type': 'application/json',
  },
});

// 1. List all DNO regions
async function getDnoRegions() {
  const response = await client.get('/reference/dnos');
  return response.data;
}

// 2. Get DUoS rates for a specific DNO
async function getDuosRates(dnoId: number, chargingYear: string = '2025-26') {
  const response = await client.get('/rates/duos', {
    params: { dno_id: dnoId, charging_year: chargingYear },
  });
  return response.data;
}

// Usage
async function main() {
  try {
    // Fetch DNO regions
    const dnos = await getDnoRegions();
    console.log(`Found ${dnos.data.length} DNO regions`);

    // Get rates for Eastern Power Networks (DNO 10)
    const rates = await getDuosRates(10);
    console.log(`Retrieved ${rates.data.rates.length} tariff categories`);
  } catch (error) {
    if (axios.isAxiosError(error)) {
      console.error('API Error:', error.response?.data);
    } else {
      console.error('Unexpected error:', error);
    }
  }
}

main();

cURL (Shell Script)

bash
#!/bin/bash

API_KEY="YOUR_API_KEY_HERE"
BASE_URL="https://www.energycode.ai/api/v1"

# Get all DNO regions
curl -X GET "$BASE_URL/reference/dnos" \
  -H "X-Api-Key: $API_KEY" \
  | jq '.'

# Get DUoS rates for DNO 10
curl -X GET "$BASE_URL/rates/duos?dno_id=10&charging_year=2025-26" \
  -H "X-Api-Key: $API_KEY" \
  | jq '.'

Best Practices

1. Caching Strategies

Rate data changes annually (per charging year). Cache aggressively:

python
from functools import lru_cache
import requests

@lru_cache(maxsize=100)
def get_duos_rates_cached(dno_id: int, charging_year: str):
    """Cache rates by DNO and charging year (rates don't change within a year)"""
    response = requests.get(
        f"{BASE_URL}/rates/duos",
        headers=headers,
        params={"dno_id": dno_id, "charging_year": charging_year}
    )
    return response.json()

Cache invalidation:

  • Rates change on 1 April each year (start of new charging year)
  • Cache can be invalidated annually or when a new charging year is published

2. Batch Requests Efficiently

Instead of making 14 separate requests for all DNOs, fetch reference data once and iterate:

python
# GOOD: One request for all DNOs
dnos = get_dno_regions()
for dno in dnos['data']:
    rates = get_duos_rates(dno['id'])
    # Process rates...

3. Monitor Rate Limits Proactively

Check X-RateLimit-Remaining and slow down before hitting the limit:

python
def make_request(url, headers):
    response = requests.get(url, headers=headers)

    remaining = int(response.headers.get('X-RateLimit-Remaining', 100))
    if remaining < 10:
        print("Approaching rate limit, slowing down...")
        time.sleep(2)

    return response.json()

4. Validate Input Before Sending

Reduce failed requests by validating inputs locally:

python
VALID_DNO_IDS = [10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]

def validate_dno_id(dno_id: int):
    if dno_id not in VALID_DNO_IDS:
        raise ValueError(f"Invalid DNO ID: {dno_id}")

5. Handle Errors Gracefully

Always check the success field before accessing data:

python
response = requests.get(url, headers=headers)
data = response.json()

if not data.get('success'):
    error = data.get('error', {})
    raise Exception(f"API Error [{error.get('code')}]: {error.get('message')}")

# Safe to access data
result = data['data']

6. Use Test Keys for Development

Keep test and live keys separate:

python
# Use environment-specific keys
API_KEY = os.getenv("ENERGYCODE_API_KEY_TEST")  # Development
# API_KEY = os.getenv("ENERGYCODE_API_KEY_LIVE")  # Production

Getting Help

API Reference Documentation

Full API reference with all endpoints, parameters, and response schemas:

Support Channels

ChannelPurposeResponse Time
Email[email protected]1-2 business days
Technical Issues[email protected]24 hours
Account/Billing[email protected]1 business day

Before Contacting Support

Please include:

  1. API key prefix (first 12 characters, e.g., ec_live_abc1...)
  2. Request ID from X-Request-ID response header
  3. Timestamp of the issue
  4. Complete request (cURL format preferred)
  5. Complete response (including status code and headers)
  6. Expected behavior vs. actual behavior

Appendix: Reference Data

Valid DNO IDs

IDNameOperator Group
10Eastern Power NetworksUKPN
11East MidlandsNGED
12London Power NetworksUKPN
13SP ManwebSPEN
14West MidlandsNGED
15Northern Powergrid (Northeast)NPg
16Electricity North WestENWL
17Scottish Hydro (SHEPD)SSEN
18SP DistributionSPEN
19South Eastern Power NetworksUKPN
20Southern Electric (SEPD)SSEN
21South WalesNGED
22South WestNGED
23Northern Powergrid (Yorkshire)NPg

Charging Year Format

Charging years run from 1 April to 31 March and are formatted as YYYY-YY:

  • 2025-26 = 1 April 2025 to 31 March 2026
  • 2026-27 = 1 April 2026 to 31 March 2027

Common Tariff Categories (CDCM)

CodeDescription
DOM_AGGDomestic Aggregated
ND_AGG_B1Non-Domestic Aggregated, Band 1
ND_AGG_B2Non-Domestic Aggregated, Band 2
ND_AGG_B3Non-Domestic Aggregated, Band 3
ND_AGG_B4Non-Domestic Aggregated, Band 4
LV_SS_B1LV Site Specific, Band 1
HV_SS_B1HV Site Specific, Band 1
LV_GEN_AGGLV Generation Aggregated

This is a representative subset. The tariff_category code is returned on every DUoS rate row, and the full CDCM category reference is available on request - email [email protected].

Last Updated: 2026-07-09 · API Version: v1 · Status: Stable