Skip to main content
Instrumenting a service takes three things: a network and an API key from the dashboard, an SDK in your process, and a few capture calls where you already handle protocol traffic. This page covers everything the two protocols share. When you’re ready to write 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.

Supported languages

Availability, honestly. The Go SDK is published and is the reference implementation the others track.The npm registry currently serves an older @evpanda/sdk; the API documented here is 0.1.0. Until it ships, the OCPP session handle and the OCPI adapters are not available — the flat capture calls are.The Python SDK is not on PyPI yet. Install it from source, and note it has no HTTP adapters:

Source and API reference

Optional compression dependency

Compression defaults to zstd, which needs one optional package in Node and Python. Without it the SDK falls back to gzip silently.

What the SDK does

The SDK is passive. It observes traffic your application already handles — it never sits in the request path, never blocks on the network, and never fails your process if EVPanda is unreachable.
A capture call validates the identity, enforces a size cap, redacts secrets, copies the payload, writes it to an in-process buffer, and returns. No I/O, no locks your code contends on. Delivery happens on a background worker.
The buffer has a ceiling you set. When delivery stalls the SDK evicts its oldest captures rather than growing without limit or applying backpressure to your service.
OCPI headers pass through an allowlist before anything is buffered, so Authorization, Cookie, and API keys are dropped at capture. Tokens in OCPI /credentials bodies are masked. See What gets captured.
If the endpoint or API key is wrong the client starts inert: every capture is a no-op and the error is reported to you. Your service still boots.

Get an API key

A network is the container for everything EVPanda records about one of your systems, and an API key is scoped to exactly one network.
1

Create a network

In app.evpanda.io, create a Charger network for an OCPP CSMS or a Roaming network for an OCPI server. One per system, per environment.
2

Generate a key

Open the network’s Settings tab and generate a key. The raw value — evp_sk_… — is shown once. EVPanda stores only a hash of it.
3

Put it in the environment

Every SDK falls back to EVPANDA_API_KEY when no key is passed in code, which keeps the key out of your source.
See Networks for network settings, key rotation, and what a key can and cannot do.

Start a client

The protocol is the client. OCPI and OCPP have separate client types, separate configs, and separate API keys. Start one at boot and share it across your process — each client runs its own background worker and its own buffer.
Swap OCPP for OCPI if that’s what your service speaks. Both clients are safe for concurrent use.
Log the startup error. An inert client behaves exactly like a healthy one on an idle system — silent, no panics, no failures — so a swallowed config error looks identical to “no traffic yet”.In Go the error comes back from Start*. In Node and Python nothing is reported unless debug is on, which is why both examples above turn it on.

Configuration

endpoint and apiKey are hard-required. Every other option is a tunable: an out-of-range value falls back to its default and says so in the logs, rather than failing.
Intervals are time.Duration. The buffer is capped by bytes, so the number you set is the memory footprint, not an estimate of it.
LogMode is LogModeSilent, LogModeErrors (default), or LogModeDebug. The EVPANDA_LOG environment variable sets it without a code change; an explicit config value wins.

Sizing the buffer

The buffer absorbs a delivery stall. Size it for how long you want to survive one: message rate × average size × seconds. At 400 messages/second and 500 bytes each — roughly a 10,000-charger CSMS — one minute of stalled delivery is about 12 MB. The 32 MiB Go default covers about two and a half minutes of that: enough for a blip, small enough for an ordinary container limit.
Don’t set the buffer ceiling below the per-capture cap. A full-size capture could then never fit, so every large message would be dropped after being redacted.

How delivery works

One background worker per client owns delivery. It flushes when 1,000 messages are waiting or the flush interval elapses, whichever comes first, compresses the batch, and POSTs it to https://ingest.evpanda.io. Retries are deliberately not configurable: an SDK on a customer’s hot path that retries aggressively against a struggling API turns a partial outage into a full one.

Where data can be lost

Five places, all counted:

Shut down cleanly

