OCPP-J — JSON over WebSockets
Source: OCPP 2.0.1 Part 4 — JSON over WebSockets implementation guide (FINAL, 2020-03-31)
OCPP 2.0.1 only supports JSON, but the -J designation is preserved (e.g. OCPP2.0.1J) in case a future transport is added. Part 2 defines the messages; Part 4 defines the transport: WebSocket handshake, the RPC framework that wraps every message, error codes, reconnection, routing through a Local Controller, signed messages, and configuration variables.
1. WebSocket handshake
The CSMS is the WebSocket server, the Charging Station is the WebSocket client. The CS keeps the WebSocket connection open at all times.
Connection URL
The CSMS exposes one or more OCPP-J endpoint URLs (ws:// or wss://). The CS appends /<charging-station-identity> to derive its connection URL. The identity is identifierString (max 48 chars). The colon : is not allowed in the identity (it conflicts with HTTP Basic auth).
CSMS endpoint URL: wss://csms.example.com/ocpp
CS connection URL: wss://csms.example.com/ocpp/CS001
(with percent-encoding where needed)
RECOMMENDED that the CSMS does not rely solely on the URL identity — double-check against authentication credentials.
OCPP version negotiation (Sec-WebSocket-Protocol)
The CS lists supported versions (in preference order) using these IANA-registered subprotocol names:
| OCPP version | WebSocket subprotocol |
|---|---|
| 1.2 | ocpp1.2 |
| 1.5 | ocpp1.5 |
| 1.6 | ocpp1.6 |
| 2.0 | ocpp2.0 |
| 2.0.1 | ocpp2.0.1 |
Example handshake
Client request (CS → CSMS):
GET /webServices/ocpp/CS3211 HTTP/1.1
Host: some.server.com:33033
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: ocpp2.0.1, ocpp1.6
Sec-WebSocket-Version: 13Server response (CSMS → CS):
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: ocpp2.0.1CSMS-specific failure modes:
- Unknown CS identity in path → respond with HTTP
404and abort the WebSocket connection. - No matching subprotocol → complete the handshake without
Sec-WebSocket-Protocoland immediately close.
WebSocket compression (RFC 7692)
| Device | Compression support |
|---|---|
| Charging Station | Optional (RECOMMENDED on mobile-data CS) |
| CSMS | Required |
| Local Controller | Required |
When both parties support it, DEFLATE compression is used. When only one does, the JSON is sent uncompressed (like OCPP 1.6J). The CS SHOULD NOT close the connection just because compression is not negotiated.
2. RPC framework
WebSocket is full-duplex with no inherent request/response correlation, so OCPP-J defines a tiny RPC framework (inspired by WAMP, simplified). Three message types:
| Type | Number | Description |
|---|---|---|
CALL | 2 | Request message |
CALLRESULT | 3 | Response to a successful CALL |
CALLERROR | 4 | Error response to a CALL |
When a server receives a message with a MessageTypeNumber not in this list, it SHALL ignore the payload and respond with CALLERROR MessageTypeNotSupported.
Synchronicity
A CS or CSMS SHALL NOT send a new CALL on the same connection until all previous CALLs it sent have been responded to (CALLRESULT/CALLERROR) or have timed out. Note: cross-direction CALLs may still arrive while waiting for a response — that's expected.
Message validity
The whole envelope (wrapper + payload) MUST be valid JSON encoded as UTF-8 (RFC 3629). For optional fields with cardinality 0..1 or 0..*, omit the field entirely — do not send null, {}, or [].
A message is valid only if: action is known, JSON is valid, all required fields are present, and all data types are correct. Otherwise the receiver responds with CALLERROR.
Action naming
The Action field is the OCPP message name without the Request suffix.
For BootNotificationRequest → action is "BootNotification".
CALLRESULT does not contain the action — match by MessageId.
Message structures
CALL — 4-element array:
[<MessageTypeId=2>, "<MessageId>", "<Action>", {<Payload>}]
[2, "19223201", "BootNotification", {
"reason": "PowerUp",
"chargingStation": { "model": "SingleSocketCharger", "vendorName": "VendorX" }
}]CALLRESULT — 3-element array:
[<MessageTypeId=3>, "<MessageId>", {<Payload>}]
[3, "19223201", {
"currentTime": "2013-02-01T20:53:32.486Z",
"interval": 300,
"status": "Accepted"
}]CALLERROR — 5-element array:
[<MessageTypeId=4>, "<MessageId>", "<errorCode>", "<errorDescription>", {<errorDetails>}]
[4, "162376037", "NotSupported", "SetDisplayMessageRequest not implemented", {}]When CALLERROR is sent for a corrupt message: don't include syntactically invalid JSON literally (encode it first). When even the MessageId cannot be read, use "-1".
The MessageId matches CALL ↔ CALLRESULT/CALLERROR. Max length 36 (allows UUID/GUID).
RPC framework error codes
| ErrorCode | Description |
|---|---|
FormatViolation | Payload for Action is syntactically incorrect. |
GenericError | Any other error not covered by the more specific codes. |
InternalError | Internal error; receiver couldn't process the Action. |
MessageTypeNotSupported | Unknown MessageTypeNumber. |
NotImplemented | Requested Action is unknown to the receiver. |
NotSupported | Action is recognized but not supported. |
OccurrenceConstraintViolation | At least one field violates occurrence constraints. |
PropertyConstraintViolation | At least one field contains an invalid value. |
ProtocolError | Payload doesn't conform to PDU structure. |
RpcFrameworkError | Content is not a valid RPC request (e.g. unreadable MessageId). |
SecurityError | Security issue prevented completion. |
TypeConstraintViolation | At least one field violates data type constraints. |
Extension fallback
If a future OCPP version adds new MessageTypeIds, an OCPP 2.0.1 implementation responds with CALLERROR MessageTypeNotSupported. The sender then either terminates the connection or falls back to CALL/CALLRESULT/CALLERROR.
3. Connection management
Data integrity
OCPP-J relies on the underlying TCP/IP transport — no application-layer integrity check beyond what TLS provides.
WebSocket Ping/Pong vs OCPP Heartbeat
WebSocket Ping/Pong frames keep the connection alive across NATs. They can substitute for most OCPP Heartbeat traffic — but not for time synchronization. At least one OCPP Heartbeat per 24 hours is recommended for clock sync.
Reconnect with exponential back-off
When the connection is lost, the CS retries with a back-off + jitter to avoid thundering-herd reconnects against the CSMS:
- First attempt: wait
RetryBackOffWaitMinimumseconds + random ∈ [0,RetryBackOffRandomRange]. - After every failed attempt, double the back-off (up to
RetryBackOffRepeatTimesdoublings) and add a fresh random ∈ [0,RetryBackOffRandomRange]. - After
RetryBackOffRepeatTimes, keep retrying with the last back-off — don't grow it further.
When reconnecting, the CS SHOULD NOT send BootNotification unless something has actually changed (in 1.6 SOAP this was good practice; with WebSocket the CSMS already matches identity to channel at connection time).
Network nodes / NAT
Because the CS opens the TCP connection to the CSMS and keeps it open, NAT issues that plagued OCPP-S are gone — no smart device needed in between.
4. OCPP Routing — Local Controller
A Local Controller (LC) sits between the CSMS and a group of Charging Stations near the site. It enables Local Smart Charging (load-balancing across the group). The LC must be transparent: the CS works the same as if it were directly connected to the CSMS, and the CSMS cannot tell that an LC is in the path.
Connections
Per Charging Station, two WebSocket connections exist:
CS ──ws→ Local Controller ──ws→ CSMS
(configured on CS) (configured on LC)
Both use the same connection-URL path (same CS identity). Optionally, the LC opens a separate WebSocket to the CSMS so the CSMS can address the LC directly (settings, overall profiles).
Connection loss
- LC↔CSMS lost → LC closes all corresponding LC↔CS WebSockets, forcing the CSes to queue messages as if directly disconnected from CSMS.
- CS↔LC lost → LC closes the matching LC↔CSMS WebSocket so CSMS sees the CS as offline.
LC-initiated messages
The LC SHALL relay any CS-initiated message to the CSMS (and vice versa). When the LC sends its own messages to a CS:
- LC
MessageIds SHALL NOT collide with CSMSMessageIds — assign a number range, or use UUIDs. - Replies to those LC-initiated messages SHALL NOT be forwarded to the CSMS.
LC security
Standard OCPP security profiles apply. In each leg, the LC plays the role of the appropriate side:
| Leg | LC acts as | TLS role |
|---|---|---|
| CS ↔ LC | CSMS | TLS server |
| LC ↔ CSMS | Charging Station | TLS client |
Under Security Profile 3 (mTLS), the LC needs both a CSMS Certificate and a Charging Station Certificate, both unique to the LC. The LC SHALL NOT store the certificates of the attached CSes nor of the CSMS.
The CSMS Certificate URL SHALL distinguish the LC from the actual CSMS — so a stolen LC certificate cannot be used against other Charging Stations in the wider infrastructure.
The TLS connections terminate on the LC — the LC can read and manipulate everything in transit. To detect tampering, sign critical messages (see §5).
5. Signed messages (JWS)
Optional. Allows the CS and CSMS to prove authorship even when an LC is in the path, and supports forwarding signed data to third parties (e.g. DSO).
Format
For each normal message:
[<MessageTypeId>, "<MessageId>", {<Extension>}, "<Action>", {<Payload>}]
The signed equivalent:
[<MessageTypeId>, "<MessageId>", {<Extension>}, "<SignedAction>", {<SignedPayload>}]
<SignedAction>=<Action>+"-Signed"(e.g.BootNotification-Signed).<SignedPayload>= JWS (RFC 7515) encoding of the original payload, in Flattened JWS JSON Serialization.- JWS Protected Header SHALL contain
OCPPActionandOCPPMessageTypedId. - JWS Protected Header SHOULD contain
x5t#S256(SHA-256 hash of DER-encoded signing certificate) to identify the key.
Allowed algorithms (JWA, RFC 7518 §3.1)
To match TLS-allowed algorithms:
ES256— ECDSA using P-256 and SHA-256.RS256— RSASSA-PKCS1-v1_5 using SHA-256.RS384— RSASSA-PKCS1-v1_5 using SHA-384.
Handling
A receiver SHALL extract the encapsulated normal message and process it normally per OCPP 2.0.1. Signature validation is OPTIONAL (a TLS connection is already expected). If a CS receives a signed request and supports signing, it SHALL send a signed reply.
Key management
Signing keys MAY be the same as the TLS certificates, but often won't be (e.g. signed meter readings should use the calibrated meter's key). Note: with an LC in the path, the TLS client/server keys differ from the end-to-end signing keys — that mismatch is expected and SHALL NOT invalidate the signature.
6. Configuration variables (component OCPPCommCtrlr)
| Variable | Type | Description |
|---|---|---|
RetryBackOffRepeatTimes | integer | Max number of times the back-off is doubled before being capped. |
RetryBackOffRandomRange | integer (s) | Max value of the random jitter added to each reconnect attempt. |
RetryBackOffWaitMinimum | integer (s) | Minimum back-off applied on the first reconnect attempt. |
WebSocketPingInterval | integer (s) | 0 disables client-side WS Ping/Pong. Negative is Rejected. RECOMMENDED < MessageAttemptsTransactionEvent × MessageAttemptIntervalTransactionEvent to avoid resend storms triggered by connectivity issues. |
7. CustomData extension
In the JSON Schemas, every class has additionalProperties: false — strict by default. To allow vendor-specific experiments, every class also exposes a customData property of type CustomDataType whose only required field is vendorId (reverse-DNS, like DataTransfer.vendorId). CustomDataType itself does not set additionalProperties: false, so it can be freely extended.
Schema sketch
{
"$id": "HeartbeatRequest",
"definitions": {
"CustomDataType": {
"type": "object",
"properties": {
"vendorId": { "type": "string", "maxLength": 255 }
},
"required": ["vendorId"]
}
},
"type": "object",
"additionalProperties": false,
"properties": {
"customData": { "$ref": "#/definitions/CustomDataType" }
}
}Example — extended HeartbeatRequest
{
"customData": {
"vendorId": "com.mycompany.customheartbeat",
"mainMeterValue": 12345,
"sessionsToDate": 342
}
}A CSMS that has implemented the extension (matched by vendorId) processes it; others ignore the extra fields. The CustomizationCtrlr component lets a CSMS report the customizations a CS supports.