OCPP in Practice: Choosing Between 1.6, 2.0.1 and 2.1 for Your EV Charging Platform
What each OCPP version actually buys you, the WebSocket message choreography with real frames, a minimal CSMS endpoint in C#, and the state-management work that is the real product.
Every public EV charger you've ever used speaks one protocol to its backend: OCPP — the Open Charge Point Protocol, stewarded by the Open Charge Alliance. It's the reason a charging network can mix hardware vendors without rewriting its platform, and the reason your CSMS (Charging Station Management System) can be a product instead of a driver zoo. This guide compares the three living versions — 1.6, 2.0.1 and 2.1 — and walks through what implementing a CSMS actually involves, WebSockets and all.
Key takeaways
- OCPP 1.6 (2015) is still the installed-base king: JSON over WebSocket, smart charging profiles, good-enough security via profiles and TLS.
- OCPP 2.0.1 (2020) is the modern baseline — device management, far stronger security, ISO 15118 plug-and-charge support — and since 2024 an IEC standard (63584). Not backward compatible with 1.6.
- OCPP 2.1 (2025) adds bidirectional power (V2X), DER integration, battery swapping and dynamic QR payments — while staying compatible with 2.0.1 application logic.
- A production CSMS is 20% protocol handling and 80% state management: connectivity, transactions, offline behaviour, and money.
Version comparison
| 1.6 | 2.0.1 | 2.1 | |
|---|---|---|---|
| Year | 2015 | 2020 (IEC 63584 in 2024) | 2025 |
| Transport | SOAP or JSON/WebSocket | JSON/WebSocket | JSON/WebSocket |
| Security | Bolt-on profiles | Built-in: TLS, cert management, security events | 2.0.1 + hardening |
| ISO 15118 (Plug & Charge) | — | ✔ | ✔ incl. 15118-20 bidirectional |
| V2X / DER | — | — | ✔ (V2G, battery swap, DER control) |
| Ad-hoc payment | external | external | dynamic QR, prepaid cards, local cost |
| Compatibility | — | breaks 1.6 | compatible with 2.0.1 logic |
Which one do you target? If you operate existing hardware: 1.6J, because that's what's bolted to the wall. Greenfield network or platform play: 2.0.1 as the core with a 1.6 adapter at the edge, and 2.1 features (V2X, QR payments) on your roadmap rather than your MVP.
Anatomy of the connection
A charge point dials out to your CSMS over WebSocket and everything rides that socket as JSON arrays: [MessageType, UniqueId, Action, Payload]. A boot sequence in OCPP 1.6J:
→ [2, "19223201", "BootNotification",
{ "chargePointVendor": "VendorX", "chargePointModel": "CP-7kW" }]
← [3, "19223201",
{ "status": "Accepted", "currentTime": "2026-09-01T10:00:00Z", "interval": 300 }]
→ [2, "19223202", "StatusNotification",
{ "connectorId": 1, "status": "Available", "errorCode": "NoError" }]
→ [2, "19223203", "Heartbeat", {}]
← [3, "19223203", { "currentTime": "2026-09-01T10:05:00Z" }]
A charging session, condensed: Authorize → StartTransaction → MeterValues… → StopTransaction (1.6) or the unified TransactionEvent stream (2.0.1+). Your CSMS confirms each step and owns the authorization decision.
A minimal CSMS endpoint (C#)
// ASP.NET Core WebSocket endpoint — the skeleton every CSMS grows from
app.Map("/ocpp/{chargePointId}", async (HttpContext ctx, string chargePointId) =>
{
// OCPP subprotocol negotiation matters: "ocpp1.6" / "ocpp2.0.1"
var socket = await ctx.WebSockets.AcceptWebSocketAsync("ocpp1.6");
var session = registry.Attach(chargePointId, socket);
await foreach (var frame in session.ReadFramesAsync())
{
var (type, id, action, payload) = OcppFrame.Parse(frame);
var response = action switch
{
"BootNotification" => handlers.Boot(chargePointId, payload),
"Heartbeat" => handlers.Heartbeat(chargePointId),
"Authorize" => await handlers.Authorize(payload), // RFID/token lookup
"StartTransaction" => await handlers.StartTx(chargePointId, payload),
"MeterValues" => handlers.Meter(chargePointId, payload), // billing fuel
"StopTransaction" => await handlers.StopTx(chargePointId, payload),
_ => OcppError.NotImplemented(id),
};
await session.SendAsync(OcppFrame.CallResult(id, response));
}
});
The protocol part really is this small. The product part is everything around it:
- Offline transactions — chargers queue sessions during outages and replay them; your billing must tolerate late, out-of-order events.
- Smart charging — charging profiles let you cap site load and shift energy to cheap hours; in 2.1 this extends to feeding power back (V2G).
- Security profiles — TLS with basic auth at minimum; client certificates for anything serious; 2.0.1's security events feed your SOC.
- Fleet state — thousands of sockets, each a stateful connection with heartbeats, retries and firmware campaigns. This is where Redis-backed presence and an event-sourced transaction log earn their keep.
FAQ
Can one platform serve 1.6 and 2.0.1 chargers simultaneously?
Yes — negotiate the subprotocol per connection and normalize both dialects into one internal domain model at the edge. Don't let 1.6 semantics leak into your core.
Is OCPP enough for roaming (other networks' users charging on mine)?
No — roaming is OCPI's job (CSMS-to-CSMS). OCPP is charger-to-CSMS. A full platform usually speaks both.
Do I need 2.1 today?
Only if bidirectional charging, DER orchestration or QR-based ad-hoc payment is on your near-term roadmap. Otherwise: build clean 2.0.1 logic — 2.1 was explicitly designed to extend it.
Next steps
EV charging platforms sit at a fun intersection: realtime protocol work, embedded quirks, and billing correctness. If you're building or scaling a CSMS, tell me about your project — or start with a system-design session to get the state model right before the fleet grows.