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

OCPP 1.6-J Tutorial, Part 1: Architecture and Message Framing

How OCPP 1.6-J works on the wire: WebSocket URL and subprotocol, CALL/CALLRESULT/CALLERROR frames, message IDs and the six feature profiles.

HashTech Engineering · October 26, 2026 · 7 min read

OCPP 1.6 is still the protocol most deployed charging hardware speaks. If you are building a charging station management system (CSMS), writing charger firmware, or integrating a new hardware model into an existing platform, you will spend a lot of time with it. This series walks through OCPP 1.6-J message by message, with the actual JSON frames you will see on the wire and the problems that show up when real chargers meet real backends.

This first part covers the foundations everything else depends on: who talks to whom, how the WebSocket connection is set up, how every message is framed, and how the spec is split into feature profiles. If you are deciding between protocol versions, start with OCPP 1.6 vs 2.0; if you are new to the backend side, What is a CSMS gives the business context.

The two actors: Charge Point and Central System

OCPP 1.6 uses its own vocabulary. The charger is the Charge Point, and the backend is the Central System. (OCPP 2.0.1 renamed these to Charging Station and CSMS; this series uses CSMS and Central System interchangeably.) A Charge Point has one or more connectors, numbered from 1. Connector 0 is a special value that means the Charge Point as a whole, and it shows up in status reporting, availability changes and smart charging.

Both sides can initiate messages. The Charge Point sends things like BootNotification, StatusNotification, StartTransaction and MeterValues. The Central System sends commands like RemoteStartTransaction, ChangeConfiguration, Reset and SetChargingProfile. Every request gets exactly one response.

OCPP-J versus OCPP-S

OCPP 1.6 was published with two transports: SOAP over HTTP (OCPP-S) and JSON over WebSocket (OCPP-J). The "-J" in OCPP 1.6-J refers to the JSON variant, and it is what nearly every modern deployment uses. The JSON transport has a practical advantage beyond smaller messages: the charger opens the connection outbound, so the Central System can push commands down that same socket without the charger needing a public IP address or an open inbound port. That matters when chargers sit behind cellular carrier NAT.

Opening the connection

The Charge Point connects to a URL that ends with its own identity. The Central System base URL is configured on the charger, and the charger appends its Charge Point identity as the last path segment:

http
GET /ocpp/CP-0001 HTTP/1.1
Host: csms.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Version: 13
Sec-WebSocket-Protocol: ocpp1.6

Two things in that handshake matter. First, the identity in the path is how the Central System knows which charger is on the other end of the socket. There is no identity field in OCPP 1.6 messages themselves, so everything you do later is keyed off the connection. Second, the Sec-WebSocket-Protocol header must include ocpp1.6, and the server must echo the subprotocol it selected in its response:

http
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: ocpp1.6

If the identity contains characters that need escaping in a URL, the charger should percent-encode them. In practice, keep identities to letters, digits, hyphens and underscores, and normalise them before you look them up. Authentication (HTTP Basic auth on the upgrade request, TLS, client certificates) was formalised later in the OCPP 1.6 Security Whitepaper, which we cover in part 11.

The three message types

Every OCPP-J message is a JSON array. The first element is a message type number, the second is a unique message ID, and the rest depends on the type.

MessageTypeIdNameShape
2CALL[2, "<UniqueId>", "<Action>", {payload}]
3CALLRESULT[3, "<UniqueId>", {payload}]
4CALLERROR[4, "<UniqueId>", "<errorCode>", "<errorDescription>", {errorDetails}]

A CALL is a request. Here a charger sends its boot notification:

json
[
  2,
  "19223201",
  "BootNotification",
  {
    "chargePointVendor": "VendorX",
    "chargePointModel": "ACW-22"
  }
]

The CALLRESULT carries the same unique ID and the response payload. Note that it does not repeat the action name. The receiver of a CALLRESULT has to remember which action it sent under that ID to know how to parse the payload.

json
[
  3,
  "19223201",
  {
    "status": "Accepted",
    "currentTime": "2026-10-26T09:15:02.000Z",
    "interval": 300
  }
]

A CALLERROR is sent when the receiver could not process a CALL at all: the action is unknown, the payload is malformed, or something failed internally. It is not used for business-level rejections. A RemoteStartTransaction the charger declines is a normal CALLRESULT with "status": "Rejected", not a CALLERROR.

json
[
  4,
  "19223201",
  "FormationViolation",
  "Payload is not valid JSON for BootNotification",
  {}
]

