Skip to content
Mobility

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.

4 min read Updated Sep 4, 2026
OCPP in Practice: Choosing Between 1.6, 2.0.1 and 2.1 for Your EV Charging Platform

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.

Flow: charge point connects over WebSocket/OCPP to the CSMS, which drives billing and smart charging

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.62.0.12.1
Year20152020 (IEC 63584 in 2024)2025
TransportSOAP or JSON/WebSocketJSON/WebSocketJSON/WebSocket
SecurityBolt-on profilesBuilt-in: TLS, cert management, security events2.0.1 + hardening
ISO 15118 (Plug & Charge)✔ incl. 15118-20 bidirectional
V2X / DER✔ (V2G, battery swap, DER control)
Ad-hoc paymentexternalexternaldynamic QR, prepaid cards, local cost
Compatibilitybreaks 1.6compatible 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.

Keep reading

Related articles