Skip to content
Last updated

Creating custom tariffs

ev.energy schedules charging around the user's electricity tariff, so the tariff needs to be right. Most users can pick their tariff from the public list at GET /tariffs. If a user's tariff is not in that list, you can create a custom tariff from its rates and assign it to the user.

This guide covers:

Build it visually

The tariff builder turns a tariff's prices and hours into the request body, and checks it against the rules in this guide before you send it.

When to create a custom tariff

Create a custom tariff when:

  • the user's tariff is not in the public list, for example a local supplier or a business tariff;
  • the user has negotiated rates that differ from the published ones;
  • your own product already knows the user's rates, and you want ev.energy to use exactly those rates.

Search the public list first. Public tariffs are kept up to date by ev.energy, including price changes. A custom tariff is only updated when you update it.

A custom tariff has fixed rates that repeat every week, with optional rates for public holidays. Dynamic tariffs, whose prices change every day, cannot be created this way.

Requirements

To begin you will need:

  • A valid Authorization token (see this page for instructions).
  • The tariff:write scope to create, update and delete tariffs, and tariff:read to read them and their generated prices back.
  • The user:write scope if you want to assign the tariff to a user, either by sending user when you create it or afterwards. See Assigning the tariff to a user.
  • The region:read scope if you want to look up region URLs with GET /regions.

Who can see a custom tariff

A custom tariff created with the Client Credentials grant belongs to the organisation linked to the application that created it, and is private to that organisation.

  • GET /tariffs lists your organisation's custom tariffs alongside the public tariffs. It never lists another organisation's custom tariffs.
  • Another organisation's custom tariff returns 404 from GET /tariffs/{tariff_id} and GET /tariffs/{tariff_id}/prices.
  • When another organisation reads a resource that links to your custom tariff, such as a user's default_location.tariff, the link renders as null and cannot be expanded.
  • Webhooks follow the same rule. A charging_sub_session webhook sent to your organisation links to your custom tariff, and the same webhook sent to another organisation has tariff: null.
  • The one exception is ev.energy's own apps: a user who has your custom tariff sees it in the ev.energy app. They cannot change or delete it.
  • Only your organisation can update or delete the tariff.

Only the Client Credentials grant creates and manages tariffs for your organisation, and only when your application is linked to one: otherwise POST /tariffs returns 403. If your application acts for a user with the Authorization Code grant, the tariff belongs to that user instead, even when your application is linked to an organisation. Only that user, through your application, can update or delete it, and a user token cannot update or delete your organisation's tariffs.

A tariff that belongs to a user, including one the user created in the ev.energy app, is private to them:

  • The user who created it, and any user it is assigned to, can read it.
  • An organisation can read it while it is the import or export tariff of one of the organisation's users, or was created by one. This lets you see the tariff your users are on, even when they set it up themselves.
  • Everyone else, including other users of your application, gets 404, and cannot assign it to a user.

Creating a tariff

A tariff is described by rate_periods. Each period gives the rates for one day, and the months and weekdays that day applies to.

  • months uses 1 for January to 12 for December.
  • iso_weekdays uses 1 for Monday to 7 for Sunday.
  • Every combination of month and weekday must be covered by exactly one period.
  • Each period's rates must cover the whole day, with no gaps or overlaps.

Each rate has:

FieldDescription
start_timeLocal start time as HH:MM, on the hour or half hour. Inclusive.
end_timeLocal end time as HH:MM, on the hour or half hour. Exclusive. 00:00 means midnight at the end of the day.
price_minor_units_per_kwhPrice per kWh in the smallest currency unit (pence, cents), as a decimal string such as "7.5", with up to 16 digits before the decimal point and 6 after. Negative prices are allowed.
is_off_peakOptional. true for off-peak rates. Defaults to false.

One rate in a day may cross midnight, such as 23:30 to 05:30. A single rate whose start_time equals its end_time, such as 00:00 to 00:00, is a flat rate for the whole day.

Times are in the tariff's timezone, timezone_name. Clock changes are handled for you.

The tariff builder builds rate_periods for you: start from a common shape such as day and night or weekday peak, set the hours of each daily schedule, choose which days use each one, and copy the request body. You can also paste a request body or a GET /tariffs/{tariff_id} response into it to see and edit its rates.

Example: weekday, weekend and public holiday rates

This tariff has:

  • Weekdays: 7.5p off-peak from midnight to 07:00, and 24.5p for the rest of the day.
  • Weekends: 7.5p off-peak from midnight to 09:00, and 18p for the rest of the day.
  • Public holidays in England: 7.5p all day.
