Push charging sessions
POST /partner/sessions • deliver your customers' charging sessions yourself, one by one, with transaction id and meter readings
POST /partner/sessions is for a partner that runs the back office of its
customers' laadstations. You already receive every session from the charger,
so you deliver them to Joulo yourself. We do not pull them from your system,
and the customer never logs in with a charger brand.
We book each session on the customer's laadstation, exactly as the sessions
of a charger we read ourselves. Every session counts toward the ERE position
in GET /partner/ere-positions, and Joulo pays
the customer every quarter.
We enable this per partner. Until we do, the endpoint answers
403 session_push_not_enabled, and an upload with charger_brand push
is rejected as charger_brand_unsupported.
The sequence
- Upload the customer with
POST /partner/customersand pre-couple the charge point. See below. - Let the customer onboard in your app with an auth-link. See Onboarding inside your app. When they activate, we attach the charge point to their account.
- Push the history. Once
GET /partner/customersshows the customer, send every finished session since 1 January 2026. Did the MID meter come later? Then start there. - Keep pushing. Send each session once it has stopped. Once a day is enough. Sooner is fine.
A session for a charge point we have not attached yet comes back as
unknown_charger. Nothing is lost: send it again after step 2.
Pre-coupling the charge point
Send the charge point with the customer at upload. charger_brand is push,
charger_id your own id for the charge point:
{
"consent": true,
"send_invite": false,
"customers": [
{
"name": "Jan de Vries",
"email": "[email protected]",
"external_ref": "KLANT-8842",
"postal_code": "6971LB",
"house_number": "12",
"charger_brand": "push",
"charger_id": "PF-000123",
"charger_vendor": "Alfen BV",
"charger_model": "NG910-60027",
"charger_serial": "ACE0012345",
"charger_location": { "postal_code": "6971LB", "house_number": "12", "street": "Dorpsstraat", "city": "Brummen" }
}
]
}| Field | Required | What it does |
|---|---|---|
charger_brand | yes | push. |
charger_id | yes | Your charge point id, kept as sent, at most 120 characters. Send the same id with every session. |
charger_vendor, charger_model | recommended | chargePointVendor and chargePointModel from the BootNotification. They decide the catalog model, and so whether the charger has a MID meter. Without them we review the charger by hand. |
charger_serial | recommended | The charger's serial number. A serial that is already on a Joulo laadstation is rejected as charger_claimed: the charger would reach us twice and count twice. |
charger_location | recommended | The address of the charge point: postal_code and house_number required, house_number_addition, street and city optional. We compare it with the address of the EAN. A mismatch never refuses the row. Without a postcode or house number we ignore the address and say so in the answer. |
The upload answers with chargers: per created row the catalog model we
matched and whether it has a MID meter. catalog_model: null means no match,
and we review that charger by hand. location says whether we stored the
address: used, ignored (no postcode or house number) or missing. mid_meter: false means the charger earns
no ERE. A MID meter outside the charger, such as one in the meter cupboard,
never counts for home charging.
Push sessions
Send at most 100 sessions per request, for any of your customers and in any order.
curl -X POST https://api.joulo.nl/functions/v1/api/partner/sessions \
-H "Authorization: Bearer jpk_live_..." \
-H "Content-Type: application/json" \
-d '{
"sessions": [
{
"charger_id": "PF-000123",
"transaction_id": "88213",
"connector_id": 1,
"started_at": "2026-10-01T18:02:11+02:00",
"stopped_at": "2026-10-01T23:40:09+02:00",
"meter_start_wh": 1203400,
"meter_stop_wh": 1214950
}
]
}'| Field | Required | What it is |
|---|---|---|
charger_id | yes | Your charge point id, as sent at upload. |
transaction_id | yes | Your id for this session, unique per charge point. A string or a number, at most 100 characters. |
started_at, stopped_at | yes | ISO 8601 with a time zone: Z or an offset such as +02:00. |
meter_start_wh, meter_stop_wh | yes | The register of the MID meter at start and stop, in Wh: meterStart and meterStop from StartTransaction and StopTransaction. |
connector_id | no | The connector the session ran on. |
kwh | no | When you send it, it must match the meter range within 0.1 kWh. |
We book the meter range, never a number next to it. The verifier asks for the source data of the MID meter per session: transaction id, start, stop and the charge point. That is why the meter readings are required. We keep every session you send as evidence, also after a correction.
A time without a zone is rejected with naive_timestamp. We never guess the
zone: a wrong guess moves every session an hour or two.
The answer
{
"ok": true,
"received": 2,
"inserted": 1,
"updated": 0,
"unchanged": 0,
"rejected": 1,
"failed": 0,
"results": [
{ "charger_id": "PF-000123", "transaction_id": "88213", "status": "inserted" },
{
"charger_id": "PF-000999",
"transaction_id": "1204",
"status": "rejected",
"reason": "unknown_charger",
"detail": "No active laadstation with this charger_id for your customers. Push sessions once the customer finished onboarding."
}
]
}results holds one entry per session, in request order. A rejected session
never fails the request.
status | Meaning |
|---|---|
inserted | A new session, now booked. |
updated | We held this session and took your new values. |
unchanged | We already held this session with these values. |
rejected | We did not book it. reason says why. |
failed | We could not store it. Send it again. |
Resending and corrections
transaction_id is the key. Resending is always safe:
- The same values:
unchanged. Resend a whole month if that is easier. - More energy or a later stop:
updated. We raise the booking. - Less energy: rejected with
lower_kwh. ERE may already be sold on the old value, so a correction downward goes through us. - Another start: rejected with
started_at_changed. A session is booked on its start, so a new start would count it twice. Contact us.
Rejection reasons
reason | Meaning |
|---|---|
invalid | A required field is missing or cannot be read. detail names it. |
naive_timestamp | A time without a zone. |
no_energy | The meter did not move. |
before_2026 | The session started before 1 January 2026. |
in_future | stopped_at lies in the future. |
meter_range_invalid | meter_stop_wh is lower than meter_start_wh. A replaced or reset meter goes through us. |
kwh_meter_mismatch | kwh differs from the meter range by more than 0.1 kWh. |
implausible | More than 25 kW on average: check the readings and the times. |
unknown_charger | No active laadstation with this charger_id for your customers. Usually the customer has not finished onboarding. |
started_at_changed | This transaction_id is booked with another start. |
lower_kwh | This transaction_id is booked with more energy. |
duplicate_start | Another transaction_id on this laadstation already holds this session. |
Not every session earns ERE
We book every session you send. Later we decide per session whether it counts, with the same rules as for every customer:
- the charger has a MID meter, and the session started after it was installed;
- the customer signed the machtiging, and the session falls inside it;
- the customer confirmed that the charger sits behind their EAN, and we approved the registration.
The ERE position in GET /partner/ere-positions
shows the result per quarter. A session from before the MID meter never counts,
also not when you send it later.
Errors
| HTTP | error |
|---|---|
400 | Invalid JSON body, sessions_required, too_many_sessions |
403 | session_push_not_enabled |
500 | lookup_failed, evidence_failed. Send the same request again: it is idempotent. |
On the sandbox
The sandbox works the same. Mint your sandbox key in the partner portal after
we enabled you. The key carries the same permission, and GET /partner/me
shows it as session_push_source. Then upload a customer with a charge point, finish the
onboarding with the sandbox helpers, and push sessions.
Nothing reaches the NEa.