Upload data
POST /v3/company/{companyId}/data-points stores activity and finance records for a company: invoices, fuel and energy use, posting journal lines and the like. Energi.AI classifies each record and calculates its emissions after the upload returns.
It needs the data-point:write scope.
A first upload
The body is an object whose data holds 1 to 500 records:
curl
curl --max-time 60 -X POST "https://api.energi.ai/v3/company/1042/data-points" \
-H "Authorization: Bearer $ENERGI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"data": [
{
"correlationId": "INV-2211-1",
"date": "2025-03-31",
"category1": "Fuel consumption",
"category2": "Diesel",
"usage": 420.5,
"usageUnit": "l",
"cost": { "amount": 8410, "currency": "NOK" },
"vendorName": "Example Vendor AS",
"vendorCountryCode": "NO"
},
{
"correlationId": "INV-2211-2",
"date": "2025-01-01",
"dateTo": "2025-03-31",
"category1": "Electricity usage",
"usage": 18250,
"usageUnit": "kWh",
"assetName": "Head office"
}
]
}'curl
curl --max-time 60 -X POST "https://api.us.energi.ai/v3/company/1042/data-points" \
-H "Authorization: Bearer $ENERGI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"data": [
{
"correlationId": "INV-2211-1",
"date": "2025-03-31",
"category1": "Fuel consumption",
"category2": "Diesel",
"usage": 420.5,
"usageUnit": "l",
"cost": { "amount": 8410, "currency": "NOK" },
"vendorName": "Example Vendor AS",
"vendorCountryCode": "NO"
},
{
"correlationId": "INV-2211-2",
"date": "2025-01-01",
"dateTo": "2025-03-31",
"category1": "Electricity usage",
"usage": 18250,
"usageUnit": "kWh",
"assetName": "Head office"
}
]
}'A successful upload answers 201 Created with one result per record, in the order you sent them:
{
"data": {
"results": [
{ "index": 0, "correlationId": "INV-2211-1", "id": "88120" },
{ "index": 1, "correlationId": "INV-2211-2", "id": "88121" }
]
}
}index is the record’s position in your data, from 0. correlationId is null for a record sent without one. id is the stored record’s id.
All or nothing
An upload stores every record or none of them. If any record is invalid, the whole request is rejected with a 400 and nothing is stored; errors names each invalid field by its path, such as data[3].date. Fix those records and send the whole batch again.
To send more than 500 records, split them into several uploads. Each upload is its own all-or-nothing unit.
Records
Each record needs a date, a category1 and at least one of cost, usage and productQuantity. Everything else is optional.
| Field | Type | Meaning |
|---|---|---|
date | string, required | When the activity happened: a date (2025-03-31) or a UTC timestamp (2025-03-31T08:15:00Z). With dateTo, the first day of the range. |
dateTo | string | The last day of a range, inclusive, in the same formats, and not before date. A range is stored by day, so any time of day is dropped. Leave it out for a point in time. |
category1 | string, required | What the record is: a general ledger account number (4350), an activity (Electricity usage), a grouping (Vehicle fleet) or your own label. |
category2 … category5 | string | Each narrows the one before, e.g. category2 as a general ledger account name. |
cost | object | What the activity cost: { "amount": 8410, "currency": "NOK" }, with an ISO 4217 currency. |
usage, usageUnit | number, string | How much was used and in which unit, e.g. 18250 and kWh, or litres of fuel as l. Send both or neither. |
productQuantity | number | How many products the record covers. |
correlationId | string | Your unique key for the record. See Retries and correlationId. |
description | string | Free text, such as an invoice number. It doesn’t affect the calculation. |
vendorName, vendorOrgNumber, vendorCountryCode | string | The supplier: its name, its organisation number in its national registry, and its ISO 3166-1 alpha-2 country. |
customerName, customerOrgNumber, customerCountryCode | string | The customer, the same way. |
assetName, assetCode | string | The asset the record belongs to, such as a building or a ship, and your unique code for it. |
region | string | The region whose emission factors apply, when it differs from your company’s country: a country (DK), a subdivision (CA-ON) or a US eGRID subregion (RFCW). |
entryNo, subEntryId | string | For accounting data: the posting journal entry number, and the line within the entry. |
dimensions | array | Up to 10 labels for exploring emissions by project, department and the like: { "type": "Project", "value": "Summer sale" }. Each needs a type and an id or value. |
The API reference has every field’s exact rules. An unknown field is rejected with UNKNOWN_PARAMETER, so a misspelled name never goes unnoticed.
Retries and correlationId
correlationId is your own unique key for a record, such as an invoice line number. Send one with every record you might send again:
- A record whose
correlationIdthe company already has replaces the stored record, instead of adding a second one. Send it whole: a field you leave out is cleared. - A record without a
correlationIdis added as a new record every time you send it.
So with a correlationId on every record, retrying an upload is safe, for example after a timeout, when you can’t tell whether the first attempt was stored. The same key also lets you correct a record later by sending it again.
Each correlationId may appear only once per upload; a repeated one is a 400 with DUPLICATE_CORRELATION_ID in errors. It may not be an empty string either: leave the field out for a record without a key.
Don’t send the same record from two processes at the same time: the one stored last wins.
Locked records
Once a report has been issued for a period, the records behind it are locked. An upload that would change a locked record is rejected with 409 CONFLICT, and nothing from it is stored. errors names each such record:
{
"type": "about:blank",
"title": "Conflict",
"status": 409,
"code": "CONFLICT",
"detail": "1 record updates a locked record, so nothing was stored.",
"errors": [
{
"in": "BODY",
"name": "data[0].correlationId",
"code": "DATA_LOCKED",
"message": "it would change a record locked behind an issued report, which can no longer change"
}
]
}Remove those records, or send them unchanged, and upload again. Sending a locked record exactly as it is stored is not a conflict: it answers 201 with the stored record’s id, so retries stay safe.
A new record dated inside a locked period is stored, but it is locked from then on, so it can’t be changed afterwards.
When emissions reflect an upload
Emissions are calculated after the upload returns, so they don’t change at once. The emissions summaries say how current their figures are with meta.asOf: once asOf is later than the moment your upload finished, your upload is reflected.