Introduction
API guide
  • AuthenticationSoon
  • Your first API callSoon
  • Interactive API referenceSoon
  • Roles and permissionsSoon
  • Errors, pagination and versioningSoon
  • Live updatesSoon
  • Sending commands to a chargerSoon
  • Rate limitsSoon
Troubleshooting
  • My charger will not connectSoon
  • It connects, then dropsSoon
  • A command timed out or returned 409Soon
  • A card is not being authorizedSoon
  • My transaction looks wrongSoon
  • Meter values are missing or sparseSoon
  • Reading the OCPP message logSoon
  • Known limitationsSoon
Reference
  • GlossarySoon
  • Endpoint indexSoon
  • OCPP message supportSoon
  • Status and error codesSoon
  • ChangelogSoon
Flowion Docs

OCPP in ten minutes

OCPP is smaller than its page count suggests. Strip away the message catalogue and what remains is a WebSocket, a three-element JSON array, and one rule about not talking over yourself. This page is the whole of it.

Everything here is OCPP-J — OCPP over WebSocket using JSON. There was once a SOAP flavour (OCPP-S); it is not relevant to anything you will do here.

The station is the client#

The charging station opens the WebSocket. The CSMS is the server.

This is the reverse of what people expect from a device-management system, and it is deliberate: stations sit behind NAT with no reachable inbound address, so the connection has to be established outward. Once open, it is full-duplex — the CSMS sends commands down the same socket the station opened.

The station's identity is in the URL path. In Flowion:

wss://gateway.example.com/ocpp/{org_slug}/{charge_point_id}
wss://gateway.example.com/ocpp/{org_slug}/{env_slug}/{charge_point_id}

The second form names a specific environment; the first uses your organization's default one. Getting a real station connected is covered in Getting started.

Because identity is established once, at connection time, individual messages do not repeat it. Every message on a socket is implicitly from — or to — that station.

Agreeing on a version#

The station lists the OCPP versions it can speak in the WebSocket handshake's Sec-WebSocket-Protocol header, in its own order of preference:

GET /ocpp/acme/CP3211 HTTP/1.1
Host: gateway.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Protocol: ocpp2.0.1, ocpp1.6
Sec-WebSocket-Version: 13

The server picks one and echoes it back:

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

The subprotocol names are exactly ocpp1.6, ocpp2.0.1 and ocpp2.1. If the server supports none of the offered versions it must complete the handshake without that header and immediately close the connection — which is why a station that silently disconnects right after connecting is usually a version mismatch, not a credentials problem.

Which version gets chosen, and why Flowion prefers the one it does, is covered in Which OCPP version am I dealing with?.

Three message types#

A WebSocket is just a pipe; it has no notion of a request matching a response. OCPP adds a thin RPC layer on top. Every message is a JSON array whose first element says what kind it is.

TypeNumberShape
Call2[2, "<id>", "<Action>", {payload}]
CallResult3[3, "<id>", {payload}]
CallError4[4, "<id>", "<code>", "<description>", {details}]

A Call is a request. It names an ActionBootNotification, Authorize, Reset — and carries that action's arguments:

[2, "19223201", "BootNotification",
 {"chargePointVendor": "VendorX", "chargePointModel": "SingleSocketCharger"}]

The answer reuses the same id, so the sender can match them up:

[3, "19223201",
 {"status": "Accepted", "currentTime": "2013-02-01T20:53:32.486Z", "interval": 300}]

The id is a string of at most 36 characters — long enough for a GUID — and must be unique among the Calls that sender has issued on that connection.

Either side may send a Call. The station sends BootNotification, Authorize, StatusNotification, MeterValues. The CSMS sends Reset, UnlockConnector, RemoteStartTransaction. Same framing in both directions.

The mistake almost everyone makes#

A CallError is not "the answer was no".

A rejected authorization, a refused reset, a station saying it cannot unlock a connector — all of these are perfectly successful message exchanges. They come back as CallResults carrying a status field whose value happens to be disappointing. The specification is explicit that outcomes covered by a message's own response definition are regular results, "even if the result is undesirable for the recipient".

A CallError means something else entirely, and only in two cases:

  1. The message could not be transported.
  2. The message arrived but was not a valid message — missing mandatory fields, a duplicate id, an unparseable payload.

So a CallError is a bug report, not a business outcome. The defined codes:

CodeMeaning
NotImplementedAction unknown to the receiver
NotSupportedAction recognised but not supported
InternalErrorReceiver failed while processing
ProtocolErrorPayload incomplete
SecurityErrorSecurity problem prevented processing
FormationViolationPayload syntactically wrong
PropertyConstraintViolationValid syntax, invalid field value
OccurenceConstraintViolationField occurrence constraints violated
TypeConstraintViolationField of the wrong data type
GenericErrorAnything else

OccurenceConstraintViolation is misspelled in the specification. It is misspelled on the wire too.

One Call at a time#

Neither side should send a Call while one of its own earlier Calls is still outstanding — unanswered and not yet timed out.

This is the rule that shapes CSMS design more than any other. It means commands to a station are effectively serialised, and a station that stops answering blocks the queue behind it until a timeout fires. The specification leaves the timeout length to the implementation, recommending only that it suit the network — mobile links have much worse worst-case round trips than fixed lines.

The rule is per direction. While you are waiting for a station to answer your Reset, it may perfectly well send you a StatusNotification; Calls from the two sides cross each other constantly and that is expected.

Heartbeats and pings#

OCPP has a Heartbeat message. WebSocket has its own Ping/Pong frames. They overlap but are not interchangeable.

Ping/Pong is enough to keep a connection alive and detect a dead peer, and can replace most Heartbeats. What it cannot do is carry a timestamp — and the Heartbeat response's real job is clock synchronisation. A station with no battery-backed clock depends on the CSMS to know what time it is, and a station with the wrong time writes the wrong timestamps into your transaction records. The specification recommends at least one real Heartbeat per day for this reason.

Reconnecting#

A station sends BootNotification when it boots. It should not send one merely because the TCP connection dropped and came back — on a WebSocket the server already learns the station's identity from the handshake, so no extra message is needed.

A BootNotification you did not expect therefore means something: the station actually restarted, or its reported details changed.

That's it#

You now know the transport, the framing, the error model and the concurrency rule. Everything else in OCPP is the catalogue of Actions and their payloads — which you can look up in the API reference as you need it, rather than learning up front.

Next: The vocabulary, which is where most early confusion actually comes from.