Stop accepting traffic first, then drain. Draining while chargers or partners are still sending is a race you’ll lose.
Close is idempotent, and captures made after it are safe no-ops.
Node: close() returns a promise. Not awaiting it is the difference between draining the buffer and discarding it.Go: Shutdown(ctx) uses your context’s deadline; Close() uses the configured DrainTimeout. Both return ErrDrainIncomplete if the deadline passed with messages still buffered.Python: close() blocks while it drains, up to drain_timeout. Call it from your shutdown path, not from a request handler.
flush() forces an immediate delivery and waits for it — useful in tests and at shutdown, never on a request path.

Check that it’s working

1

Confirm the client is live, not inert

In Go, the error from Start* is nil. In Node and Python, debug is on and no config error appeared at startup.
2

Drive real traffic

Connect a charge point, or make one OCPI call in each direction. Synthetic traffic that skips your auth layer won’t exercise identity resolution — the part that usually breaks.
3

Wait one flush interval

Five seconds by default, or call flush().
4

Check the dashboard

Messages appear under your network. Chargers and platforms are created automatically the first time they send traffic, so a new entity appearing is itself confirmation.
5

Check both directions

The step people skip. Confirm you see frames you sent as well as frames you received. Half-instrumented is the most common production state, and it silently disables the most valuable checks.

Counters and logs

Go exposes the delivery counters directly, on any client — including an inert or closed one:
They are always on, so you can feed them straight into your metrics:
Go also logs by default, at a bounded rate — at most one summary line per minute, and nothing at all while healthy:
Node and Python don’t expose a counters API yet. Turn on debug to get the same picture in your logs, and use the dashboard’s message volume to confirm an integration is healthy.

What to alert on

Alert on rates, not totals: the counters are monotonic since client start, so a total tells you something once went wrong while a rate tells you it is going wrong now.

Troubleshooting

Either the client is inert or the capture calls aren’t running.Check the startup error (Go) or turn on debug (Node, Python). Confirm EVPANDA_API_KEY is set in the environment your service actually runs in — set in your shell, absent from the container is the classic miss — and that endpoint is https://ingest.evpanda.io with no path.
Identity resolution is failing. This is the most common integration fault by a wide margin.For OCPI, the usual cause is a capture adapter mounted before the middleware that authenticates the partner, so nothing has an identity yet. Move capture after auth. See Identity.
Capture works, delivery doesn’t. Check egress first:
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’s key; an OCPI client needs a roaming network’s key.
The ingestion API validates each message independently and reports {"captured": N, "failed": M}. Messages counted in failed were rejected after a successful delivery.Usual causes, in order:
  • An OCPP message with no tenant pair. The API currently requires tenant_id and tenant_name on OCPP messages even though the SDKs treat them as optional.
  • An OCPI exchange with no status code. It must be between 100 and 599; there is no null form.
  • An identity field over its limit. Tenant name is 32 characters; the rest are 64.
See Identity.
You are capturing one direction. For OCPP, that means the write path isn’t instrumented; for OCPI, that the outbound HTTP client isn’t wrapped.It matters more than it looks: without both directions the validation engine can’t pair requests with responses, so you lose every timeout, orphan, and duplicate-ID finding.
A body or frame over maxCaptureBytes (64 KiB by default) drops the whole message rather than storing a truncated one. Raise the cap — 256 KiB is reasonable for a roaming server — and raise the buffer ceiling with it.
Expected and bounded: the buffer holds undelivered captures up to its ceiling. If that’s more than your pod can afford, lower the ceiling. A healthy client idles far below it.
Still stuck? Mail support@evpanda.io with the SDK and version, the network name and protocol, and a counter snapshot or debug log covering a minute of live traffic.

No SDK for your language?

The ingestion API is a small HTTP contract: two routes, one auth header, one response shape.
POST /v1/ocpp and POST /v1/ocpi each take up to 1,000 messages per batch, optionally gzip- or zstd-compressed, capped at 5 MB compressed and 20 MB decompressed. Mail support@evpanda.io for the full message schemas. If you implement your own client, copy the behaviors that make an SDK safe on a hot path: never block the caller, bound your memory, batch, retry only 5xx and network errors, redact before you buffer, and count what you drop.

Next

OCPP server

The full integration, step by step, in Go, Node, and Python.

OCPI server

The same, plus the drop-in HTTP adapters.