OCPP server
Capture WebSocket frames from your charge points.
OCPI server
Capture roaming exchanges with your partners.
Install
node:zlib (which is why it needs Node 22.15, where zstd landed), and Python brings zstandard.
Optional extras, needed only for the adapter you use:
Get an API key
1
Create a network
In the dashboard, create a Charger network for an OCPP CSMS or a Roaming network for an OCPI server.
2
Generate a key
Under the network’s Settings tab. The raw value is shown once.
3
Export it
Start a client
The protocol is the client. Start one at boot and share it across your process; each client runs one background worker and one buffer. The API key is the only thing you must supply.endpoint defaults to the production ingestion API, so leave it unset unless you are pointing at another environment.
OCPP for OCPI if that is what your service speaks. Both clients are safe for concurrent use.
A bad config never crashes your boot
start always returns a usable client. If the config is bad you get an inert one that captures nothing, and the reason comes back as an error (Go) or on .error (Node, Python).
Only two things can fail: a missing API key and a malformed endpoint. Both are matchable, which is useful because a missing key is usually a deployment problem while a bad endpoint is a code one.
Configuration
Every field except the API key takes its default when left at its zero value. An out of range value falls back to that default with a warning rather than failing.
Go takes
time.Duration, Node takes milliseconds, Python takes float seconds.
Sizing the buffer
maxBufferBytes caps everything waiting to be delivered. That is the number to provision against; the SDK evicts rather than exceed it.
Size it for the stall you want to survive: message rate times average size times seconds. At 400 messages per second and 500 bytes each, roughly a 10,000 charger CSMS, one minute of stalled delivery is about 12 MB. The 32 MiB default covers about two and a half minutes.
The HTTP adapters add a second, smaller cost. While a request is in flight they hold a copy of its bodies, bounded per request by maxCaptureBytes and released as soon as the exchange is captured. That scales with concurrency rather than with the buffer. Calling the capture methods yourself avoids it, since you already hold the bytes.
How delivery works
One background worker per client. It flushes when 1,000 messages are waiting or the flush interval elapses, whichever comes first, compresses the batch, and posts it.
Retries are not configurable. An SDK on a customer hot path that retries aggressively against a struggling API turns a partial outage into a full one.
Under sustained failure the buffer fills and the SDK evicts its own oldest captures. It never applies backpressure to your service.
Is it working?
stats() is a snapshot of the client’s delivery counters. It is always available, including on an inert or closed client, and each counter maps to one root cause.
It is a pull based snapshot, so it feeds Prometheus, OpenTelemetry, or a log line without the SDK depending on any of them.
Logging
The SDK reports problems to your logger by default, at a bounded rate: at most one summary line per minute, and nothing at all while healthy.
Set
logMode in config, or EVPANDA_LOG=silent|errors|debug in the environment to change it without a code change. Config wins over the environment.
Shutdown
Stop accepting traffic first, then drain.- Go.
Shutdown(ctx)uses your context deadline,Close()usesDrainTimeout. Both returnErrDrainIncompleteif the deadline passed with messages still buffered. - Node.
close(timeoutMs?)resolves to whether it drained. The flush timer isunref’d, so it never holds your process open, which also means an unclosed client loses what it captured since the last flush. - Python.
close(timeout=None)returns whether it drained. Clients are context managers, a client the host never closes is drained at interpreter exit, and a client inherited acrossfork()restarts its delivery thread in the child.
flush() forces an immediate delivery and waits for it. It blocks for as long as the transport retries take, so use it at shutdown or while debugging, never on a request path.
Troubleshooting
Nothing arrives and captured is 0
Nothing arrives and captured is 0
Either the client is inert or the capture calls are not running.Check the startup fault. Confirm
EVPANDA_API_KEY is set in the environment your service actually runs in; set in your shell but absent from the container is the classic miss.Nothing arrives and droppedInvalid is climbing
Nothing arrives and droppedInvalid is climbing
Identity resolution is failing. This is the most common integration fault.For OCPI, check that something is actually stamping the partner:
setIdentity on the request, use_identity around the call, the context, or a resolver. See Identity.Nothing arrives and droppedUndeliverable is climbing
Nothing arrives and droppedUndeliverable is climbing
Capture works, delivery does not. Check egress:If that fails, allowlist
ingest.evpanda.io on port 443. If it succeeds, suspect the key: a revoked or wrong protocol key returns 401, which is permanent and never retried. An OCPP client needs a charger network key, an OCPI client a roaming network key.Messages deliver but do not show up
Messages deliver but do not show up
The ingestion API validates each message independently and drops what fails, after returning success. The usual causes, in order:
- An OCPP message with no tenant pair. The API requires it even though the SDKs treat it as optional.
- An OCPI exchange with no status code. It must be 100 to 599.
- An identity field over its limit. Tenant name is 32 characters, the rest are 64.
droppedOversize is high
droppedOversize is high
A body or frame over
maxCaptureBytes, 64 KiB by default, drops the whole message. Raise the cap; 256 KiB is reasonable for a roaming server carrying large location or CDR payloads. Raise the buffer ceiling with it.Memory grew after adding the SDK
Memory grew after adding the SDK
Expected and bounded. The buffer holds undelivered captures up to
maxBufferBytes. If that is more than your pod can afford, lower it. A healthy client idles far below its ceiling.Next
OCPP server
The full integration in Go, Node, and Python.
OCPI server
The same, plus the HTTP adapters.