Skip to main content
Everything on this page is the same for both protocols. When you are ready to write capture code, go to the guide for the protocol your service speaks.

OCPP server

Capture WebSocket frames from your charge points.

OCPI server

Capture roaming exchanges with your partners.

Install

Every SDK compresses batches with zstd. Go bundles it through one dependency, Node uses 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

See Networks for settings, scoping, and rotation.

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.
Swap 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.
Log the startup fault. An inert client behaves exactly like a healthy one on an idle system, so a swallowed config error looks identical to “no traffic yet”.

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.
Close is idempotent, never raises, and captures after it are safe no-ops.
  • Go. Shutdown(ctx) uses your context deadline, Close() uses DrainTimeout. Both return ErrDrainIncomplete if the deadline passed with messages still buffered.
  • Node. close(timeoutMs?) resolves to whether it drained. The flush timer is unref’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 across fork() 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

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.
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.
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.
The ingestion API validates each message independently and drops what fails, after returning success. The usual causes, in order:
  1. An OCPP message with no tenant pair. The API requires it even though the SDKs treat it as optional.
  2. An OCPI exchange with no status code. It must be 100 to 599.
  3. An identity field over its limit. Tenant name is 32 characters, the rest are 64.
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.
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.
Still stuck? Mail support@evpanda.io with the SDK and version, the network name and protocol, and a counter snapshot covering a minute of live traffic.

Next

OCPP server

The full integration in Go, Node, and Python.

OCPI server

The same, plus the HTTP adapters.