curl --request POST \
     --url https://api.ev.energy/v2/tariffs \
     --header 'accept: application/json' \
     --header 'authorization: Bearer example' \
     --header 'content-type: application/json' \
     --data '
{
  "name": "EV Saver",
  "supplier_tariff_id": "EVS-2026-10",
  "currency": "GBP",
  "timezone_name": "Europe/London",
  "user": "https://api.ev.energy/v2/users/user01HN2NJ9NMRZBXT1H6FT9N7735",
  "rate_periods": [
    {
      "months": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
      "iso_weekdays": [1, 2, 3, 4, 5],
      "rates": [
        {"start_time": "00:00", "end_time": "07:00", "price_minor_units_per_kwh": "7.5", "is_off_peak": true},
        {"start_time": "07:00", "end_time": "00:00", "price_minor_units_per_kwh": "24.5"}
      ]
    },
    {
      "months": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
      "iso_weekdays": [6, 7],
      "rates": [
        {"start_time": "00:00", "end_time": "09:00", "price_minor_units_per_kwh": "7.5", "is_off_peak": true},
        {"start_time": "09:00", "end_time": "00:00", "price_minor_units_per_kwh": "18"}
      ]
    }
  ],
  "public_holiday_rates": {
    "country": "GB",
    "subdivision": "ENG",
    "rates": [
      {"start_time": "00:00", "end_time": "00:00", "price_minor_units_per_kwh": "7.5", "is_off_peak": true}
    ]
  }
}
'

To give the summer months different rates, split months across two or more periods, for example [4, 5, 6, 7, 8, 9] and [1, 2, 3, 10, 11, 12], each with its own weekday periods.

Other fields

FieldRequiredDescription
nameYesThe tariff's name as the user knows it. Up to 200 characters.
rate_periodsYesThe weekly rates, as described above.
public_holiday_ratesNoRates that replace the normal rates on public holidays. country is an ISO 3166-1 alpha-2 code, and subdivision is optional. When a holiday falls on a weekend and the country moves it to a weekday, as with UK bank holidays, only the weekday it moves to uses these rates.
supplierNoURL of the supplier from GET /suppliers. Defaults to a generic "My supplier".
supplier_tariff_idNoYour own or the supplier's reference for the tariff. Up to 100 characters.
userNoURL of a user you can access. The tariff is assigned to this user as part of the same request. Requires user:write.
regionOnly if user is not sentURL of the region the tariff applies to: a public reference region, or one your organisation created if your application is linked to one. Applications linked to an organisation can list both with GET /regions. Defaults to the user's region.
currencyOnly if user is not sentISO 4217 currency code. Defaults to the user's currency.
timezone_nameOnly if user is not sentIANA timezone name from GET /timezones. Defaults to the user's timezone. Its offset from UTC must always be a whole or half hour.

The response

A successful request returns 201 Created with the tariff, in the same format as GET /tariffs/{tariff_id}. The response includes your rate_periods and public_holiday_rates. Periods with identical rates may be grouped differently from your request, but they describe the same prices.

Prices for the next 52 weeks are generated straight away, along with the four weeks before today. You can read them from GET /tariffs/{tariff_id}/prices with the tariff:read scope. You don't need the tariff:prices:read scope: it is only for prices licensed from third-party data providers, and a custom tariff's prices are never licensed.

In the sandbox, the request returns a sample tariff and nothing is created.

Assigning the tariff to a user

There are two ways to make a custom tariff a user's import tariff.

When creating it. Send user in the POST /tariffs body, as in the example above. This requires the user:write scope as well as tariff:write; without it the request fails with 403 and nothing is written. The tariff is created and assigned in one step. If either part fails, nothing is written.

Later. Update the user's default_location.tariff with PATCH /users/{user_id}. This requires the user:write scope.

curl --request PATCH \
     --url https://api.ev.energy/v2/users/user01HN2NJ9NMRZBXT1H6FT9N7735 \
     --header 'accept: application/json' \
     --header 'authorization: Bearer example' \
     --header 'content-type: application/json' \
     --data '
{
  "default_location": {
    "tariff": "https://api.ev.energy/v2/tariffs/tari01JA7Q3ZKX2V8N5RB6TCWD4M9H"
  }
}
'

You can assign a public tariff or one of your organisation's custom tariffs. You cannot assign another organisation's custom tariff.

When you assign a tariff with user on POST /tariffs, any price threshold the user has set is cleared, because it was set against their old prices.

Repricing a tariff

When the user's rates change, update the existing tariff with PATCH /tariffs/{tariff_id} rather than creating a new one. Send the complete new rate_periods with effective_from, the first day the new rates apply:

