PHP2Shell alert: critical PHP flaws CVE-2026-109618 / 109619 — check your servers and get patched fast
HashTech Systems
EV Charging
Part 4 of 10

OCPI Locations Module: Locations, EVSEs, Connectors and Status

Modelling Location, EVSE and Connector in OCPI 2.2.1, pushing status with PUT and PATCH, evse_id format, and how eMSPs should pull and sync.

HashTech Engineering · October 26, 2026 · 6 min read

Locations is the module everyone needs first. It tells eMSPs, navigation apps and hubs where your chargers are, what connectors they have, which tariffs apply and whether they are free right now. It is also the module with the highest message volume, because every status change on every EVSE is a potential update.

This is part 4 of the OCPI 2.2.1 series. Transport conventions such as pagination and last_updated are explained in Part 3.

The three-level model

OCPI models a site as a hierarchy:

  • Location: a site with an address and coordinates, such as a car park or a forecourt. Owns operator details, opening times, facilities and time zone.
  • EVSE: one point that can charge one vehicle at a time. This is the level status lives at.
  • Connector: a physical plug or socket on an EVSE. An EVSE with a CCS cable and a CHAdeMO cable has two connectors, but can still only charge one car at a time.

This maps reasonably well to OCPP 2.0.1's Charging Station / EVSE / Connector model and less neatly to OCPP 1.6, where a "connector" is often what OCPI would call an EVSE. Part 10 covers that mapping in detail.

A complete example

json
{
  "country_code": "DE",
  "party_id": "CPX",
  "id": "LOC-0042",
  "publish": true,
  "name": "Riverside Car Park",
  "address": "Hafenstrasse 12",
  "city": "Hamburg",
  "postal_code": "20457",
  "country": "DEU",
  "coordinates": { "latitude": "53.543210", "longitude": "9.987650" },
  "parking_type": "PARKING_LOT",
  "operator": { "name": "Example CPO" },
  "time_zone": "Europe/Berlin",
  "opening_times": { "twentyfourseven": true },
  "evses": [
    {
      "uid": "3256",
      "evse_id": "DE*CPX*E0042*1",
      "status": "AVAILABLE",
      "capabilities": ["RFID_READER", "REMOTE_START_STOP_CAPABLE", "UNLOCK_CAPABLE"],
      "connectors": [
        {
          "id": "1",
          "standard": "IEC_62196_T2_COMBO",
          "format": "CABLE",
          "power_type": "DC",
          "max_voltage": 920,
          "max_amperage": 200,
          "max_electric_power": 150000,
          "tariff_ids": ["DC-STANDARD"],
          "last_updated": "2026-10-26T08:00:00Z"
        }
      ],
      "physical_reference": "1",
      "last_updated": "2026-10-26T08:00:00Z"
    }
  ],
  "last_updated": "2026-10-26T08:00:00Z"
}

Some details worth noticing:

  • country on the Location is ISO 3166-1 alpha-3 (DEU), while country_code identifying the party is alpha-2 (DE). Mixing them up is a common validation failure.
  • coordinates are strings, not numbers, with a decimal point. Some receivers validate the format strictly.
  • time_zone is required in 2.2.1 and must be an IANA name such as Europe/Berlin. Tariff restrictions and opening times are interpreted in it.
  • max_electric_power is in watts. max_voltage and max_amperage describe the connector's limits; for AC, the voltage is line-to-neutral.
  • publish: false hides the location from general listing; publish_allowed_to can then list the tokens or visual numbers that may see it (useful for private or fleet-only sites).

uid vs evse_id

Each EVSE has two identifiers, and they serve different purposes. uid is the CPO's internal, stable identifier, unique within the CPO; it is what appears in URLs, sessions, CDRs and commands. evse_id is the public, human-facing identifier in the eMI3 format (<country>*<operator ID>*E<identifier>, e.g. DE*CPX*E0042*1) printed on the charger and used by drivers and roaming partners for lookup. evse_id is optional in the object, but in roaming you should treat it as required: many eMSPs and hubs refuse or hide EVSEs without one, and in several countries operator IDs are issued by a national registry.

Enumerations you will use constantly

FieldValues
EVSE statusAVAILABLE, BLOCKED, CHARGING, INOPERATIVE, OUTOFORDER, PLANNED, REMOVED, RESERVED, UNKNOWN
Connector formatSOCKET, CABLE
Connector power_typeAC_1_PHASE, AC_2_PHASE, AC_2_PHASE_SPLIT, AC_3_PHASE, DC
Connector standard (common)IEC_62196_T1, IEC_62196_T1_COMBO, IEC_62196_T2, IEC_62196_T2_COMBO, CHADEMO, GBT_DC, plus domestic and other types
parking_typeALONG_MOTORWAY, PARKING_GARAGE, PARKING_LOT, ON_DRIVEWAY, ON_STREET, UNDERGROUND_GARAGE

A few status semantics trip people up. BLOCKED means the EVSE cannot be used because something is physically in the way (a non-EV parked in the bay, for example), not that it is in use. INOPERATIVE means temporarily unavailable but not broken (maintenance, administratively disabled), while OUTOFORDER means broken. REMOVED is how you delete an EVSE: OCPI has no DELETE for locations, so you set the status to REMOVED and keep the object. For a broader look at connector standards, see EV charging connector types.