The error codes defined for OCPP-J 1.6 are NotImplemented, NotSupported, InternalError, ProtocolError, SecurityError, FormationViolation, PropertyConstraintViolation, OccurenceConstraintViolation, TypeConstraintViolation and GenericError. Yes, OccurenceConstraintViolation is misspelled in the 1.6 specification, and you should send it exactly that way. Part 12 covers when each one applies.

Unique IDs and request/response matching

The unique ID is a string of at most 36 characters. It only has to be unique among the sender's outstanding requests on that connection, but most implementations use a UUID or a monotonically increasing counter. Both sides generate their own IDs independently, so the charger and the CSMS may both have a message "1" in flight at the same time; they are different messages, and you match a response to a request by looking it up in the table of CALLs that you sent.

The 1.6-J specification also says a sender should not send a new CALL until the previous one has been answered or has timed out. In other words, each direction has at most one outstanding request at a time. This is a simple rule that many CSMS implementations break, typically by firing a burst of ChangeConfiguration messages at a freshly booted charger. Some chargers handle that fine, some silently drop all but the first, and some reply out of order. Queue outgoing CALLs per connection and send them one at a time.

Data types you will meet everywhere

  • dateTime: ISO 8601 timestamps. Send UTC with a Z suffix. Many chargers produce timestamps without milliseconds or with a numeric offset, so parse leniently and emit strictly.
  • CiString20Type, CiString25Type, CiString50Type, CiString255Type, CiString500Type: case-insensitive strings with a maximum length. idTag is a CiString20, so RFID UIDs longer than 20 characters do not fit.
  • integer and decimal: meter readings in StartTransaction and StopTransaction are integers in Wh, while MeterValues carries readings as strings.
  • Optional fields are omitted rather than sent as null. A payload with "reservationId": null fails strict schema validation on many implementations.

Feature profiles

OCPP 1.6 groups messages into feature profiles. A charger reports which profiles it supports through the SupportedFeatureProfiles configuration key, and only Core is mandatory.

ProfileMessages
CoreAuthorize, BootNotification, ChangeAvailability, ChangeConfiguration, ClearCache, DataTransfer, GetConfiguration, Heartbeat, MeterValues, RemoteStartTransaction, RemoteStopTransaction, Reset, StartTransaction, StatusNotification, StopTransaction, UnlockConnector
Firmware ManagementGetDiagnostics, DiagnosticsStatusNotification, FirmwareStatusNotification, UpdateFirmware
Local Auth List ManagementGetLocalListVersion, SendLocalList
ReservationReserveNow, CancelReservation
Smart ChargingSetChargingProfile, ClearChargingProfile, GetCompositeSchedule
Remote TriggerTriggerMessage

In your CSMS, read SupportedFeatureProfiles after boot (see configuration keys) and gate features on it. Do not offer smart charging for a charger that does not list SmartCharging, and expect a NotImplemented or NotSupported CALLERROR if you try anyway.

A typical session, end to end

Here is the sequence of messages for an RFID-started charging session on a single-connector charger. Each step has its own part in this series.

  1. Charger connects and sends BootNotification; CSMS replies Accepted with a heartbeat interval. (Part 2)
  2. Charger sends StatusNotification for connector 0 and connector 1: Available. (Part 3)
  3. Driver plugs in: StatusNotification Preparing.
  4. Driver taps a card: Authorize with the idTag, CSMS returns Accepted. (Part 4)
  5. Charger sends StartTransaction with meterStart; CSMS returns a transactionId. (Part 5)
  6. StatusNotification Charging, then periodic MeterValues. (Part 6)
  7. Driver taps again or unplugs: StopTransaction with meterStop and a reason.
  8. StatusNotification Finishing, then Available once the cable is removed.

Commands from the backend (remote start, reset, configuration, smart charging, firmware) are layered on top of that flow, and they are covered in parts 7 through 10.

Key takeaways

  • OCPP-J is JSON arrays over a WebSocket that the charger opens, with the charger identity as the last URL path segment and ocpp1.6 as the negotiated subprotocol.
  • Three frame types: CALL (2), CALLRESULT (3), CALLERROR (4). Match responses by unique ID, and keep one outstanding CALL per direction.
  • CALLERROR is for protocol-level failures, not for business rejections.
  • Only the Core profile is mandatory. Check SupportedFeatureProfiles before using anything else.

Next: Part 2, BootNotification and Heartbeat. HashTech builds and operates OCPP 1.6-J and 2.0.1 charging platforms; more about that work is on our EV charging page.

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.