OCPI Locations Module Explained: Data Model & Push Updates

The OCPI Locations module, explained: the Location/EVSE/Connector data model, real-time push updates, evse_id format, and gotchas — with worked examples.

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. 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 OCPI
  • name — human-readable name
  • address, city, postal_code, country — physical address
  • coordinates (latitude/longitude) — 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 — info 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 — 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 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: 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, 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 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:

  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 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 exactly E (for EVSE). It sits right after the party ID — i.e. 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. 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

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/hubs consume it. It's the largest and most-used module in OCPI.

How is real-time station status communicated?

CPOs push EVSE status updates to eMSPs via PATCH requests to the Locations endpoint. OCPI calls this "semi real-time" and sets no numeric latency; in practice updates flow within seconds.

Found this useful? Share it.