curl --request PATCH \
     --url https://api.ev.energy/v2/tariffs/tari01JA7Q3ZKX2V8N5RB6TCWD4M9H \
     --header 'accept: application/json' \
     --header 'authorization: Bearer example' \
     --header 'content-type: application/json' \
     --data '
{
  "effective_from": "2026-11-01",
  "rate_periods": [
    {
      "months": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
      "iso_weekdays": [1, 2, 3, 4, 5, 6, 7],
      "rates": [
        {"start_time": "23:30", "end_time": "05:30", "price_minor_units_per_kwh": "8.5", "is_off_peak": true},
        {"start_time": "05:30", "end_time": "23:30", "price_minor_units_per_kwh": "26.1"}
      ]
    }
  ]
}
'

The rules for repricing are:

  • effective_from is a date in the tariff's timezone. It must be after today and no more than two years after today. You cannot reprice today or the past, so charging that has already been costed is never changed. To change prices from tomorrow, send tomorrow's date.
  • rate_periods replaces all of the existing rates from effective_from. Send public_holiday_rates again if the tariff should keep its holiday rates; leaving it out removes them from that date.
  • Prices before effective_from are unchanged. Prices from effective_from onwards use the new rates.
  • Until effective_from, GET /tariffs/{tariff_id} still shows the rates in force today. GET /tariffs/{tariff_id}/prices shows the new prices for future dates straight away.

You can change name and supplier_tariff_id at any time, without effective_from:

curl --request PATCH \
     --url https://api.ev.energy/v2/tariffs/tari01JA7Q3ZKX2V8N5RB6TCWD4M9H \
     --header 'authorization: Bearer example' \
     --header 'content-type: application/json' \
     --data '{"name": "EV Saver (2027)"}'

currency, timezone_name, region and supplier cannot be changed after a tariff is created, and are ignored if sent. To change them, create a new tariff and assign it to the user.

Only the tariff's owner can update it. Updating a tariff you can see but do not own, such as a public tariff, returns 403.

Deleting a tariff

Delete a custom tariff you no longer need with DELETE /tariffs/{tariff_id}. This deletes the tariff and its prices, and returns 204 No Content.

curl --request DELETE \
     --url https://api.ev.energy/v2/tariffs/tari01JA7Q3ZKX2V8N5RB6TCWD4M9H \
     --header 'authorization: Bearer example'

A tariff that is still in use cannot be deleted. The request returns 409 Conflict with problem type tariff-in-use when:

  • the tariff is any user's import or export tariff. Assign the user a different tariff first, then delete it;
  • the tariff has been used for charging, or is referenced by other historical records. Such a tariff cannot be deleted, because those records depend on its prices. Leave it in place.
{
  "type": "https://api.ev.energy/v2/problems/tariff-in-use/",
  "status": 409,
  "title": "Tariff in use",
  "detail": "This tariff is assigned to one or more users or referenced by historical records and cannot be deleted.",
  "instance": "https://api.ev.energy/v2/tariffs/tari01JA7Q3ZKX2V8N5RB6TCWD4M9H"
}

Validation errors

If the request body is invalid, the API returns 422 Unprocessable Content with problem type validation-failed-tariff. Each invalid field is listed under field_errors, as described in Error responses.

{
  "type": "https://api.ev.energy/v2/problems/validation-failed-tariff/",
  "status": 422,
  "title": "Request failed validation.",
  "detail": "Error on fields: rate_periods",
  "field_errors": {
    "rate_periods": ["Every month and weekday must be covered exactly once. Not covered: month 12, iso_weekday 7."]
  }
}

Common causes are:

ProblemFix
A month and weekday is not covered, or covered twiceCheck that every month 1–12 and weekday 1–7 appears in exactly one period.
A day's rates leave a gap or overlapMake each rate's end_time the next rate's start_time, ending where the first rate starts.
A time is not on the hour or half hourUse times ending in :00 or :30.
Unknown holiday country or subdivisionUse an ISO 3166-1 alpha-2 country code and a subdivision that country recognises, or leave subdivision out.
region, supplier or user not foundUse a URL returned by the API to your application.
region, currency or timezone_name missingSend them, or send user so they can be taken from the user.
effective_from is today, in the past, or more than two years aheadUse a date after today and within two years, in the tariff's timezone.
timezone_name is offset from UTC by a quarter hour at some point in the year, such as Asia/Kathmandu, Australia/Eucla or Pacific/ChathamPrices start on UTC half hours, so these timezones can't be used. Use a nearby timezone on a whole or half hour offset.
rate_periods sent without effective_from, or the other way roundSend both together.

The exact wording of error messages may change. Use the problem type and the field names in field_errors to handle errors in code.