Skip to Content
Upload data

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 --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 --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.

FieldTypeMeaning
datestring, requiredWhen 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.
dateTostringThe 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.
category1string, requiredWhat the record is: a general ledger account number (4350), an activity (Electricity usage), a grouping (Vehicle fleet) or your own label.
category2 … category5stringEach narrows the one before, e.g. category2 as a general ledger account name.
costobjectWhat the activity cost: { "amount": 8410, "currency": "NOK" }, with an ISO 4217 currency.
usage, usageUnitnumber, stringHow much was used and in which unit, e.g. 18250 and kWh, or litres of fuel as l. Send both or neither.
productQuantitynumberHow many products the record covers.
correlationIdstringYour unique key for the record. See Retries and correlationId.
descriptionstringFree text, such as an invoice number. It doesn’t affect the calculation.
vendorName, vendorOrgNumber, vendorCountryCodestringThe supplier: its name, its organisation number in its national registry, and its ISO 3166-1 alpha-2 country.
customerName, customerOrgNumber, customerCountryCodestringThe customer, the same way.
assetName, assetCodestringThe asset the record belongs to, such as a building or a ship, and your unique code for it.
regionstringThe 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, subEntryIdstringFor accounting data: the posting journal entry number, and the line within the entry.
dimensionsarrayUp 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 correlationId the 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 correlationId is 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.

Last updated on