The Locations module is the largest and most central module in OCPI. It’s how a CPO publishes everything an eMSP needs to know about charging stations: where they are, what they offer, and what state they’re in right now.
If you only implement one OCPI module, it’s Locations. This article is the deep dive.
What “Locations” actually contains
In OCPI’s vocabulary, a Location is one physical site. The site contains one or more EVSEs (Electric Vehicle Supply Equipment — individual charging points). Each EVSE has one or more Connectors (physical plugs).
The hierarchy:
flowchart LR
Loc[Location: highway service area] --> E1[EVSE 1: charging post A]
Loc --> E2[EVSE 2: charging post B]
Loc --> E3[EVSE 3: charging post C]
E1 --> C1[Connector: CCS]
E1 --> C2[Connector: CHAdeMO]
E2 --> C3[Connector: CCS]
E2 --> C4[Connector: CHAdeMO]
E3 --> C5[Connector: CCS]
style Loc fill:#dbeafe,stroke:#2563eb
Concretely:
A typical highway service station might have:
- 1 Location
- 8 EVSEs (e.g., 8 fast charging stalls)
- 16 Connectors (each EVSE has both a CCS and CHAdeMO plug, say)
Or a small parking lot:
- 1 Location
- 2 EVSEs
- 2 Connectors (1 J1772 each, AC only)
Or a complex hub:
- 1 Location
- 20+ EVSEs of mixed AC and DC types
The flexibility lets the model fit anything from a single home charger up to massive multi-hundred-stall hubs.
What fields each level carries
Location (the site)
Key fields:
id,country_code,party_id— the unique identifier across OCPIname— human-readable nameaddress,city,postal_code,country— physical addresscoordinates(latitude/longitude) — for map placementdirections— multilingual driving directionsoperator(CPO contact info)suboperator(if applicable)owner(if different from operator)facilities— amenities nearby (cafe, restroom, hotel, etc.)time_zone— IANA timezone identifieropening_times— when the site is accessible (24/7? business hours?)charging_when_closed— booleanimages— photos of the station, optionalenergy_mix— info about how clean the electricity is (optional)last_updated— timestamp of last data change
EVSE (the charging point)
uid— unique within the Locationevse_id— the globally unique, shareable EVSE identifier, in eMI3 / DIN SPEC 91286 format:<country>*<party>*E*<id>(e.g.NL*TNM*E*001). TheEis mandatory — this is the field people get wrong most often (the “Common implementation gotchas” section below breaks down the format)status— current operational state (AVAILABLE, BLOCKED, CHARGING, INOPERATIVE, OUTOFORDER, PLANNED, REMOVED, RESERVED, UNKNOWN)status_schedule— planned future status changes (e.g., maintenance windows)capabilities— what features the EVSE supports (RFID_READER, REMOTE_START_STOP_CAPABLE, CREDIT_CARD_PAYABLE, etc.)connectors— array of Connector objectsfloor_level— useful for parking garagescoordinates— EVSE-level coordinates if different from Locationphysical_reference— e.g., the stall number painted on the grounddirections— local directions (e.g., “by the cafe entrance”)parking_restrictions— EV_ONLY, PLUGGED, DISABLED, CUSTOMERS, MOTORCYCLESimages— photos of the specific EVSElast_updated
Connector (the physical plug)
id— unique within the EVSEstandard— connector type, an OCPIConnectorTypeenum:IEC_62196_T1(Type 1 / J1772),IEC_62196_T2(Type 2 / Mennekes),IEC_62196_T2_COMBO(CCS2),IEC_62196_T1_COMBO(CCS1),CHADEMO,TESLA_S, etc.format— CABLE or SOCKET (does the station have an attached cable, or do you bring your own?)power_type— AC_1_PHASE, AC_3_PHASE, DCmax_voltage— Vmax_amperage— Amax_electric_power— W (the kW capability)tariff_ids— references to applicable Tariff recordsterms_and_conditions— URL to T&Cs for using this connectorlast_updated
The Sender / Receiver pattern
OCPI uses the Sender / Receiver pattern across all modules. For Locations:
- Sender = CPO. They own the data and decide when it changes.
- Receiver = eMSP or Hub. They consume the data.
Each side implements specific endpoints. The CPO exposes a “Sender Interface” that the eMSP can poll (with GET requests) for current data. The eMSP exposes a “Receiver Interface” that the CPO can push updates to (with PATCH and PUT requests) when data changes.
Both directions exist because OCPI does real-time updates via push. The CPO doesn’t wait for the eMSP to poll — it actively pushes status changes the moment they happen.
Push update patterns (the real-time part)
Here’s where Locations gets interesting. There are three patch granularities:
Full Location update (PUT)
The CPO sends a complete Location object with all nested EVSEs and Connectors. Used when a Location is created or when sweeping changes occur. The body is the same complete Location object shown field-by-field in the worked example below; only the URL and HTTP verb differ:
PUT /ocpi/emsp/2.2.1/locations/NL/ABC/LOC1
Authorization: Token cpo-credentials-token
# Body = the full Location object (country_code, party_id, id, publish, address,
# city, country, coordinates, evses[], time_zone, last_updated, plus any optional
# fields). See the complete, populated object in the worked example below.
EVSE-level update (PATCH or PUT)
Just one EVSE within a Location is updated. The CPO doesn’t need to resend the whole Location.
PATCH /ocpi/emsp/2.2.1/locations/NL/ABC/LOC1/EVSE1
Authorization: Token cpo-credentials-token
{
"status": "CHARGING",
"last_updated": "2026-09-17T12:34:56Z"
}
Connector-level update (PATCH)
Even more granular: just a connector. Note that a Connector has no status field in OCPI — operational state lives on the EVSE — so a connector-level PATCH updates connector attributes like tariff_ids, max_electric_power, or terms_and_conditions, not availability.
PATCH /ocpi/emsp/2.2.1/locations/NL/ABC/LOC1/EVSE1/CONN1
Authorization: Token cpo-credentials-token
{
"tariff_ids": ["kwh-rate-2026-q4"],
"last_updated": "2026-09-17T12:34:56Z"
}
The PATCH model means the CPO sends only what changed, and the eMSP merges into their local copy.
Status values and what they mean
The EVSE status field is the heart of the real-time pattern. The valid values:
- AVAILABLE — ready to use, no one charging
- BLOCKED — physically blocked (someone parked in front, blocked by ice, etc.)
- CHARGING — currently in use
- INOPERATIVE — temporarily not operating (e.g., software issue), but expected to recover
- OUTOFORDER — broken; needs technician intervention
- PLANNED — exists in the data but not yet operational (under construction, awaiting permits)
- REMOVED — was operational but has been physically removed; kept for historical context
- RESERVED — reserved by a driver or for maintenance
- UNKNOWN — communication with the station is lost; status is uncertain
Each implies a different driver-facing UX. AVAILABLE → show as available. CHARGING → show as in-use. OUTOFORDER → show as broken. UNKNOWN → show with a warning (“status unknown, may not be available”).
Real-time expectations
OCPI itself doesn’t put a number on latency — the spec describes the Push model as “semi real-time” and leaves timing to the implementation. In practice, well-run integrations propagate a status change to receivers within a few seconds.
Where a hard number does come from is regulation, not the protocol. In the EU, AFIR requires CPOs to keep dynamic data (availability, current price) fresh within one minute of a change, and static data (location, connector type) within 24 hours. Push over OCPI is one common way CPOs meet that bar. North American operators aren’t bound by AFIR, but the same near-real-time expectation shows up in network and roaming agreements.
Receivers can also poll if they’re worried about pushed updates being missed. The GET endpoint returns current data on demand.
Filtering and pagination
For large Locations datasets, GET requests support filtering and pagination:
?date_from=— only Locations updated after this timestamp?date_to=— only updates before this timestamp?offset=and?limit=— for paged results?country_code=(in some implementations)
A typical eMSP integration pattern:
- Initial bulk GET to populate the database
- Push updates handle incremental changes
- Periodic full-sync GET (daily or weekly) as a safety net to catch missed updates
A complete worked example: CPO publishes, eMSP reads
The snippets above are trimmed for clarity. Here is the full round trip — the
CPO pushing a Location and the eMSP reading it back — with the wire-level
details that trip people up called out in the comments. (Comments use // and
# for teaching; real JSON has no comments.)
1. The CPO pushes a Location (PUT to the eMSP)
# ── CPO ──▶ eMSP ────────────────────────────────────────────────────────────
# The CPO owns the data, so it PUSHES the full object to the eMSP's Receiver
# Interface. PUT = "here is the complete, authoritative Location; replace
# whatever you hold for this id." Use PUT to create or fully overwrite; use
# PATCH when only one field changed (e.g. just an EVSE status).
PUT /ocpi/emsp/2.2.1/locations/NL/TNM/LOC1 HTTP/1.1
Host: emsp.example.com
# Auth scheme is the literal word "Token" — NOT "Bearer" — then the credentials
# token the eMSP issued to THIS CPO during the Credentials handshake.
Authorization: Token <token-the-eMSP-issued-to-this-CPO>
Content-Type: application/json
# Recommended so both sides can trace one call across their logs; echo them back.
X-Request-ID: 12345
X-Correlation-ID: 67890
# The URL path carries {country_code}/{party_id}/{location_id}. They MUST equal
# the country_code / party_id / id in the body — a mismatch is a common reject.
{
"country_code": "NL", // ISO 3166-1 alpha-2 of the CPO (2 chars)
"party_id": "TNM", // the CPO's party id (3 chars)
"id": "LOC1", // Location id, unique within this CPO
"publish": true, // 2.2.1: is this Location visible to drivers?
"name": "Amsterdam Central Garage",
"address": "Stationsplein 1",
"city": "Amsterdam",
"postal_code": "1012 AB",
"country": "NLD", // gotcha: this one is alpha-3 (3 letters)
"coordinates": { // decimal degrees, sent as STRINGS in OCPI
"latitude": "52.379189",
"longitude": "4.899431"
},
"evses": [
{
"uid": "3256", // stable + unique WITHIN this Location. Internal.
// evse_id = the GLOBAL, shareable id (eMI3 / DIN SPEC 91286):
// <country(2)> <party(3)> E <power-outlet-id>
// The 6th character (right after the 3-char party id) MUST be "E" — it is
// the ID-type and "E" = EVSE. Drop it, or use S (station) / P (pool), and
// conformant receivers reject the object. The "*" separators are optional.
"evse_id": "NL*TNM*E*001", // identical to "NLTNME001" without separators
"status": "AVAILABLE", // AVAILABLE | CHARGING | BLOCKED | INOPERATIVE | ...
"connectors": [
{
"id": "1", // unique within this EVSE
"standard": "IEC_62196_T2", // OCPI enum for a Type 2 connector
"format": "SOCKET", // SOCKET = bring your cable; CABLE = attached
"power_type": "AC_3_PHASE",
"max_voltage": 400,
"max_amperage": 32,
"max_electric_power": 22000, // watts → 22 kW
"last_updated": "2026-07-03T09:00:00Z"
}
],
"last_updated": "2026-07-03T09:00:00Z"
}
],
"time_zone": "Europe/Amsterdam", // required — IANA tz id; drives opening_times
// RFC 3339, UTC ("Z"). Receivers use last_updated to detect real changes and
// to resolve out-of-order updates (newest wins). Forget to bump it on a change
// and downstream caches silently keep stale data — a classic, painful bug.
"last_updated": "2026-07-03T09:00:00Z"
}
# ── eMSP ──▶ CPO (the response) ─────────────────────────────────────────────
HTTP/1.1 200 OK
Content-Type: application/json
X-Request-ID: 12345
X-Correlation-ID: 67890
{
"data": {}, // for a PUT, receivers usually echo nothing
// THE key OCPI gotcha: HTTP 200 only means the HTTP call arrived. You must
// ALSO check status_code INSIDE the envelope. 1000 = success. You can get
// HTTP 200 with status_code 2001 (invalid parameters) — always read the
// envelope, never trust the HTTP status alone. 2xxx = client error, 3xxx = server.
"status_code": 1000,
"status_message": "Success",
"timestamp": "2026-07-03T09:00:01Z"
}
2. The eMSP reads Locations back (GET from the CPO)
# ── eMSP ──▶ CPO ────────────────────────────────────────────────────────────
# The eMSP reads from the CPO's Sender Interface. Typical flow: one bulk GET to
# seed the database (below), then rely on the CPO's pushed PUT/PATCH updates for
# changes. The list endpoint is paginated — follow the pages or you only ever
# see the first slice.
GET /ocpi/cpo/2.2.1/locations?date_from=2026-07-01T00:00:00Z&offset=0&limit=50 HTTP/1.1
Host: cpo.example.com
# Opposite direction, opposite token: the one the CPO issued to the eMSP.
Authorization: Token <token-the-CPO-issued-to-this-eMSP>
# Query params:
# date_from / date_to → only objects changed in this window (delta sync)
# offset / limit → paging; the CPO may cap limit — read the real one back
# from the response headers below.
# ── CPO ──▶ eMSP (the response) ─────────────────────────────────────────────
HTTP/1.1 200 OK
Content-Type: application/json
# Pagination lives in HEADERS, not the body:
# Link → URL of the NEXT page; keep following until there is no "next"
# X-Total-Count → total objects matching the filter, across ALL pages
# X-Limit → the page size the CPO actually applied (can be < your limit)
Link: <https://cpo.example.com/ocpi/cpo/2.2.1/locations?date_from=2026-07-01T00:00:00Z&offset=50&limit=50>; rel="next"
X-Total-Count: 137
X-Limit: 50
{
"data": [ // an ARRAY of Location objects (this page only)
{
"country_code": "NL", // required
"party_id": "TNM", // required
"id": "LOC1", // required
"publish": true, // required
"name": "Amsterdam Central Garage", // optional
"address": "Stationsplein 1", // required
"city": "Amsterdam", // required
"postal_code": "1012 AB", // optional
"country": "NLD", // required — alpha-3 (3 letters)
"coordinates": { "latitude": "52.379189", "longitude": "4.899431" }, // required
"evses": [
{
"uid": "3256",
"evse_id": "NL*TNM*E*001", // same well-formed id — note the mandatory "E"
"status": "AVAILABLE",
"connectors": [ // the same connector, shown in full (no truncation)
{
"id": "1", // required — unique within this EVSE
"standard": "IEC_62196_T2", // required — OCPI enum for a Type 2 connector
"format": "SOCKET", // required — SOCKET = bring your cable; CABLE = attached
"power_type": "AC_3_PHASE", // required — AC_1_PHASE | AC_3_PHASE | DC | ...
"max_voltage": 400, // required — volts
"max_amperage": 32, // required — amps
"max_electric_power": 22000, // optional — watts → 22 kW
"last_updated": "2026-07-03T09:00:00Z" // required — RFC 3339 UTC
}
],
"last_updated": "2026-07-03T09:00:00Z"
}
],
"time_zone": "Europe/Amsterdam", // required — IANA tz id
"last_updated": "2026-07-03T09:00:00Z"
}
// … up to `limit` Locations, then follow the Link header for the next page.
],
"status_code": 1000, // again: check this, not just the HTTP 200
"status_message": "Success",
"timestamp": "2026-07-03T09:05:00Z"
}
Common implementation gotchas
A few things that catch real-world implementations:
The evse_id format — the #1 mistake
The single most common reject is a malformed evse_id. It must follow the
eMI3 / DIN SPEC 91286 EVSE ID scheme:
<country code> <party / operator ID> E <power outlet ID>
NL TNM E 001 → NL*TNM*E*001
- Country code — 2 characters. The first part of the ID.
- Party / operator ID — 3 characters. Together with the country code, these make up the first 5 characters of the ID.
E— the ID type, and it must be exactlyE(for EVSE). It sits right after the party ID — i.e. the 6th character. This is the character people drop or get wrong.Sdesignates a whole charge station andPa charge pool; using one of those, omitting the type entirely, or confusing it with a connector id all get the Location rejected.- Power outlet ID — 1–30 alphanumeric characters the CPO assigns.
The * separators are optional — NL*TNM*E*001 and NLTNME001 are equivalent —
but the field order and the E type are not. A real-world example is
FRA23E45B78C (country FR + operator A23 + type E + charge point 45B78C).
For the full syntax — including the contract-ID, charge-pool, and station variants — see the EVRoaming Foundation’s ID format and syntax whitepaper.
Connector vs EVSE confusion
People conflate the two. An EVSE is the charging point (with its own controller, screen, cable). A Connector is the specific plug on the EVSE. One EVSE can have multiple Connectors (e.g., a CCS + a CHAdeMO on the same charging post). Status applies at the EVSE level, not the Connector level — when an EVSE is CHARGING, all its Connectors are effectively in use. (The status the CPO reports over OCPI here typically originates from OCPP messages the station sends to the CPO’s backend — see OCPI vs OCPP vs ISO 15118 for how those protocols divide the work.)
Status of CHARGING means “session in progress”
It doesn’t mean “currently transferring electricity right now.” A session that’s plugged in but waiting for authentication still reads as CHARGING for OCPI purposes. The driver-facing UX should differentiate but the protocol field doesn’t.
last_updated is critical
Receivers use last_updated to detect when data has actually changed. Some implementations forget to bump this on every update, which causes downstream caches to miss the change.
Patches must be applied correctly
A PATCH request only contains changed fields. The receiver must merge with its existing data, not replace. Implementations that treat PATCH like PUT cause data loss.
Coordinates accuracy
Approximate coordinates (e.g., the street address centroid) lead to bad driver experiences (“the GPS says I’m here but I can’t find the chargers”). Submit precise coordinates — at the EVSE level if the site is large.
What it looks like in practice
The CPO publishes a Location once. As reality changes — sessions start and stop, stations go down for maintenance, prices update, new EVSEs come online — they push targeted updates. The eMSP’s app database stays current within seconds of reality.
When a driver opens their app and sees “8 of 10 chargers available” at a station, that’s the result of dozens of EVSE-level PATCH messages flowing through OCPI in the last few minutes.
What we cover in the next module deep-dive
- OCPI Tariffs Module — how pricing data flows from CPO to eMSP
- OCPI Sessions Module — live charging session data
- OCPI CDRs Module — final billing records
- OCPI Tokens Module — driver identity validation
For the wire-level details of every Locations field, see the OCPI 2.2.1 specification directly. To see how the Locations model has changed across releases — including newer fields like the publish flag used in the worked example above — check the OCPI version history.