OCPI MODULE · UPDATED 11 AUG 2026

Commands: remote control across company lines

When a driver taps “start charging” in one company's app and a charger owned by a different company responds, the Commands module is what crossed the gap. It is the only place in OCPI where one party reaches out and changes physical state on another party's hardware.

PART OF THE OCPI 2.2.1 COMPLETE GUIDE · 15 MODULE & COMMAND GUIDES

IN ONE PARAGRAPH

Commands is asynchronous in two phases, and that is the whole design. The first response tells you only whether the request was accepted for processing; the real outcome arrives later as a separate POST to a callback URL you supplied. Treating the first response as the result is the most common and most expensive mistake in the module.

The five commands

All five are sent by the eMSP (Sender) to the CPO (Receiver), and all five ultimately translate into an OCPP message to the charge point.

CommandWhat it doesUnderlying OCPP
START_SESSIONBegin charging at a specific EVSE for a given tokenRemoteStartTransaction / RequestStartTransaction
STOP_SESSIONEnd an in-progress session by session IDRemoteStopTransaction / RequestStopTransaction
RESERVE_NOWHold an EVSE for a token until an expiry timeReserveNow
CANCEL_RESERVATIONRelease a reservation earlyCancelReservation
UNLOCK_CONNECTORPhysically release a latched cableUnlockConnector
UNLOCK_CONNECTOR is not a convenience feature. It exists because a cable can latch and refuse to release with a driver's car attached to it. Many CPOs deliberately restrict or disable it, since unlocking a connector mid-session has obvious safety implications. Do not assume a partner supports it — expect NOT_SUPPORTED and design the driver-facing flow around that.

The two-phase pattern

Every command follows the same shape:

  1. The eMSP POSTs the command to the CPO, including a response_url it controls.
  2. The CPO replies immediately with a CommandResponse. This says only whether the command was accepted for processing, plus a timeout hint.
  3. The CPO forwards the command to the charger over OCPP and waits.
  4. When the charger responds — or fails to — the CPO POSTs a CommandResult to the response_url.

The gap between step 2 and step 4 is typically 2 to 30 seconds, and can be much longer if the charger is on a poor cellular link. That latency is why the pattern exists: an HTTP request cannot be held open for the round trip to a piece of roadside hardware.

CommandResponse — accepted for processing

ValueMeaning
ACCEPTEDQueued and forwarded. A CommandResult will follow.
REJECTEDNot processed. No result will follow.
NOT_SUPPORTEDThis CPO does not implement this command.
UNKNOWN_SESSIONFor STOP_SESSION — the session ID is not recognised.

CommandResult — what actually happened

ValueMeaning
ACCEPTEDThe charger carried out the command.
REJECTEDThe charger refused it.
CANCELED_RESERVATIONReservation successfully cancelled.
EVSE_OCCUPIEDAnother vehicle is already using it.
EVSE_INOPERATIVEThe EVSE is faulted or unavailable.
FAILEDThe command was attempted and failed.
NOT_SUPPORTEDThe charger does not support it.
TIMEOUTThe charger never answered.
UNKNOWN_RESERVATIONThe reservation ID is not recognised.

Worked example: START_SESSION end to end

The eMSP asks the CPO to start a session for one of its customers at a specific EVSE:

POST /ocpi/2.2.1/commands/START_SESSION
Authorization: Token <base64 token>
OCPI-from-country-code: IN
OCPI-from-party-id: EMS
OCPI-to-country-code: IN
OCPI-to-party-id: EFI
X-Request-ID: 7c1a4e90
X-Correlation-ID: booking-88231

{
  "response_url": "https://emsp.example.com/ocpi/2.2.1/commands/START_SESSION/7c1a4e90",
  "token": {
    "country_code": "IN", "party_id": "EMS",
    "uid": "DR-88231", "type": "APP_USER",
    "contract_id": "IN-EMS-C0142",
    "issuer": "Example Mobility Services",
    "valid": true, "whitelist": "ALLOWED",
    "last_updated": "2026-08-11T08:55:00Z"
  },
  "location_id": "LOC-ND-014",
  "evse_uid": "EVSE-014-02",
  "authorization_reference": "auth-4471"
}