CPO pushing to the eMSP

The eMSP implements the Locations Receiver interface. The CPO sends a full object with PUT when a location is created or substantially changed:

http
PUT /ocpi/emsp/2.2.1/locations/DE/CPX/LOC-0042 HTTP/1.1
Host: emsp.example.com
Authorization: Token ZWJmM2IzOTktNzc5Zi00NDk3LTliOWQtYWM2YWQzY2M0NGQy
Content-Type: application/json
X-Request-ID: 2b8f1a7c-5d4e-4c3b-9a8f-7e6d5c4b3a21
X-Correlation-ID: 2b8f1a7c-5d4e-4c3b-9a8f-7e6d5c4b3a21

For status changes, PATCH the EVSE with just the fields that changed, always including last_updated:

http
PATCH /ocpi/emsp/2.2.1/locations/DE/CPX/LOC-0042/3256 HTTP/1.1
Host: emsp.example.com
Authorization: Token ZWJmM2IzOTktNzc5Zi00NDk3LTliOWQtYWM2YWQzY2M0NGQy
Content-Type: application/json
X-Request-ID: 8d7c6b5a-4f3e-4d2c-8b1a-0f9e8d7c6b5a
X-Correlation-ID: 8d7c6b5a-4f3e-4d2c-8b1a-0f9e8d7c6b5a
json
{
  "status": "CHARGING",
  "last_updated": "2026-10-26T10:15:04Z"
}

The same URL pattern works one level down for connectors (.../LOC-0042/3256/1) and one level up for the location itself. PUT on an EVSE URL replaces that EVSE, including its connectors; PUT on a location replaces everything below it.

Ordering and stale updates

Pushes can arrive out of order, especially behind retries or hubs. A receiver should compare the incoming last_updated with what it has stored and ignore older updates. A CPO should not let a slow retry of an old CHARGING patch overwrite a newer AVAILABLE one. This is the main reason last_updated is mandatory on every PATCH.

PATCH on an unknown object

If the eMSP receives a PATCH for an EVSE or location it does not have, for example because the original PUT failed, it cannot apply it. The robust CPO behaviour is to respond to that error by sending a full PUT of the location. The robust eMSP behaviour is to schedule a GET of that location from the CPO's Sender interface.

eMSP pulling from the CPO

The CPO's Sender interface offers a paginated list and single-object lookups:

http
GET /ocpi/cpo/2.2.1/locations?date_from=2026-10-26T09:00:00Z&limit=100 HTTP/1.1
Host: cpo.example.com
Authorization: Token OWU4MGE1OWMtNmEyZi00YjVkLThlMWMtMmYzYTRiNWM2ZDdl
X-Request-ID: 1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b
X-Correlation-ID: 1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b

Single objects are fetched with GET {locations_url}/{location_id}, .../{location_id}/{evse_uid} and .../{location_id}/{evse_uid}/{connector_id}. Note that the Sender interface URLs do not include country_code/party_id: the CPO already knows they are its own.

A typical eMSP strategy is a full pull on first connection, an incremental pull with date_from every few minutes as a safety net, and pushes for real-time status. Do not rely on pulls alone for status; a charger can go from available to occupied and back between two polls.

Other fields that matter in practice

  • opening_times: either twentyfourseven: true or regular_hours per weekday, with exceptional_openings and exceptional_closings. charging_when_closed says whether a session can continue after closing time.
  • status_schedule on the EVSE: planned future status changes, e.g. scheduled maintenance.
  • capabilities: drives what eMSPs offer in their apps. If you do not list REMOTE_START_STOP_CAPABLE, expect eMSPs not to offer app start; if START_SESSION_CONNECTOR_REQUIRED is listed, the eMSP must specify a connector in START_SESSION (see Part 8).
  • tariff_ids on the connector: links to Tariff objects from Part 7. Leaving it empty while publishing tariffs makes prices impossible to display.
  • directions, images, facilities, energy_mix: optional, but navigation partners use them.

Common mistakes

  • Using the database primary key as uid and later changing it during a migration. uid must be stable forever; sessions and CDRs refer to it.
  • Publishing EVSEs that are installed but not commissioned as AVAILABLE instead of PLANNED.
  • Flooding partners with PATCH calls for meter-level noise. Push status changes, not every OCPP message. Debounce rapid flapping (a connector reporting Preparing/Available repeatedly) before it reaches OCPI.
  • Sending UNKNOWN for every EVSE whose charger is offline for a few seconds. A short grace period before switching to UNKNOWN reduces noise considerably.

Key takeaways

  • Location, EVSE and Connector form a strict hierarchy; status lives on the EVSE.
  • uid is the stable internal key used everywhere else; evse_id is the public eMI3 ID. Publish both.
  • Push with PUT for full objects and PATCH for status, always with last_updated; never delete, use REMOVED.
  • Receivers discard older updates, and eMSPs back pushes up with incremental pulls.

Next: Part 5, Tokens and real-time authorization.

Let's Talk

Ready to build something that lasts?

Whether it's a charging network, an AI rollout, or a core enterprise system — let's talk about what you're building.