Conventions
Every operation follows the same rules, so once you know them you can read any response without surprises.
Versions and compatibility
The major version is the first segment of the path: /v3/…. Within a major version, changes are only additive: new operations, new optional parameters, new response fields and new enum values. Anything that would break an integration (removing or renaming something, changing a type, unit, default, rounding or meaning, or adding a required parameter) ships as a new major version instead.
So that additions never break you:
- Ignore fields you don’t recognise. Don’t fail on them, and don’t reject a response because it has more than you expected.
- Accept enum values and codes you don’t know. Handle them like a generic case rather than as an error.
When a major version is deprecated, every response from it carries a Deprecation and a Sunset header, and the sunset is at least six months after the announcement. See Migrate from v1 and v2 for the current one.
Responses
A successful response is always a JSON object with data, and meta where there is something to say about the delivery:
{
"data": { "…": "the resource, the list or the computed view" },
"meta": { "…": "delivery facts, such as how current data is" }
}data is self-describing: everything that changes what a number means (its unit, the period it covers, how it was aggregated, what was left out) is inside data, next to the number. When you store or forward a result, keep data whole.
Errors use a different format, described in Errors and limits.
Ids
Ids are strings, such as "1042". Treat them as opaque: compare them for equality, but don’t parse them, convert them to numbers, sort by them or assume a length or format. Today they look like numbers; that may change.
Fields and missing values
- Every documented field is always present. A field that doesn’t apply, or isn’t known, is
null; the reference says which fields can be. - An empty list is
[], nevernullor missing. nulland0mean different things.0is a measured zero;nullmeans there is no data. For example, a category with no data points in a month isnull, while a category whose data adds up to nothing is0.
Dates and times
| Kind | Format | Example |
|---|---|---|
| Timestamp | RFC 3339, UTC, with Z | 2026-09-16T09:12:04.000Z |
| Date | YYYY-MM-DD | 2025-03-31 |
| Month | YYYY-MM | 2025-03 |
Timestamp fields end in At, apart from meta.asOf. Ranges include both ends unless an operation says otherwise: from=2025-01&to=2025-12 is the whole of 2025.
Units and numbers
- Every quantity names its unit, in a field named after the measure:
emissionscomes withemissionsUnit. Read the unit from the response, never from what you asked for. - Money is an object with a decimal
amountand an ISO 4217currency:{ "amount": 1500.5, "currency": "NOK" }. - Percentages run from 0 to 100, and their names end in
Percent. - Rounding. Values are rounded once, from unrounded data, to the precision the reference gives for each field. So a total can differ slightly from the sum of its rounded parts: three categories of 0.333 t each are returned as 0.33 + 0.33 + 0.33 = 0.99, while their total of 0.999 t is returned as 1.00. Both are right. Don’t adjust either to make them match.
Enums and codes
- Enum values are
UPPER_SNAKE_CASE, in responses and in query parameters alike:granularity=QUARTER,SCOPE_1,UP_TO_DATE. Values from external standards keep their own form: units (kg,tCO2e), ISO 4217 currencies (NOK) and ISO 3166 countries (NO). - Values that only the server chooses can gain new members at any time (see Versions and compatibility). The reference lists the known ones.
- Where a value comes as a
codewith alabel, thecodeis stable: it is never renamed, removed or reused. Thelabelis for display only and can change at any time. Program againstcode, neverlabel.
Requests
- Send request bodies as JSON, with
Content-Type: application/json. - Unknown parameters are rejected with a
400, in the query and in the body alike. A misspelled filter fails loudly instead of being ignored. - An input the API can’t honour exactly is rejected with a
400that says what would be accepted. It is never silently adjusted: a range that doesn’t line up with the requested period size is an error, not rounded to one that does.
Lists
Every list response has meta.nextCursor. null means you have the whole list.
No list is paginated yet, so nextCursor is always null today. Once a list is paginated, it will return a cursor, and you request the next page by sending it back as cursor, until nextCursor is null. Write that loop now and your integration won’t need to change. Send cursor only when you received one: today, lists accept no query parameters, and an unknown one is a 400.
Python
import os
import requests
BASE_URL = "https://api.energi.ai"
HEADERS = {"Authorization": f"Bearer {os.environ['ENERGI_API_KEY']}"}
def get_all(path):
"""Every item of a list, following meta.nextCursor."""
items, params = [], {}
while True:
response = requests.get(f"{BASE_URL}{path}", headers=HEADERS, params=params, timeout=60)
response.raise_for_status()
page = response.json()
items.extend(page["data"])
cursor = page["meta"]["nextCursor"]
if cursor is None:
return items
params = {"cursor": cursor}
companies = get_all("/v3/company")Python
import os
import requests
BASE_URL = "https://api.us.energi.ai"
HEADERS = {"Authorization": f"Bearer {os.environ['ENERGI_API_KEY']}"}
def get_all(path):
"""Every item of a list, following meta.nextCursor."""
items, params = [], {}
while True:
response = requests.get(f"{BASE_URL}{path}", headers=HEADERS, params=params, timeout=60)
response.raise_for_status()
page = response.json()
items.extend(page["data"])
cursor = page["meta"]["nextCursor"]
if cursor is None:
return items
params = {"cursor": cursor}
companies = get_all("/v3/company")Each list documents the order it returns its items in.