The CPO answers immediately — this is not the outcome:

HTTP/1.1 200 OK

{
  "data": { "result": "ACCEPTED", "timeout": 30 },
  "status_code": 1000,
  "timestamp": "2026-08-11T09:12:01Z"
}

Eleven seconds later, after the charger has answered over OCPP, the CPO posts the real result to the callback URL:

POST /ocpi/2.2.1/commands/START_SESSION/7c1a4e90
Authorization: Token <base64 token>
X-Correlation-ID: booking-88231

{
  "result": "ACCEPTED"
}

Only now can the eMSP tell the driver charging has begun. A Session object will appear separately through the Sessions module.

The timeout field, and what to do when it passes

timeout in the CommandResponse is the number of seconds the CPO expects to need. It is a hint, not a contract, and the spec does not define what happens if it elapses without a result.

The behaviour that works in production is:

  • Start a timer at timeout plus a margin — roughly 1.5× is a reasonable default.
  • On expiry, mark the command indeterminate, not failed. The charger may still be starting.
  • Reconcile against the Sessions module rather than guessing. If a Session appears for that EVSE and token, the command succeeded regardless of whether the result ever arrived.
  • Never auto-retry START_SESSION on timeout. You risk two sessions and a very unhappy driver.
Late results are normal. A CommandResult arriving after your timeout is common with chargers on weak cellular links. Your response_url handler must accept and correctly process a result for a command you have already given up on — and must be idempotent, because retries mean you will occasionally receive the same result twice.

Correlating commands with sessions

The hardest part of this module is not sending the command — it is knowing which resulting Session belongs to which command. OCPI gives you one tool for this: authorization_reference.

If you set it on the command, a conforming CPO echoes it on the resulting Session and CDR. That gives you an end-to-end join key from the driver's tap through to the invoice. Without it you are matching on token, EVSE and timestamp proximity, which is fragile at any real volume.

Not every CPO propagates it reliably. Verify it during partner certification rather than discovering the gap during reconciliation.

Production lessons

  • Make response_url unique per command. Embedding the request ID in the path, as in the example above, means the callback identifies itself without you having to parse a body to route it.
  • Make the callback handler idempotent. Duplicate results happen. Key on the command ID and ignore repeats.
  • Never present the CommandResponse to the user. ACCEPTED at phase one means “we are trying”. Showing “charging started” at that point produces support tickets when the charger then reports EVSE_OCCUPIED.
  • Set authorization_reference on every command. It costs nothing and it is the only clean correlation key you get.
  • Treat NOT_SUPPORTED as a partner capability, not an error. Record it against the partner and stop offering that action in the UI for their locations.
  • Log both phases against one correlation ID. Commands are the most common source of partner disputes, and a single ID spanning request, response and result is what resolves them.

Frequently asked questions

What commands does OCPI 2.2.1 support?

Five: START_SESSION, STOP_SESSION, RESERVE_NOW, CANCEL_RESERVATION and UNLOCK_CONNECTOR. All are sent by the eMSP to the CPO and translate into OCPP messages to the charge point.

Why are OCPI commands asynchronous?

Because the CPO must relay the command to physical roadside hardware over OCPP and wait for it to answer, which can take seconds to minutes. An HTTP request cannot be held open that long, so OCPI splits it: an immediate CommandResponse acknowledging receipt, then a CommandResult POSTed later to a callback URL.

What is the difference between CommandResponse and CommandResult?

CommandResponse is the synchronous reply and says only whether the command was accepted for processing. CommandResult is the asynchronous callback that reports what the charger actually did. Only CommandResult is the outcome.

What happens if an OCPI command times out?

The spec does not define it. In practice you mark the command indeterminate rather than failed, reconcile against the Sessions module to see whether a session actually started, and never auto-retry START_SESSION — retrying risks starting two sessions.

How do you match an OCPI command to its resulting session?

Set authorization_reference on the command. A conforming CPO echoes it on the resulting Session and CDR, giving you an end-to-end join key from the driver's tap to the invoice.