OCPP WS IOocpp-ws-io

API Reference

Detailed API reference for OCPPClient and OCPPServer.

OCPPClient

import { OCPPClient } from 'ocpp-ws-io';

Constructor

const client = new OCPPClient(options: ClientOptions);

The client is typed for its protocols: new OCPPClient({ protocols: ["ocpp1.6"] }) is an OCPPClient<"ocpp1.6">, so calls, handlers and responses use the 1.6 types, and a version named in a call must be one of its protocols. Without protocols, or with a list only known at runtime, it is typed for every version. Custom protocols declared in OCPPMethodMap count as protocols.

A typed client still fits a variable or list typed as plain OCPPClient, which is typed for every version, as in 2.x. For a list of clients configured for different protocols that should not make calls typed for the wrong version (for example to close them all), use AnyOCPPClient: every client fits it, and it has everything except the calls typed by protocol.

ClientOptions

OptionTypeDefaultDescription
identitystringrequiredCharging station ID
endpointstringrequiredWebSocket URL (ws:// or wss://)
protocolsstring[][]OCPP subprotocols to negotiate
securityProfileSecurityProfileNONESecurity profile (0–3)
passwordstring | Buffer—Password for HTTP Basic Auth, with the identity as the username. Sent whenever it is set, on any profile; profiles 1 and 2 require it. A Buffer is sent as its bytes, a string as UTF-8
tlsTLSOptions—TLS settings for a wss:// endpoint, on every security profile (ca for a private CA even on profile 0); profile 3 needs cert and key
headersRecord<string, string>—Additional WebSocket headers
wsOptsWsClientOptions—Raw ws client options: a proxy agent, localAddress, family, origin, maxPayload, followRedirects… (see wsOpts below)
queryRecord<string, string> | string—URL query parameters, as an object or a query string ("a=1&b=2", leading ? optional); added after any query in endpoint
reconnectbooleantrueAuto-reconnect on disconnect
maxReconnectsnumberInfinityMax reconnection attempts
pingIntervalMsnumber30000Includes ±25% randomized jitter
strictModeboolean | string[]falseSchema validation, off by default. Set true (or a list of protocols) for full payload validation (2.0.1 / 2.1 §4.1.7)
strictModeMethodsstring[]—Restrict validation to specific methods
idGenerator() => string—Creates the message ID of each outgoing CALL and SEND. A per-call idempotencyKey wins; without either, a random UUID
idValidator(id: string) => boolean—Checks each incoming CALL / SEND message ID, in any mode, in place of the built-in check (see Message IDs)
loggingLoggingConfig{}Configuration for structured logging.
offlineQueuebooleanfalseQueue calls while disconnected, flush on reconnect.
offlineQueueMaxSizenumber100Max offline queue size before oldest are dropped.
compressionboolean | CompressionOptionsfalseOffer WebSocket permessage-deflate compression to the CSMS. Off by default: nothing is offered (see Compression)
callConcurrencynumber1Outbound CALLs in flight at once. 1 follows OCPP-J §4.1.1 (Synchronicity): a new CALL waits until the previous one is answered or has timed out. Above 1 only for a CSMS known to cope (see callImmediate())
respondWithDetailedErrorsbooleanfalseSend a handler error's properties (not its stack) to the CSMS in CALLERROR details. Logs a warning when on; keep it off in production
handlerTimeoutMsnumber0 (off)How long a handler may take to answer an incoming call. Past it, the call is answered InternalError, the handler's signal aborts with a TimeoutError, and handlerError reports it (see Handler signal and time limit)

wsOpts

Passed to new WebSocket() from the ws package, for what the client options do not cover. Node client only: a browser's WebSocket takes no such options.

import { HttpsProxyAgent } from 'https-proxy-agent';

const client = new OCPPClient({
	identity: 'CP-001',
	endpoint: 'wss://csms.example.com/ocpp',
	protocols: ['ocpp1.6'],
	wsOpts: {
		agent: new HttpsProxyAgent('http://proxy.corp.local:3128'), // through a proxy
		localAddress: '192.168.8.20', // leave by this network interface
		maxPayload: 64 * 1024, // refuse messages over 64 KB (the ws default is 100 MiB)
	},
});

Its type, WsClientOptions, leaves out the options the client sets itself, so they cannot be set in two places:

Not in wsOptsUse instead
handshakeTimeoutconnectTimeoutMs
perMessageDeflatecompression
headersheaders (names compared without case, as HTTP does; with password, its Authorization header wins)
ca, cert, key, passphrase, rejectUnauthorized, minVersion, maxVersion, cipherstls, which applies on every security profile
autoPongNothing: pings are always answered (RFC 6455 §5.5.2), since a CSMS disconnects a charger that stops answering them

From plain JavaScript, where the type does not apply, these are dropped and a warning names the option to use instead. closeTimeout (how long a closing connection waits for the CSMS's close frame; ws default 30 s) is accepted although ws's own type definitions do not list it yet.

LoggingConfig

OptionTypeDefaultDescription
enabledbooleantrueEnable or disable all logging.
levelstring'info'Minimum log level (trace, debug, info, warn, error, fatal).
prettifybooleanfalseUse prettyTransport for human-readable output (colors, icons).
exchangeLogbooleanfalseLog all OCPP messages with direction (IN/OUT) and metadata.
loggerLoggerLikeundefinedCustom logger instance (e.g., Pino) to replace the default logger.
handlerfunctionundefinedCustom voltlog-io transport function (entry) => void | Promise<void>
showMetadatabooleantrueShow trailing metadata object in logs.
showSourceMetabooleantrueShow source context (component, identity).
prettifySourcebooleanfalseCompact source tag [OCPPServer/CP-1].
prettifyMetadatabooleanfalseFormat metadata as key=value pairs.

Methods

connect()

Connect to the OCPP server.

await client.connect();

forProtocol(protocol)

The same client, typed for one of its protocols, or undefined while it speaks another or is not connected. A client configured for several versions is typed for all of them until narrowed this way.

const client = new OCPPClient({
	identity,
	endpoint,
	protocols: ['ocpp1.6', 'ocpp2.0.1'],
});
await client.connect();

const v16 = client.forProtocol('ocpp1.6');
if (v16) {
	const auth = await v16.call('Authorize', { idTag: 'ABC' });
	auth.idTagInfo.status; // the 1.6 response
}

call(protocol?, method, params, options?)

Make an RPC call. Throws a TimeoutError or protocol-level Error if the request fails. Make an RPC call. Throws a TimeoutError or protocol-level Error if the request fails.

  • protocol: Optional. e.g., "ocpp1.6". If omitted, generic type inference is used.
  • method: The OCPP method name (e.g., "BootNotification").
  • params: The request payload.
  • options: { timeoutMs?: number, retries?: number, idempotencyKey?: string, signal?: AbortSignal }, or { noReply: true, … } (see below)
// With manual idempotency key to prevent double execution on reconnections
const result = await client.call("ocpp1.6", "BootNotification", { ... }, { idempotencyKey: "unique-boot-123" });

With { noReply: true } (typed NoReplyCallOptions), the CALL is sent and call() resolves with undefined once it is written, without waiting for the answer. The peer still answers, as OCPP-J requires: the answer is recognised by its message ID and dropped quietly while it can still arrive (timeoutMs, default callTimeoutMs), then the ID is forgotten, so nothing piles up and nothing is logged as a stray response. Unlike a call you do not await, it frees the call queue once it is sent, so the next CALL can go out before this one is answered, which OCPP-J §4.1.1 does not allow: use it only with peers known to cope. retries cannot be combined with it. On OCPP 2.1, a message defined as unconfirmed (such as NotifyPeriodicEventStream) uses send() instead.

await client.call(
	'StatusNotification',
	{ connectorId: 1, errorCode: 'NoError', status: 'Available' },
	{ noReply: true }
);

safeCall(protocol?, method, params, options?)

Exactly like call(), except it catches all errors and returns the response payload on success, or undefined on error rather than throwing. Useful for fire-and-forget or localized error handling without breaking loops.

const result = await client.safeCall(
	'ocpp1.6',
	'ClearCache',
	{},
	{ timeoutMs: 15000 }
);

if (result === undefined) {
	client.log.warn('Station failed to clear cache');
}

callImmediate(method, params?, options?)

Sends a CALL at once, skipping the callConcurrency queue, so it can go out while earlier CALLs are still unanswered. That breaks OCPP-J §4.1.1 (Synchronicity): a new CALL waits until the previous one is answered or has timed out, on purpose: use it only with a CSMS known to handle CALLs in parallel, as with callConcurrency above 1. Rejects unless the client is connected; there is no offline queue. server.sendBatch() uses it.

// One StatusNotification per connector after boot, without waiting in turn
await Promise.all([
	client.callImmediate('StatusNotification', {
		connectorId: 1,
		errorCode: 'NoError',
		status: 'Available',
	}),
	client.callImmediate('StatusNotification', {
		connectorId: 2,
		errorCode: 'NoError',
		status: 'Available',
	}),
]);

handle(protocol?, method, handler)

Register an RPC handler.

  • protocol: Optional. If provided, handler only triggers for this protocol version.
  • method: The OCPP method name.
  • handler: (context) => Response | Promise<Response>
// Version specific
client.handle("ocpp1.6", "Reset", ({ params }) => { ... });

// Generic
client.handle("Reset", ({ params }) => { ... });

// Catch-all
client.handle(({ params,method }) => { ... });

// OCPP 2.1 SEND message: never answered, ctx.unconfirmed is true
client.handle("NotifyPeriodicEventStream", ({ params, unconfirmed }) => { ... });

A handler for a SEND message, of OCPP 2.1 (such as NotifyPeriodicEventStream) or of a custom protocol, is registered the same way. Nothing is sent back for a SEND: the return value is ignored, and a thrown error is logged and emitted as handlerError, never sent to the peer.

Handler signal and time limit

ctx.signal aborts when the handler's answer can no longer be delivered:

  • The connection closes before the handler settles. The reason is an Error, Connection closed (code: reason).
  • The handler runs past handlerTimeoutMs. The reason is a TimeoutError. The call is answered InternalError with the code's standard text, and handlerError reports the TimeoutError.

The signal doesn't stop the handler. Pass it on to fetch() or a database driver, or check signal.aborted before a side effect. Whatever the handler returns after its time limit is dropped.

const server = new OCPPServer({ protocols: ["ocpp1.6"], handlerTimeoutMs: 10_000 });

server.on("client", (client) => {
  client.handle("StartTransaction", async ({ params, signal }) => {
    const tx = await db.transactions.create(params, { signal });
    signal.throwIfAborted(); // the charger is gone, or 10 s have passed
    await billing.notify(tx);
    return { transactionId: tx.id, idTagInfo: { status: "Accepted" } };
  });
});

handlerTimeoutMs is off by default. Keep it below the peer's own call timeout, so the peer gets the InternalError before it gives up. For an OCPP 2.1 SEND, which is never answered, only the signal aborts.

An answer goes only on the connection its call came in on. If that connection closes before the handler settles, the answer is dropped and logged at debug level, and no callResult, callError or message event reports it. The same happens when the client has reconnected in the meantime: on the new connection, the answer would reply to a call that connection never made.

send(method, params)

Send an OCPP 2.1 unconfirmed message (RPC type SEND, message type 6). It resolves once the frame is written, because no response exists. It does not wait behind an outstanding call(), as the spec allows a SEND at any time. It works on ocpp2.1 and on a custom protocol, whose SEND messages you declare in OCPPSendMethodMap (ocpp generate does), and throws on OCPP 1.6 and 2.0.1, whose RPC framework has no SEND: a peer answers one with MessageTypeNotSupported. Under strictMode the payload is validated against the message's schema, $id urn:<Message>.

await client.send('NotifyPeriodicEventStream', {
	id: 1,
	pending: 0,
	basetime: new Date().toISOString(),
	data: [{ t: 0, v: '230.4' }],
});

close(options?)

Close the connection gracefully or forcefully.

By default (awaitPending: true), close() waits before closing the socket, so in-flight operations aren't cut off. It waits for two things:

  • The answers to this side's calls.
  • The answers to the peer's calls that handlers are still working on. This wait lasts at most callTimeoutMs. A handler still running then has its signal aborted when the connection closes.

server.close() closes each connection this way. A call that arrives once close() has begun is not handled.

// Graceful close (waits for pending calls, sends 1000 code)
await client.close({ code: 1000, reason: 'Normal Closure' });

// Forceful immediate drop (useful during DDoS or severe errors)
await client.close({ force: true });

// Graceful WS close, but skips waiting for pending asynchronous RPC calls
await client.close({ awaitPending: false });

Properties

log

The client's logger: the one logging configures, which adds the client's identity and component: "OCPPClient" to every message, or a no-op logger when logging is off.

client.log.info('Initializing updates', { pendingFiles: 4 });

OCPPServer

import { OCPPServer } from 'ocpp-ws-io';

Constructor

const server = new OCPPServer(options?: ServerOptions);

The server is typed for its protocols, like the client: new OCPPServer({ protocols: ["ocpp1.6"] }) is an OCPPServer<"ocpp1.6">. Its connections (the client event, getLocalClient(), clients) are OCPPServerClient<"ocpp1.6">, and its routes and their handlers, ctx.accept({ protocol }), sendToClient(), broadcast() and inline plugins take only its versions. A route's config({ protocols }) must be a subset of the server's. Without protocols, or with a list only known at runtime, the server is typed for every version.

A typed server, route or connection still fits a variable or list typed as plain OCPPServer, OCPPRouter or OCPPServerClient, typed for every version as in 2.x. To keep the exact type, use ConnectionOf<typeof server>; for connections of differently configured servers that should not make calls typed for the wrong version, AnyOCPPServerClient. On a server with several versions, client.forProtocol("ocpp1.6") returns the connection typed for the version it negotiated, or undefined:

import { type ConnectionOf, OCPPServer } from 'ocpp-ws-io';

const server = new OCPPServer({ protocols: ['ocpp1.6', 'ocpp2.0.1'] });
const connections = new Map<string, ConnectionOf<typeof server>>();

server.on('client', client => {
	connections.set(client.identity, client);
	client.forProtocol('ocpp1.6')?.handle('Authorize', () => ({
		idTagInfo: { status: 'Accepted' }, // the 1.6 response
	}));
});

ServerOptions

OptionTypeDefaultDescription
protocolsstring[][]Accepted OCPP subprotocols. Left empty, the server takes the charger's first offer, and a charger that offers none connects with no protocol instead of being closed with 1002 (see Routing, Server & Router Execution Flow)
securityProfileSecurityProfileNONESecurity profile
strictModeboolean | string[]falseValidate every message against the official OCPP JSON schemas. Off by default, so a payload with missing fields or wrong types reaches your handler. Set true (or a list of protocols) for the CALLERROR that 2.0.1 / 2.1 §4.1.7 requires. Needs protocols
idGenerator() => string—Creates the message ID of each CALL sent to a charging station. A per-call idempotencyKey wins; without either, a random UUID
idValidator(id: string) => boolean—Checks each incoming CALL / SEND message ID, in any mode, in place of the built-in check (see Message IDs)
requireBasicAuthbooleantrueOn profile 1 or 2, answer HTTP 401 when Basic Auth credentials are missing or the username is not the identity. false leaves it to the auth callback
isKnownIdentity(identity, handshake) => boolean | Promise<boolean>—Unknown identity → HTTP 404 before middleware and auth (OCPP-J §3.2). A throw → 500
duplicateConnection"replace" | "reject""replace"A charger that is already connected: replace the old connection (closed with 4000, "Evicted by new connection"), or refuse the new one with HTTP 409
maxIdentityLengthnumber—Longest identity accepted, in any mode; replaces the spec's 48. Without it, strictMode applies the spec's identity rules per version. Violation → HTTP 400
handshakeTimeoutMsnumber30000Timeout for WebSocket handshake (ms)
tlsTLSOptions—TLS options (Profile 2 & 3)
loggingLoggingConfig{}Configuration for structured logging.
sessionTtlMsnumber7200000Garbage collection inactivity timeout
rateLimitRateLimitOptions—Token bucket socket & method limiter
healthEndpointboolean | HealthEndpointOptionsfalseExpose HTTP /health and /metrics. Pass true for open access, or { auth } to require bearer token or basic auth. Missing or empty credentials throw when the server is created. Without auth, listen() logs a warning when it starts serving them.
maxPayloadBytesnumber65536Max WebSocket frame size in bytes. Rejects oversized messages at the transport layer before any parsing. Prevents OOM from malicious payloads.
wssOptionsWsServerOptions—Raw ws server options: maxFragments, maxBufferedChunks, allowSynchronousEvents, skipUTF8Validation, a WebSocket subclass, closeTimeout (see wssOptions below)
maxBadMessagesnumber50Close a connection on its Nth malformed message in a row. Every valid message resets the count and empty frames are ignored. Set 0 for zero tolerance, Infinity for no limit (logs a warning).
badMessageWindowMsnumber—Also reset a run of bad messages once it is older than this many ms (a valid message always resets it). Example: { maxBadMessages: 10, badMessageWindowMs: 60_000 } closes on the 10th bad message in a row within a minute.
workerThreadsboolean | objectfalseEnable worker thread pool for JSON parsing. true = auto pool size, { poolSize, maxQueueSize } for fine-tuning.
compressionboolean | CompressionOptionsfalseAccept WebSocket permessage-deflate compression from chargers that offer it. Off by default for memory and CPU; OCPP 2.0.1 / 2.1 require a CSMS to support it (see Compression)
headersTimeoutnumber30000Max time (ms) to receive HTTP headers on servers created by listen(). Prevents HTTP slowloris attacks. Keep it at or below requestTimeout. 0 disables. User-provided servers are unaffected.
requestTimeoutnumber30000Max time (ms) for the complete HTTP request on servers created by listen(). Works with headersTimeout to harden the HTTP layer. 0 disables.
callConcurrencynumber1Outbound CALLs in flight per charger. 1 follows OCPP-J §4.1.1 (Synchronicity): a new CALL waits until the previous one is answered or has timed out. Above 1 only for chargers known to cope; sendBatch() pipelines regardless
respondWithDetailedErrorsbooleanfalseSend a handler error's properties (not its stack) to chargers in CALLERROR details. Logs a warning when on; keep it off in production
handlerTimeoutMsnumber0 (off)How long a handler may take to answer a charger's call; every connection takes it when it connects, and client.reconfigure() changes it for one. Past it, the call is answered InternalError (see Handler signal and time limit)

wssOptions

Passed to new WebSocketServer() from the ws package, for what the server options do not cover. Changes through reconfigure() apply to new connections.

const server = new OCPPServer({
	protocols: ['ocpp2.0.1'],
	wssOptions: {
		maxFragments: 64, // close (1008) on a message split into more fragments
		maxBufferedChunks: 4096, // the same for buffered chunks of one message
		closeTimeout: 5_000, // destroy a closing socket after 5 s without the charger's close frame
	},
});
Optionws defaultUse
maxFragments, maxBufferedChunks16384, 262144Lower them on a server exposed to the internet: limits against fragment floods
allowSynchronousEventstruefalse spreads a burst of messages from one charger over several event-loop turns
skipUTF8ValidationfalseFaster parsing, but invalid UTF-8 then reaches the parser instead of closing the connection with 1007 as RFC 6455 requires
WebSocketws's WebSocketA subclass, e.g. for instrumentation
closeTimeout30000How long a closing connection waits for the charger's close frame; accepted although ws's type definitions do not list it yet

Its type, WsServerOptions, leaves out the options the server sets itself:

Not in wssOptionsUse instead
port, host, server, noServer, backloglisten(port, host, { server }) or handleUpgrade; backlog on your own HTTP server
pathroute()
handleProtocolsprotocols
verifyClientauth(), middleware or isKnownIdentity
maxPayloadmaxPayloadBytes
perMessageDeflatecompression
clientTrackingNothing: always on, stats() needs it
autoPongNothing: charger pings are always answered (RFC 6455 §5.5.2)

From plain JavaScript, where the type does not apply, these are dropped and a warning names what to use instead.

CompressionOptions

OptionTypeDefaultDescription
thresholdnumber1024Minimum payload size (bytes) to compress.
levelnumber6zlib compression level (1=fastest, 9=smallest).
memLevelnumber8zlib memory level (1–9).
serverNoContextTakeoverbooleantrueDon't retain server deflate context (saves ~120KB/conn).
clientNoContextTakeoverbooleantrueDon't retain client deflate context.

TLSOptions

Used by ServerOptions.tls (profiles 2 and 3) and ClientOptions.tls.

OptionTypeDefaultDescription
certstring | Buffer | Array<…>—Certificate(s), PEM. Pass an RSA and an ECDSA one to serve all four cipher suites OCPP 2.0.1 requires
keystring | Buffer | Array<…>—Private key(s), PEM, in the same order as cert
castring | Buffer | Array<…>—CA certificate(s) for verification
passphrasestring—Passphrase of an encrypted key
rejectUnauthorizedbooleantrueReject peers whose certificate does not verify
minVersion"TLSv1" | "TLSv1.1" | "TLSv1.2" | "TLSv1.3""TLSv1.2"Lowest TLS version. Lower it only for legacy OCPP 1.6 charge points
maxVersionsame as minVersion"TLSv1.3"Highest TLS version
ciphersstringNode'sOpenSSL cipher list

Compression

WebSocket compression (RFC 7692 permessage-deflate) compresses every message with DEFLATE. It is agreed per connection during the handshake: the client offers it, and it is used only if the server accepts. When either side does not use it, messages go uncompressed and nothing breaks.

VersionWhat the spec says
OCPP 1.6J §5.1Allowed as part of WebSocket; no other form of compression recommended
OCPP 2.0.1 §3.3 / 2.1 §3.4A CSMS SHALL support it. A charging station may; RECOMMENDED on mobile data

It is off by default on the server and on OCPPClient, because it costs memory and CPU on every connection, and Node's zlib can fragment memory under high concurrency. Turn it on with compression: true, or pass CompressionOptions to tune it:

// CSMS: accept compression from chargers that offer it (needed for full 2.0.1 / 2.1 compliance)
const server = new OCPPServer({ protocols: ['ocpp2.0.1'], compression: true });

// Charging station on mobile data: offer it to the CSMS
const client = new OCPPClient({
	identity: 'CP001',
	endpoint: 'wss://csms.example.com/ocpp',
	compression: { level: 6 },
});

The defaults keep the cost low: messages under 1 KB (most OCPP traffic, such as Heartbeat) are sent uncompressed, and no compression context is kept between messages. BrowserOCPPClient has no such option: the browser decides on its own, and all major browsers offer compression.

HealthEndpointOptions

OptionTypeDefaultDescription
authHealthEndpointAuth—Access control. Omit for open access (same as healthEndpoint: true). Missing or empty values throw at startup.

HealthEndpointAuth — one of:

ShapeFieldsDescription
Bearer{ bearer: string }Expects Authorization: Bearer <token> header. Compared in constant time.
Basic{ username: string, password: string }Expects Authorization: Basic base64(user:pass) header. Both fields compared in constant time.
// Bearer token (Prometheus, Grafana, OCPP board)
healthEndpoint: { auth: { bearer: process.env.METRICS_TOKEN! } }

// Basic auth
healthEndpoint: { auth: { username: "admin", password: "s3cret" } }

// No auth (dev / internal network)
healthEndpoint: true

RateLimitOptions

OptionTypeDefaultDescription
limitnumber—Max messages allowed globally per window
windowMsnumber—Time window in milliseconds
onLimitExceeded"ignore" | "disconnect" | function"ignore"Action to execute when limit is breached
methodsRecord<string, { limit; windowMs }>—Deep method-wise limits (e.g. Heartbeat)

CORSOptions

OptionTypeDefaultDescription
allowedOriginsstring[]—Origins allowed to connect via WebSocket
allowedIPsstring[]—Exact IPv4/v6 or CIDR IP ranges (e.g. /24)
allowedSchemes("ws" | "wss")[]—Restrict connections to specific protocols

Methods

listen(port, host?, options?)

Start listening on a port. Returns the http.Server instance.

auth(handler)

Attach authentication logic.

server.auth((ctx) => {
  // ctx.handshake has .identity, .pathname, .url, .endpoint, .headers, etc.
  ctx.accept({ session: { ... } }); // Attach session data to client
});

On a typed server, ctx.accept({ protocol }) takes only its protocols. Callbacks from defineAuth() or combineAuth(), or typed AuthCallback, fit any server; written inline in auth(), they take that server's protocols.

cors(options)

Apply CORS rules directly to the server or router levels. This protects endpoints from unauthorized WebSocket hijacking or public IP access.

server.cors({
	allowedOrigins: ['https://dashboard.example.com'],
	allowedIPs: ['192.168.1.0/24', '10.0.0.5'],
	allowedSchemes: ['wss'],
});

on("client", handler)

Listen for new connections.

server.on('client', client => {
	// client is an OCPPServerClient (extends OCPPClient)
	console.log(client.session); // Access session data
});

on("message", handler)

Every frame of every connection, received ("IN") and sent ("OUT"). This is each connection's own message event, with the connection added as the second argument.

server.on('message', ({ message, direction, ctx }, client) => {
	audit.log(client.identity, direction, message);
});

updateTLS(options)

Hot-reload TLS certificates on all active HTTPS servers without dropping connections. Only valid when using SecurityProfile.TLS_BASIC_AUTH or TLS_CLIENT_CERT.

server.updateTLS({
	cert: fs.readFileSync('./certs/server.crt'),
	key: fs.readFileSync('./certs/server.key'),
});

See TLS Certificate Hot-Reload for full details.

on("securityEvent", handler)

Emitted for every security-relevant action. Useful for SIEM, alerting, and audit logging.

server.on('securityEvent', event => {
	// event.type: "AUTH_FAILED" | "CONNECTION_RATE_LIMIT" | "UPGRADE_ABORTED"
	siem.log(event);
});

See Security Event Monitoring for full details.

sendToClient(identity, protocol?, method, params, options?)

Helper method to dispatch an outgoing RPC call to a specific connected client by its identity. Throws an error if the station is not connected. Matches the client.call() signature and supports timeoutMs in the options argument.

const result = await server.sendToClient(
	'CP-001',
	'ocpp1.6',
	'Reset',
	{
		type: 'Soft',
	},
	{ timeoutMs: 10000 }
);

safeSendToClient(identity, protocol?, method, params, options?)

Exactly like sendToClient(), but catches errors (and disconnected station errors), returning the response payload on success, or undefined on error. Highly recommended for bulk operations across your cluster where individual failures shouldn't throw exceptions.

const result = await server.safeSendToClient(
	'CP-001',
	'ocpp1.6',
	'UpdateFirmware',
	{
		location: 'https://firmware.example.com/v2.bin',
		retrieveDate: new Date().toISOString(),
	},
	{ timeoutMs: 30000 }
);

if (result !== undefined) {
	console.log('Firmware update accepted');
}

sendBatch(identity, calls)

Sends several CALLs to one charger without waiting for each answer, for a faster warm-up after a reconnect. This pipelines on purpose and breaks OCPP-J §4.1.1 (Synchronicity): a new CALL waits until the previous one is answered or has timed out: use it only with chargers known to cope, as with callConcurrency above 1. Results come back in the order of calls; a call that failed gives undefined. In a cluster, a charger connected to another node gets each call through sendToClient() instead.

const [config, , trigger] = await server.sendBatch('CP-101', [
	{
		method: 'GetConfiguration',
		params: { key: ['MeterValueSampleInterval'] },
	},
	{
		method: 'ChangeAvailability',
		params: { connectorId: 0, type: 'Operative' },
	},
	{
		method: 'TriggerMessage',
		params: { requestedMessage: 'StatusNotification' },
	},
]);

Each call is typed by its method, and the results by their actions. A call to an action the types do not know takes unchecked(): { method: unchecked("VendorPing"), params: { n: 1 } }.

broadcastBatch(identities, method, params, options?)

Broadcast an identical RPC call to many stations concurrently. For clustered architectures, unicast commands are grouped and dispatched in pipeline batches via the adapter's streams, reducing per-message TCP round-trip overhead.

const targets = ['CP-001', 'CP-002', 'CP-003', ...thousands];

await server.broadcastBatch(targets, 'Reset', { type: 'Hard' });

stats()

Returns instantaneous observability metrics about the server's local node status. This is deeply integrated with standard Node.js internals to supply immediate health checks for Prometheus and Loki collection.

const telemetry = server.stats();

console.log(`Active Managed Sessions: ${telemetry.activeSessions}`);
console.log(`Active Raw WebSockets: ${telemetry.webSockets?.total}`);
console.log(
	`Pending Network Flushes: ${telemetry.webSockets?.bufferedAmount} bytes`
);
console.log(`Memory Used: ${telemetry.memoryUsage.heapUsed} bytes`);
console.log(`Uptime: ${telemetry.uptimeSeconds}s`);
console.log(`PID: ${telemetry.pid}`);

adapterMetrics()

Fetches the clustering adapter's metrics, or null without an adapter (or when it fails). The RedisAdapter returns pendingMessages, the entries in this node's streams (an upper bound of the backlog: consumed entries count until they are trimmed), with activeStreams, streamDetails (entries per stream) and pollErrors (non-zero while delivery into this node is failing).

const metrics = await server.adapterMetrics();
console.log('Redis backlog:', metrics?.pendingMessages);

Properties

log

The server's logger: the one logging configures, with component: "OCPPServer" in every message, or a no-op logger when logging is off. The router and the exchange logs use it too.

server.log.warn('Unrecognized protocol attempted', { requested: 'ocpp1.5' });

Protocol Constants

NOREPLY

Return NOREPLY from a handler to suppress the automatic response. This is useful for asynchronous processing where you might send a response later manually, or for specific OCPP flows.

import { NOREPLY } from 'ocpp-ws-io';

client.handle('StatusNotification', ({ params }) => {
	// Process the notification but don't send a response immediately
	return NOREPLY;
});

An async handler can return it too, after its work:

client.handle('StatusNotification', async ({ params }) => {
	await queue.push(params);
	return NOREPLY;
});

unchecked(action)

Marks an action name the types do not check, for every typed method (call, safeCall, callImmediate, handle, send, removeHandler, sendToClient, safeSendToClient, broadcast, broadcastBatch, and a sendBatch call's method): an undeclared vendor action, a name passed through from the wire, or a test that sends an invalid payload on purpose. Returns the name unchanged.

import { unchecked } from 'ocpp-ws-io';

const res = await client.call(unchecked('VendorPing'), { n: 1 }); // a JSON object
client.handle(unchecked('VendorPing'), () => ({ pong: true }));

Typed methods otherwise take only the actions their protocols define, and reject keys the OCPP schema does not define; see Type Safety.

Message types

MessageType holds the RPC message type numbers: CALL (2), CALLRESULT (3), CALLERROR (4), and the OCPP 2.1 types CALLRESULTERROR (5) and SEND (6).

ReceivedOn ocpp1.6 / ocpp2.0.1On ocpp2.1
SEND (6)Unknown type (see below)Delivered to its handler, never answered
CALLRESULTERROR (5)Unknown type (see below)callResultError event, never answered
Any other unknown numberCALLERROR MessageTypeNotSupported, ID "-1"CALLERROR MessageTypeNotSupported, ID "-1"
Message ID cannot be read (invalid JSON, not an array, type not a number, ID not a string)CALLERROR, ID "-1": GenericError on 1.6, RpcFrameworkError on 2.0.1CALLERROR RpcFrameworkError, ID "-1"
Malformed CALLAnswered with CALLERRORAnswered with CALLERROR
Malformed or schema-invalid CALLRESULTNot answeredAnswered with CALLRESULTERROR
Malformed CALLERRORNot answeredNot answered

An unknown message type counts as a bad message (emitting badMessage), and it is answered with a CALLERROR MessageTypeNotSupported under message ID "-1", since the frame's own ID is not read for a type the protocol does not define. A frame whose message ID cannot be read is answered the same way with RpcFrameworkError, as 2.0.1 and 2.1 §4.2.3 require ("When also the MessageId cannot be read, the CALLERROR SHALL contain "-1""), and with GenericError on OCPP 1.6, whose error-code table has no RpcFrameworkError. A CALLERROR otherwise only answers a CALL, so two peers cannot loop by answering each other's errors.

A CALL that reuses the message ID of a CALL still being handled is answered with RpcFrameworkError on OCPP 2.0.1 and 2.1, and with GenericError on OCPP 1.6, whose error-code table has no RpcFrameworkError.

The error description of an outgoing CALLERROR or CALLRESULTERROR is cut to 255 characters on OCPP 2.0.1 and 2.1, where ErrorDescription is string[255]. OCPP 1.6 sets no limit, so descriptions are sent in full there.

Message IDs

A message ID is a string of at most 36 characters, unique for the sender across reconnects (OCPP-J §4.1.4; 2.0.1 errata 2023-12).

Behaviour
Outgoing CALL / SENDThe per-call idempotencyKey if given, else idGenerator if set, else a random UUID (36 characters). A call rejects before sending if the ID is empty or already used by a pending call.
Incoming CALL / SEND, idValidator setidValidator decides, with or without strictMode. Returning false or throwing rejects the ID.
Incoming CALL / SEND, strictMode onIDs over 36 characters are rejected.
Incoming CALL / SEND, otherwiseAny string is accepted.
ID is not a stringCALLERROR under ID "-1": RpcFrameworkError on 2.0.1 and 2.1, GenericError on 1.6 (see Message types).

A rejected CALL is answered under message ID "-1" with RpcFrameworkError on OCPP 2.0.1 and 2.1, and GenericError on OCPP 1.6, whose error-code table has no RpcFrameworkError. A rejected SEND is not answered. Both count as bad messages. The browser client has no strictMode, so it checks incoming IDs only through idValidator.


BrowserOCPPClient

import { BrowserOCPPClient } from 'ocpp-ws-io/browser';

A browser-compatible OCPP client with the same typed API as OCPPClient. Uses the native browser WebSocket API — no Node.js dependencies.

See Browser Client for the full guide with examples. See Browser Client for the full guide with examples.

Constructor

const client = new BrowserOCPPClient(options: BrowserClientOptions);

BrowserClientOptions

OptionTypeDefaultDescription
identitystringrequiredCharging station ID
endpointstringrequiredWebSocket URL (ws:// or wss://)
protocolsstring[][]OCPP subprotocols to negotiate
queryRecord<string, string> | string—URL query parameters, as an object or a query string ("a=1&b=2", leading ? optional); added after any query in endpoint
reconnectbooleantrueAuto-reconnect on disconnect
maxReconnectsnumberInfinityMax reconnection attempts
backoffMinnumber1000First reconnect delay (ms), doubled after each failed attempt; every wait adds up to 25% at random
backoffMaxnumber30000Where the doubling stops (ms); the random 25% still applies on top
callTimeoutMsnumber30000Default RPC call timeout (ms)
handlerTimeoutMsnumber0 (off)How long a handler may take to answer an incoming call. Past it, the call is answered InternalError, the handler's signal aborts with a TimeoutError, and the error is logged
callConcurrencynumber1Outbound CALLs in flight at once. 1 follows OCPP-J §4.1.1 (Synchronicity): a new CALL waits until the previous one is answered or has timed out. Above 1 only for a CSMS known to cope
maxBadMessagesnumber50Close on the Nth bad message in a row; a valid message resets the count. 0 = zero tolerance, Infinity = no limit (logs warning).
badMessageWindowMsnumber—Also reset a run of bad messages once it is older than this many ms.
respondWithDetailedErrorsbooleanfalseSend a handler error's properties (not its stack) in CALLERROR details. Logs a warning when on; keep it off in production
loggingLoggingConfig{}Configuration for structured logging.

Methods

All methods match OCPPClient:

await client.connect();

// Version-aware typed call
const result = await client.call("ocpp1.6", "BootNotification", { ... });
const heartbeat = await client.safeCall("ocpp1.6", "Heartbeat", {}); // the response, or undefined if it failed

// Register handlers (version-specific, generic, wildcard)
client.handle("ocpp1.6", "Reset", ({ params }) => { ... });
client.handle("Reset", ({ params }) => { ... });
client.handle(({ params ,method}) => { ... });

// Close
await client.close();
await client.close({ force: true });
await client.close({ awaitPending: true });

// Runtime reconfiguration
client.reconfigure({ callTimeoutMs: 10000 });

// Handler removal
client.removeHandler("Reset");
client.removeHandler("ocpp1.6", "Reset");
client.removeAllHandlers();

// Raw message
client.sendRaw(JSON.stringify([2, "uuid", "Heartbeat", {}]));

Properties

Properties

log

The browser client's logger, writing to the browser console, or a no-op logger when logging is off.

client.log.info('Retrying connection...');

Events

client.on('open', event => {
	/* connected */
});
client.on('close', ({ code, reason }) => {
	/* disconnected */
});
client.on('closing', () => {
	/* close() started: no reconnect follows; close fires once the socket has closed */
});
client.on('error', error => {
	/* error */
});
client.on('connecting', ({ url }) => {
	/* attempting */
});
client.on('reconnect', ({ attempt, delay }) => {
	/* reconnecting */
});
client.on('message', ({ message, direction, ctx }) => {
	/* OCPP message */
	/* message direction */
	/* ctx context form client */
});
client.on('call', call => {
	/* incoming call */
});
client.on('callResult', result => {
	/* result received */
});
client.on('callError', error => {
	/* error received */
});
client.on('callResultError', frame => {
	/* OCPP 2.1: the peer could not process a CALLRESULT we sent */
});
client.on('badMessage', ({ message, error }) => {
	/* malformed */
});

Error Classes

All error classes are exported for instanceof checks:

import {
	// Base errors
	TimeoutError, // Call timeout
	UnexpectedHttpResponse, // Non-101 upgrade response
	WebsocketUpgradeError, // WebSocket upgrade failure

	// RPC errors (OCPP-J spec Section 4.3)
	RPCGenericError,
	RPCNotImplementedError,
	RPCNotSupportedError,
	RPCInternalError,
	RPCProtocolError,
	RPCSecurityError,
	RPCFormationViolationError,
	RPCPropertyConstraintViolationError,
	RPCOccurrenceConstraintViolationError,
	RPCOccurenceConstraintViolationError, // 1.6 spelling, one `r`
	RPCTypeConstraintViolationError,
	RPCFormatViolationError, // 2.0.1 and 2.1 spelling
	RPCFrameworkError,
	RPCMessageTypeNotSupportedError,
} from 'ocpp-ws-io';

A handler or connection middleware that throws an RPC error answers the CALL with it: its code, its own message as the description (the code's standard text when it has none), and the details it carries. Any other error is answered InternalError with that code's standard text, so its message stays on your side; with respondWithDetailedErrors its properties go in details.

import { createRPCError } from 'ocpp-ws-io';

client.handle('Reset', () => {
	throw createRPCError('NotSupported', 'Reset is off on this site', { retryAfter: 3600 });
});
// The peer receives [4, id, "NotSupported", "Reset is off on this site", { "retryAfter": 3600 }]

A format violation goes out under the name of the connection's version, whichever of the two names was thrown: FormationViolation on OCPP 1.6, FormatViolation on 2.0.1 and 2.1. OCPP-J 1.6 §4.2.3 names it FormationViolation, and the 1.6-J errata (§5.2) keep that name on the wire. Every other code goes out as thrown. That includes 1.6's OccurenceConstraintViolation, which the errata keep as well (§5.1).

On this page