OCPI Locations Module Explained: Data Model & Push Updates

The OCPI Locations module explained: the Location/EVSE/Connector data model, push updates, evse_id format, and the gotchas that get objects rejected.

A row of EV charging stations at night, representing the network of stations OCPI Locations exposes
Photo by Stephen Mease on Unsplash

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.

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, meaning 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

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 a multi-hundred-stall hub.

What fields each level carries

Location (the site)

Key fields:

  • id, country_code, party_id: together, the unique identifier across OCPI
  • name: human-readable name
  • address, city, postal_code, country: physical address
  • coordinates (latitude/longitude), used for map placement
  • directions: multilingual driving directions
  • operator (CPO contact info)
  • suboperator (if applicable)
  • owner (if different from operator)
  • facilities: amenities nearby (cafe, restroom, hotel, etc.)
  • time_zone: IANA timezone identifier
  • opening_times: when the site is accessible (24/7? business hours?)
  • charging_when_closed: boolean
  • images: photos of the station, optional
  • energy_mix: information about how clean the electricity is (optional)
  • last_updated: timestamp of last data change

EVSE (the charging point)

  • uid: unique within the Location
  • evse_id: the globally unique, shareable EVSE identifier, in eMI3 / DIN SPEC 91286 format, <country>*<party>*E*<id> (e.g. NL*TNM*E*001). The E is mandatory. The “Common implementation gotchas” section below breaks the format down character by character
  • 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 objects
  • floor_level: useful for parking garages
  • coordinates: EVSE-level coordinates if different from Location
  • physical_reference: e.g., the stall number painted on the ground
  • directions: local directions (e.g., “by the cafe entrance”)
  • parking_restrictions: EV_ONLY, PLUGGED, DISABLED, CUSTOMERS, MOTORCYCLES
  • images: photos of the specific EVSE
  • last_updated

Connector (the physical plug)

  • id: unique within the EVSE
  • standard: connector type, an OCPI ConnectorType enum, including IEC_62196_T1 (Type 1 / J1772), IEC_62196_T2 (Type 2 / Mennekes), IEC_62196_T2_COMBO (CCS2), IEC_62196_T1_COMBO (CCS1), CHADEMO, and TESLA_S. If the physical differences behind those enum values are fuzzy, EV charging connectors compared maps each one to the plug it describes
  • 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, DC
  • max_voltage: V
  • max_amperage: A
  • max_electric_power: W (the kW capability)
  • tariff_ids: references to applicable Tariff records
  • terms_and_conditions: URL to T&Cs for using this connector
  • last_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 sides learn each other’s base URLs and tokens during the Credentials and Versions handshake before any Location moves.

Both directions exist because OCPI does real-time updates via push. The CPO doesn’t wait for the eMSP to poll; it pushes status changes as they happen. For when each direction is the better choice, see OCPI pull vs push (coming soon).

Push update patterns (the real-time part)

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, never 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, and it 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, so 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. What push buys you is structural rather than numeric: the gap between a state change and the receiver learning about it is one HTTP round trip, not one polling interval, so the ceiling is set by how fast the receiver answers rather than by how often it thinks to ask.

Where a hard obligation does come from is regulation, not the protocol. In the EU, AFIR requires operators of publicly accessible recharging points to make static data (location, connector type, rated power) and dynamic data (availability, price) available through national access points, with the refresh expectations set by that regime rather than by OCPI. Push over OCPI is one way to feed it; AFIR compliance and OCPI in Europe (coming soon) maps the obligation onto the modules. Outside the EU there is no equivalent statutory clock, which means freshness there is a commercial term in the roaming contract rather than a legal one, and it is worth reading before you sign.

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

Those are the parameters the spec defines. Anything beyond them is a vendor extension, so a client that silently depends on one will get a full unfiltered dump from the next partner it connects to.

A typical eMSP integration pattern:

  1. Initial bulk GET to populate the database
  2. Push updates handle incremental changes
  3. 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 pushes a Location, the eMSP reads it back, and the wire-level details that trip people up are 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", followed by 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". That
      // is the ID-type, and "E" means EVSE. Drop it, or use S (station) or
      // 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), so always read the
  // envelope and never trust the HTTP status alone. 2xxx = client error,
  // 3xxx = server.
  "status_code": 1000,
  "status_message": "Success",
  "timestamp": "2026-07-03T09:00:01Z"
}

The full range of envelope codes, and what each one usually means about your request, is covered in OCPI error responses explained (coming soon).

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, so 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, so 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, and 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 wider catalogue of integration failures lives in OCPI integration pitfalls.)

The evse_id format, character by character

A malformed evse_id is one of the easiest rejects to trigger, because receivers validate the field against a fixed scheme and the scheme hinges on a single character that is easy to drop. 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 exactly E (for EVSE). It sits right after the party ID, i.e. it is the 6th character. This is the character people drop or get wrong. S designates a whole charge station and P a 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. Another well-formed 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, and OCPI vs OCPP vs ISO 15118 shows 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. Send a PATCH that carries a new status but the same timestamp, and a receiver comparing timestamps concludes nothing changed and keeps serving its stale copy. The write succeeds, the HTTP status says 200, and the data is wrong. Bump the timestamp on every write.

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 against that same object. The eMSP never rebuilds the record; it merges into it.

So when a driver opens an app and sees “8 of 10 chargers available” at a station, that number isn’t a live query to the CPO. It is the eMSP’s own mirror of the Location, assembled from whatever stream of EVSE-level PATCH messages has arrived so far. Which is exactly why a forgotten last_updated bump surfaces to the driver as a charger that isn’t really there.

What we cover in the next module deep-dive

One thing worth carrying into the other modules: Locations is the only one where the receiver holds a full mirror of the sender’s data and has to keep it correct indefinitely. Sessions and CDRs are append-only event streams, so a dropped message leaves a visible hole you can reconcile against. A dropped Locations PATCH leaves a row that is present, plausible, and wrong, and nothing downstream flags it. That asymmetry is why the periodic full-sync GET earns its keep even in a well-behaved push integration.

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.

Quick check

Q1. In the OCPI Locations module, who is the Sender?

Frequently asked questions

What does the OCPI Locations module do?

It defines how CPOs publish charging station data (location, connectors, status, capabilities) and how eMSPs and hubs consume it. It is the largest module in the specification, and the one an eMSP integration cannot function without.

How is real-time station status communicated?

CPOs push EVSE status updates to eMSPs with PATCH requests against the Locations endpoint. OCPI calls this "semi real-time" and sets no numeric latency target, so the delay is the round trip of one HTTP call rather than a polling interval.

Found this useful? Share it.