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

OCPI Commands: START_SESSION, STOP_SESSION and Async Results

How OCPI 2.2.1 commands work: START_SESSION, STOP_SESSION, RESERVE_NOW, UNLOCK_CONNECTOR, CommandResponse vs CommandResult, and timeouts.

HashTech Engineering · October 26, 2026 · 5 min read

The Commands module lets an eMSP act on a CPO's chargers: start a charge from an app, stop it, reserve an EVSE, or unlock a stuck cable. It is the module that most directly touches hardware, and it is asynchronous by design, because the CPO has to relay each command to a charger over OCPP and wait for the answer. Understanding the two-phase response is the key to implementing it correctly on either side.

This is part 8 of the OCPI 2.2.1 series. The OCPP side of these operations is covered in OCPP 1.6 remote start and stop.

The commands

CommandTypeRequest objectTypical OCPP 1.6 equivalent
START_SESSIONStartSessionRemoteStartTransaction
STOP_SESSIONStopSessionRemoteStopTransaction
RESERVE_NOWReserveNowReserveNow
CANCEL_RESERVATIONCancelReservationCancelReservation
UNLOCK_CONNECTORUnlockConnectorUnlockConnector

The CPO implements the Commands Receiver at {commands_url}/{command}. The eMSP implements the Sender side, which is really just an endpoint to receive results: every command includes a response_url, and the CPO posts the final result there.

Two phases: CommandResponse and CommandResult

  1. The eMSP POSTs the command. The CPO replies synchronously, within the HTTP request, with a CommandResponse. This only says whether the CPO accepted the command for processing.
  2. The CPO forwards the command to the charger. When the charger answers, or a timeout passes, the CPO POSTs a CommandResult to the response_url.
ObjectEnumValues
CommandResponse (synchronous)CommandResponseTypeACCEPTED, NOT_SUPPORTED, REJECTED, UNKNOWN_SESSION
CommandResult (asynchronous)CommandResultTypeACCEPTED, CANCELED_RESERVATION, EVSE_OCCUPIED, EVSE_INOPERATIVE, FAILED, NOT_SUPPORTED, REJECTED, TIMEOUT, UNKNOWN_RESERVATION

The names are similar enough that people mix them up. CommandResponseType is the CPO's immediate reaction to the HTTP request; CommandResultType is what the charger did.

START_SESSION

http
POST /ocpi/cpo/2.2.1/commands/START_SESSION HTTP/1.1
Host: cpo.example.com
Authorization: Token OWU4MGE1OWMtNmEyZi00YjVkLThlMWMtMmYzYTRiNWM2ZDdl
Content-Type: application/json
X-Request-ID: c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f
X-Correlation-ID: c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f
json
{
  "response_url": "https://emsp.example.com/ocpi/emsp/2.2.1/commands/START_SESSION/7d1e5a",
  "token": {
    "country_code": "NL",
    "party_id": "EXA",
    "uid": "APP-5f2c8e",
    "type": "APP_USER",
    "contract_id": "NL-EXA-C12345678-X",
    "issuer": "Example eMSP",
    "valid": true,
    "whitelist": "ALLOWED",
    "last_updated": "2026-10-26T10:59:58Z"
  },
  "location_id": "LOC-0042",
  "evse_uid": "3256",
  "connector_id": "1",
  "authorization_reference": "APPSTART-7d1e5a"
}

The token is a full Token object, so the CPO does not need to have synced it beforehand; a CPO should accept an app user token it has never seen. evse_uid is optional in the object, but most CPOs need it to know which charger to talk to. connector_id is optional too, unless the EVSE advertises the START_SESSION_CONNECTOR_REQUIRED capability. If the eMSP supplies an authorization_reference, the CPO must copy it into the resulting Session and CDR, exactly as with real-time authorization in Part 5.

The synchronous response:

json
{
  "data": {
    "result": "ACCEPTED",
    "timeout": 30
  },
  "status_code": 1000,
  "timestamp": "2026-10-26T11:00:00Z"
}

timeout is in seconds and tells the eMSP how long the CPO expects to need before the result arrives. Use it to drive the spinner in the app and as the eMSP's own deadline. An optional message (a list of DisplayText) can explain a rejection.

When the charger answers, the CPO posts the result:

http
POST /ocpi/emsp/2.2.1/commands/START_SESSION/7d1e5a HTTP/1.1
Host: emsp.example.com
Authorization: Token ZWJmM2IzOTktNzc5Zi00NDk3LTliOWQtYWM2YWQzY2M0NGQy
Content-Type: application/json
X-Request-ID: e5f6a7b8-c9d0-4e1f-a2b3-c4d5e6f7a8b9
X-Correlation-ID: c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f
json
{
  "result": "ACCEPTED"
}

