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

OCPP 1.6 BootNotification and Heartbeat Explained

What happens when a charger connects: BootNotification fields, Accepted/Pending/Rejected handling, heartbeat interval and clock sync in OCPP 1.6-J.

HashTech Engineering · October 26, 2026 · 6 min read

BootNotification is the first OCPP message a charger sends after it connects, and the response decides whether that charger is allowed to operate on your network. Heartbeat keeps the link alive and the charger's clock correct. Both look trivial, and both are responsible for a surprising number of field problems. This is part 2 of the series; part 1 covers the WebSocket connection and message framing.

When the charger sends BootNotification

The charger sends BootNotification after it has (re)started and opened the WebSocket connection. That includes a power cycle, a firmware update, and a Reset from the CSMS. It is not supposed to be sent on every reconnect after a network blip; a charger that simply lost connectivity reconnects and carries on. In practice, firmware differs here: some chargers send BootNotification on every new WebSocket connection. Your CSMS should handle both without side effects, which mostly means not treating a boot as proof that ongoing transactions were lost.

The request

json
[
  2,
  "a1b2c3",
  "BootNotification",
  {
    "chargePointVendor": "VendorX",
    "chargePointModel": "DCF-60",
    "chargePointSerialNumber": "DCF60-2025-00412",
    "firmwareVersion": "3.4.1",
    "iccid": "8944500102198304826",
    "imsi": "234150999999999",
    "meterType": "Internal MID meter",
    "meterSerialNumber": "MTR-77812"
  }
]
FieldTypeRequired
chargePointVendorCiString20Yes
chargePointModelCiString20Yes
chargePointSerialNumberCiString25No
chargeBoxSerialNumberCiString25No (deprecated, use chargePointSerialNumber)
firmwareVersionCiString50No
iccidCiString20No
imsiCiString20No
meterTypeCiString25No
meterSerialNumberCiString25No

Store all of these, every time. Vendor, model and firmware version let you apply per-model workarounds (and every CSMS ends up with some). The ICCID and IMSI identify the SIM card, which is exactly what your field team needs when a cellular charger goes quiet. Recording the firmware version on each boot also gives you a free audit trail for firmware rollouts, which ties into part 10.

The response: Accepted, Pending, Rejected

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

All three fields are required. currentTime is the Central System's clock, and the charger is expected to use it to set its own. The meaning of interval depends on status.

statusMeaningWhat interval means
AcceptedThe charger is registered and may operate normally.Heartbeat interval in seconds.
PendingThe CSMS is not ready to accept the charger yet but wants to talk to it, typically to provision configuration first.Minimum wait before the charger sends BootNotification again.
RejectedThe charger is not accepted, for example an unknown identity.Minimum wait before the charger retries BootNotification.

Accepted

After Accepted, the charger sends StatusNotification for its connectors (see part 3) and starts normal operation. It should also adopt interval as its heartbeat interval, which the spec ties to the HeartbeatInterval configuration key.

Pending

Pending is the most useful and least used status. While a charger is pending, the CSMS may send it requests, such as GetConfiguration, ChangeConfiguration or TriggerMessage, so you can provision it before it goes live: set the meter value sample interval, enable the local auth list, push a smart charging default. The charger should not send its own requests other than a new BootNotification (and responses to CSMS requests) until it is accepted. Once provisioning is done, you can either wait for the charger's next BootNotification and answer Accepted, or send TriggerMessage for BootNotification to shorten the wait, if the charger supports Remote Trigger.

json
[3, "a1b2c3", {"status": "Pending", "currentTime": "2026-10-26T09:15:02.000Z", "interval": 30}]
[2, "csms-001", "ChangeConfiguration", {"key": "MeterValueSampleInterval", "value": "60"}]
[3, "csms-001", {"status": "Accepted"}]
[2, "csms-002", "TriggerMessage", {"requestedMessage": "BootNotification"}]
[3, "csms-002", {"status": "Accepted"}]
[2, "b7e9f0", "BootNotification", {"chargePointVendor": "VendorX", "chargePointModel": "DCF-60"}]
[3, "b7e9f0", {"status": "Accepted", "currentTime": "2026-10-26T09:16:10.000Z", "interval": 300}]