Correlating the session

The eMSP needs to know which incoming Session belongs to which START_SESSION. authorization_reference is the reliable link: the eMSP generates it, the CPO copies it into the Session. Matching by token UID and EVSE alone breaks when the same app user starts two sessions or retries quickly.

STOP_SESSION

json
{
  "response_url": "https://emsp.example.com/ocpi/emsp/2.2.1/commands/STOP_SESSION/9a3f11",
  "session_id": "SESS-000123"
}

If the CPO does not know the session, it answers synchronously with UNKNOWN_SESSION. Otherwise it sends the stop to the charger and reports the outcome to response_url. As with start, the definitive signal is the Session moving to COMPLETED, followed by a CDR.

RESERVE_NOW and CANCEL_RESERVATION

json
{
  "response_url": "https://emsp.example.com/ocpi/emsp/2.2.1/commands/RESERVE_NOW/r-2201",
  "token": { "country_code": "NL", "party_id": "EXA", "uid": "APP-5f2c8e", "type": "APP_USER", "contract_id": "NL-EXA-C12345678-X", "issuer": "Example eMSP", "valid": true, "whitelist": "ALLOWED", "last_updated": "2026-10-26T10:59:58Z" },
  "expiry_date": "2026-10-26T11:30:00Z",
  "reservation_id": "r-2201",
  "location_id": "LOC-0042",
  "evse_uid": "3256",
  "authorization_reference": "RES-r-2201"
}

reservation_id is chosen by the eMSP. Sending RESERVE_NOW again with the same reservation_id updates the existing reservation. CANCEL_RESERVATION carries only response_url and reservation_id; the async results CANCELED_RESERVATION and UNKNOWN_RESERVATION belong to these flows. Only EVSEs with the RESERVABLE capability should receive reservations. OCPP 1.6 identifies reservations with an integer reservationId, so a CPO has to map the eMSP's string ID to one of its own.

UNLOCK_CONNECTOR

json
{
  "response_url": "https://emsp.example.com/ocpi/emsp/2.2.1/commands/UNLOCK_CONNECTOR/u-88",
  "location_id": "LOC-0042",
  "evse_uid": "3256",
  "connector_id": "1"
}

Unlock is a support tool for a cable stuck in a socket. It only makes sense on connectors with format SOCKET, and on EVSEs advertising UNLOCK_CAPABLE. Many CPOs restrict it to cases where there is no active session, because unlocking mid-charge on some hardware ends the session abruptly.

Timeouts and failure handling

  • CPO side: if the charger does not answer within your own deadline (or is offline), post TIMEOUT or FAILED to response_url. Never leave a command without a result; the eMSP's app is waiting.
  • Respect your own timeout. If you returned timeout: 30, make sure a result is posted in roughly that time. OCPP call timeouts on the CSMS side should be shorter than the OCPI timeout you advertised.
  • eMSP side: if no result arrives within timeout (plus some margin), treat the command as failed in the app, but keep accepting a late result and a late Session; chargers on slow cellular links do answer after the deadline.
  • Synchronous rejection. Return REJECTED for EVSEs you know are out of order or offline, rather than accepting and then timing out. It gives the driver an answer in one round trip.
  • Result endpoint authentication. The response_url is on the eMSP's side and is called with the token the eMSP issued (token B or C, depending on who registered). Some eMSPs embed a one-time ID in the URL; do not modify it.

Mapping charger answers to results

OCPP outcomeCommandResultType
RemoteStartTransaction.conf status AcceptedACCEPTED
RemoteStartTransaction.conf status RejectedREJECTED
Charger offline / no responseTIMEOUT (or FAILED if you know it is offline before sending)
EVSE occupied by another sessionEVSE_OCCUPIED
EVSE faulted or unavailableEVSE_INOPERATIVE
CALLERROR NotImplemented / NotSupportedNOT_SUPPORTED

The full translation layer, including how the remote start's resulting transaction becomes an OCPI Session, is in Part 10.

Key takeaways

  • Commands are two-phase: synchronous CommandResponse (CommandResponseType) then asynchronous CommandResult (CommandResultType) to response_url.
  • A START_SESSION ACCEPTED result means the charger accepted, not that charging started; the Session module is the source of truth.
  • Link commands to sessions with authorization_reference.
  • Always post a result, including TIMEOUT, and keep the advertised timeout honest.

Next: Part 9, Charging Profiles and Hub Client Info.

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.