Rejected

A rejected charger waits at least interval seconds and tries BootNotification again. The spec also says the Central System may close the connection, and that while rejected the charger should not respond to Central System requests. Use Rejected for genuinely unknown or decommissioned chargers. A very short interval on rejection creates a busy loop across your fleet whenever something goes wrong with your registry, so choose something measured in minutes.

What the CSMS should do on boot

  • Look up the charger by the identity from the WebSocket URL, not by the serial number in the payload. The serial number is useful for cross-checking but the identity is the protocol key.
  • Record vendor, model, serial, firmware, ICCID and the boot timestamp.
  • Do not close open transactions just because a boot arrived. If the charger rebooted mid-session it should send StopTransaction (often with reason PowerLoss, HardReset or Reboot) from its queue. Close orphans with a background reconciliation job, not inline in the boot handler.
  • Respond quickly. Some chargers have short timeouts on their first message, and a slow database lookup at boot looks like an unreachable CSMS.
  • If the charger was mid-firmware-update, compare firmwareVersion with what you sent to decide whether the update succeeded.

Heartbeat

Heartbeat has an empty request and a response containing only the Central System time.

json
[2, "hb-0193", "Heartbeat", {}]
[3, "hb-0193", {"currentTime": "2026-10-26T09:20:02.114Z"}]

It does two jobs: it lets the CSMS know the charger is still alive, and it keeps the charger's clock synchronised. The interval comes from the BootNotification response and can be changed later through the HeartbeatInterval configuration key.

The 1.6-J specification notes that heartbeats are not strictly necessary over WebSocket for liveness, since the transport itself detects dead connections, but recommends sending at least one heartbeat per 24 hours for time synchronisation. It also says the charger may skip a heartbeat if it sent another message within the interval, and the CSMS should treat any message from the charger as a sign of life. Your online/offline logic should therefore use "last message received" rather than "last heartbeat received".

WebSocket ping/pong

WebSocket has its own ping/pong frames, and OCPP 1.6-J defines a WebSocketPingInterval configuration key for them. Pings are cheap and do not touch your OCPP message handling, so for liveness detection a ping interval of a minute or two plus a long heartbeat interval works well. Mobile networks and NAT gateways often drop idle TCP flows after a few minutes, so some traffic on the socket at least that often keeps the path open.

MechanismPurposeTypical setting
WebSocketPingIntervalKeep NAT mappings alive, detect dead sockets60 to 120 seconds
HeartbeatIntervalOCPP-level liveness and clock sync300 to 3600 seconds, depending on fleet size and whether ping is used

Time synchronisation gotchas

  • Chargers that ignore currentTime drift. Transactions then arrive with timestamps minutes or hours off, and billing disputes follow. Compare charger timestamps with your receive time and flag chargers with large skew.
  • Chargers that boot without a clock (no RTC battery) may send timestamps from 1970 or 2000 before the first BootNotification response. Queued StartTransaction messages from that window are common after a power cut. Do not reject them; record both timestamps and flag the session.
  • Always return UTC. A CSMS that returns local time without an offset creates offsets equal to your timezone across the whole fleet.

Key takeaways

  • Identify the charger by its WebSocket URL identity; store everything in the boot payload.
  • interval means heartbeat interval only when the status is Accepted. For Pending and Rejected it is a retry delay.
  • Use Pending to provision configuration before a charger goes live, but test each model first.
  • Treat any inbound message as a heartbeat, and use WebSocket ping for cheap liveness.

Previous: Part 1, architecture and framing. Next: Part 3, StatusNotification. If you are testing these flows without hardware, see testing OCPP with a charge point simulator.

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.