From Zero to OCPP: Launching a White-Label EV Charging Platform

Introduction

The EV charging market is growing fast — and the software layer underneath it is where the real competitive advantage lives. Whether you’re a mobility startup, an energy company, or an enterprise spinning up an internal fleet charging solution, building your own white-label OCPP platform gives you control, scalability, and brand ownership that off-the-shelf SaaS simply can’t offer.

In this guide, we’ll walk through everything you need to go from zero to a fully operational, white-label EV charging management system — covering protocol fundamentals, system architecture, backend implementation, branding layers, and go-to-market considerations.


1. What is OCPP and Why Does It Matter?

OCPP (Open Charge Point Protocol) is the de facto open standard for communication between EV charge points (hardware) and a Central System (your backend). Maintained by the Open Charge Alliance (OCA), it defines how chargers connect, authenticate, report status, and handle transactions.

Key versions to know

  • OCPP 1.6 — The most widely deployed version. Uses SOAP or JSON over WebSocket. Still the industry baseline and supported by the vast majority of hardware vendors.
  • OCPP 2.0.1 — The modern standard. JSON-only over WebSocket, with improved security profiles, smart charging, and device management. Required for newer compliance certifications.

Why build your own platform?

Approach Control Cost at Scale Brand Ownership
Off-the-shelf SaaS Low High None
White-label reseller Medium Medium Partial
Custom OCPP platform Full Low Full

If you expect more than a few hundred charge points or need deep integration with your product ecosystem, a custom platform is almost always the right long-term bet.


2. System Architecture Overview

A production-grade white-label OCPP platform consists of five core layers:

graph TD
    A["🖥️ White-Label Frontend\nOperator Dashboard / Driver App"]
    B["🔀 API Gateway Layer\nREST / GraphQL APIs"]
    C["⚡ OCPP Central System\nWebSocket Server"]
    D["⚙️ Business Logic & Event Bus\nSessions · Billing · Notifications"]
    E["🗄️ Data & Infrastructure\nPostgreSQL · Redis · Kafka · S3"]

    A -->|"Operator / Driver requests"| B
    B -->|"OCPP commands"| C
    C -->|"Charger events"| D
    D -->|"Read / Write"| E

    style A fill:#1A6FBF,color:#fff,stroke:#0d4d8a
    style B fill:#2E86C1,color:#fff,stroke:#1a5276
    style C fill:#2874A6,color:#fff,stroke:#1a4f72
    style D fill:#21618C,color:#fff,stroke:#154360
    style E fill:#1B4F72,color:#fff,stroke:#0e2f44

Component breakdown

Component Responsibility Recommended Tech
OCPP WebSocket Server Handles charger connections Node.js / Python (ocpp lib)
REST API Operator & driver-facing endpoints FastAPI / Express
Session Manager Tracks active charging sessions Redis + PostgreSQL
Event Bus Async processing of charger events Kafka / RabbitMQ
Billing Engine Usage calculation, invoicing Custom + Stripe
Notification Service Alerts via email/SMS/push SendGrid / Firebase
Frontend Dashboard Operator white-label UI React / Next.js
Driver Mobile App End-user app (optional) React Native / Flutter

3. Setting Up the OCPP Central System

The Central System is the heart of your platform — it maintains persistent WebSocket connections with every charge point.

3.1 Choosing a WebSocket Library

For Node.js, the ocpp-j-1.6 or community ocpp packages are popular starting points. For Python, the ocpp library by mobilityhouse is production-tested and supports both 1.6 and 2.0.1.

# Python example using mobilityhouse/ocpp
pip install ocpp websockets

3.2 Basic Central System in Python

import asyncio
import websockets
from ocpp.routing import on
from ocpp.v16 import ChargePoint as cp
from ocpp.v16 import call_result
from ocpp.v16.enums import Action, RegistrationStatus

class ChargePoint(cp):

    @on(Action.BootNotification)
    async def on_boot_notification(self, charge_point_vendor, charge_point_model, **kwargs):
        return call_result.BootNotificationPayload(
            current_time=datetime.utcnow().isoformat(),
            interval=10,
            status=RegistrationStatus.accepted
        )

    @on(Action.Heartbeat)
    async def on_heartbeat(self):
        return call_result.HeartbeatPayload(
            current_time=datetime.utcnow().isoformat()
        )

async def on_connect(websocket, path):
    charge_point_id = path.strip("/")
    cp = ChargePoint(charge_point_id, websocket)
    await cp.start()

async def main():
    server = await websockets.serve(
        on_connect,
        "0.0.0.0",
        9000,
        subprotocols=["ocpp1.6"]
    )
    await server.wait_closed()

asyncio.run(main())

3.3 Key OCPP Messages to Implement

Core Profile (must-have)

  • BootNotification — Charger registers with your system
  • Heartbeat — Periodic keepalive
  • Authorize — Token/RFID validation
  • StartTransaction / StopTransaction — Session lifecycle
  • MeterValues — Energy consumption reporting
  • StatusNotification — Charger status changes

Remote Control (essential for operators)

  • RemoteStartTransaction — Start a session from the dashboard
  • RemoteStopTransaction — Stop a session remotely
  • ChangeAvailability — Enable/disable a connector
  • Reset — Soft or hard reboot a charger

Smart Charging (OCPP 2.0.1 / advanced)

  • SetChargingProfile — Apply power limits or schedules
  • GetCompositeSchedule — Query current active schedule

4. Multi-Tenancy: The White-Label Core

White-labeling requires a robust multi-tenancy model. Each operator (tenant) should have:

  • Their own branded domain (e.g., app.youroperator.com)
  • Isolated data (charge points, sessions, users)
  • Custom branding (logo, colors, app name)
  • Configurable pricing models

4.1 Tenant Data Model

-- Tenants (operators)
CREATE TABLE tenants (
    id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    slug        TEXT UNIQUE NOT NULL,       -- e.g. "greenenergy"
    name        TEXT NOT NULL,
    domain      TEXT,                       -- custom domain
    logo_url    TEXT,
    theme       JSONB,                      -- { primary_color, font, etc. }
    created_at  TIMESTAMPTZ DEFAULT NOW()
);

-- Charge points belong to a tenant
CREATE TABLE charge_points (
    id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id   UUID REFERENCES tenants(id),
    ocpp_id     TEXT UNIQUE NOT NULL,       -- the ID used in WebSocket path
    name        TEXT,
    location    JSONB,
    status      TEXT DEFAULT 'Unknown',
    created_at  TIMESTAMPTZ DEFAULT NOW()
);

4.2 Routing Tenant Connections

When a charger connects via WebSocket, extract its OCPP ID from the path and look up which tenant it belongs to:

async def on_connect(websocket, path):
    ocpp_id = path.strip("/")

    # Resolve tenant from charge point registry
    tenant = await db.get_tenant_by_ocpp_id(ocpp_id)
    if not tenant:
        await websocket.close(code=1008, reason="Unknown charge point")
        return

    cp = TenantAwareChargePoint(ocpp_id, websocket, tenant)
    await cp.start()

5. Session Management and Billing

5.1 Session Lifecycle

sequenceDiagram
    participant CP as Charge Point
    participant CS as Central System
    participant DB as Database
    participant BE as Billing Engine

    CP->>CS: Authorize(idTag)
    CS->>DB: Validate token
    DB-->>CS: Accepted
    CS-->>CP: AuthorizeResponse(Accepted)

    CP->>CS: StartTransaction(connectorId, idTag, meterStart)
    CS->>DB: Open session record
    CS-->>CP: StartTransactionResponse(transactionId)

    loop Every 60s
        CP->>CS: MeterValues(transactionId, energy)
        CS->>DB: Store meter reading
    end

    CP->>CS: StopTransaction(transactionId, meterStop, reason)
    CS->>DB: Close session
    CS->>BE: Calculate cost
    BE-->>CS: Invoice created
    CS-->>CP: StopTransactionResponse

5.2 Storing Meter Values

@on(Action.MeterValues)
async def on_meter_values(self, connector_id, meter_value, **kwargs):
    for reading in meter_value:
        for sampled in reading.get("sampledValue", []):
            await db.insert_meter_reading({
                "session_id": self.active_session_id,
                "timestamp": reading["timestamp"],
                "measurand": sampled.get("measurand", "Energy.Active.Import.Register"),
                "value": float(sampled["value"]),
                "unit": sampled.get("unit", "Wh")
            })

5.3 Pricing Models to Support

  • Per kWh — Most common, based on energy delivered
  • Per minute — Time-based billing (useful for parking enforcement)
  • Session flat fee — Fixed price per charge
  • Hybrid — e.g., $0.30/kWh + $0.05/min after 60 minutes

Store pricing as a JSONB config per tenant or per charge point for maximum flexibility.


6. Security and Authentication

6.1 Charge Point Authentication

OCPP 1.6 supports Basic Auth via the WebSocket URL:

ws://username:password@yourplatform.com/ocpp/CP001

For OCPP 2.0.1, use Security Profile 3 with TLS client certificates for production deployments.

6.2 Auth & RBAC Flow

flowchart LR
    CP["⚡ Charge Point"] -->|"WSS + Basic Auth\nOCPP 1.6"| WS["WebSocket Server"]
    CP2["⚡ Charge Point"] -->|"WSS + TLS Cert\nOCPP 2.0.1"| WS
    OP["👤 Operator App"] -->|"HTTPS + JWT"| API["REST API"]
    API -->|"RBAC check\nAdmin / Operator / Viewer"| BL["Business Logic"]
    WS -->|"Forward events"| BL

    style CP fill:#27AE60,color:#fff,stroke:#1e8449
    style CP2 fill:#27AE60,color:#fff,stroke:#1e8449
    style OP fill:#8E44AD,color:#fff,stroke:#6c3483
    style WS fill:#1A6FBF,color:#fff,stroke:#0d4d8a
    style API fill:#1A6FBF,color:#fff,stroke:#0d4d8a
    style BL fill:#1B4F72,color:#fff,stroke:#0e2f44
  • Use JWT tokens with tenant-scoped claims for operator APIs
  • Implement RBAC (Admin, Operator, Viewer roles) per tenant
  • Rate-limit all public-facing endpoints
  • Audit-log every remote command sent to a charger

7. Building the White-Label Frontend

The operator dashboard is what your customers see every day. It needs to be fast, rebrandable, and functional.

7.1 Core Dashboard Features

  • Live map — Real-time charger status on a map view
  • Session monitor — Active sessions with energy and duration
  • Charge point management — Add, configure, enable/disable chargers
  • User management — RFID cards, driver accounts, access control
  • Analytics — Revenue, utilization, energy dispensed over time
  • Pricing configuration — Per-site or per-connector pricing rules
  • Remote actions — Start/stop sessions, reboot chargers

7.2 Theming System

Use CSS variables + a tenant config endpoint to make theming seamless:

// Fetch tenant theme on app load
const { data: tenant } = await api.get('/api/v1/tenant/config');

document.documentElement.style.setProperty('--color-primary', tenant.theme.primary_color);
document.documentElement.style.setProperty('--color-secondary', tenant.theme.secondary_color);
document.title = tenant.name;

7.3 White-Label Deployment Options

Option Complexity Isolation
Subdomain per tenant (tenant.yourplatform.com) Low Shared infra
Custom domain with SSL (app.clientbrand.com) Medium Shared infra
Dedicated deployment per tenant High Full isolation

For most SaaS use cases, start with subdomain routing and add custom domain support via Nginx + Let’s Encrypt automation as you grow.


8. Infrastructure and Scaling

8.1 Starting Architecture (0–500 Chargers)

graph LR
    CH["⚡ Chargers"] -->|"WebSocket"| WS["WebSocket Server\n1 node"]
    OP["👤 Operators"] -->|"HTTPS"| API["REST API\n1-2 nodes"]

    WS -->|"Read / Write"| PG[("PostgreSQL")]
    WS -->|"Session cache"| RD[("Redis\nSession Cache")]
    API -->|"Read / Write"| PG
    API -->|"Store logs"| S3[("S3\nLogs / Reports")]

    style CH fill:#27AE60,color:#fff,stroke:#1e8449
    style OP fill:#8E44AD,color:#fff,stroke:#6c3483
    style WS fill:#1A6FBF,color:#fff,stroke:#0d4d8a
    style API fill:#1A6FBF,color:#fff,stroke:#0d4d8a
    style PG fill:#1B4F72,color:#fff,stroke:#0e2f44
    style RD fill:#C0392B,color:#fff,stroke:#922b21
    style S3 fill:#D68910,color:#fff,stroke:#9a6709

8.2 Scaling to 10,000+ Chargers

At scale, the WebSocket layer becomes the bottleneck. Each charger maintains a persistent connection, so you need:

  • Horizontal scaling of the WebSocket server with a shared session store (Redis)
  • A message broker (Kafka or RabbitMQ) to fan out charger events to downstream services
  • Connection routing so remote commands reach the right node holding the charger’s socket
graph TD
    CH["⚡ Chargers"] -->|"WebSocket"| LB["Load Balancer"]
    LB -->|"Sticky session"| WS1["WS Node 1"]
    LB -->|"Sticky session"| WS2["WS Node 2"]
    LB -->|"Sticky session"| WS3["WS Node N"]

    WS1 & WS2 & WS3 -->|"Publish events"| KF[["Kafka\ncharger.events"]]
    WS1 & WS2 & WS3 -->|"Register socket"| RD[("Redis\nConnection Registry")]

    KF -->|"session.started/stopped"| SS["Session Service"]
    KF -->|"meter.values"| BS["Billing Service"]
    KF -->|"status.changed"| NS["Notification Service"]

    SS & BS & NS -->|"Persist"| PG[("PostgreSQL")]

    style CH fill:#27AE60,color:#fff,stroke:#1e8449
    style LB fill:#566573,color:#fff,stroke:#2c3e50
    style WS1 fill:#1A6FBF,color:#fff,stroke:#0d4d8a
    style WS2 fill:#1A6FBF,color:#fff,stroke:#0d4d8a
    style WS3 fill:#1A6FBF,color:#fff,stroke:#0d4d8a
    style KF fill:#E67E22,color:#fff,stroke:#ca6f1e
    style RD fill:#C0392B,color:#fff,stroke:#922b21
    style SS fill:#21618C,color:#fff,stroke:#154360
    style BS fill:#21618C,color:#fff,stroke:#154360
    style NS fill:#21618C,color:#fff,stroke:#154360
    style PG fill:#1B4F72,color:#fff,stroke:#0e2f44

8.3 Observability Checklist

  • Prometheus metrics on WebSocket connections, message latency, and error rates
  • Structured JSON logging per charger/tenant
  • Alerting on charger disconnection spikes
  • Grafana dashboard for real-time charger health

9. OCPI Integration for Roaming

Once your platform is live, operators will want roaming — allowing drivers from other networks to charge at their stations. This is handled by OCPI (Open Charge Point Interface).

flowchart LR
    DR["🚗 Driver\nPartner Network"] -->|"Auth Token"| EM["eMSP\nMobility Provider"]
    EM -->|"OCPI 2.2.1"| HUB["OCPI Hub /\nAggregator"]
    HUB -->|"CDRs / Locations / Tariffs"| YOUR["Your Platform\nCPO Role"]
    YOUR -->|"OCPP"| CP["⚡ Charge Points"]

    style DR fill:#27AE60,color:#fff,stroke:#1e8449
    style EM fill:#8E44AD,color:#fff,stroke:#6c3483
    style HUB fill:#E67E22,color:#fff,stroke:#ca6f1e
    style YOUR fill:#1A6FBF,color:#fff,stroke:#0d4d8a
    style CP fill:#1B4F72,color:#fff,stroke:#0e2f44

OCPI lets your platform:

  • List locations and tariffs on public aggregators (PlugShare, ABRP, etc.)
  • Accept payment from drivers on partner networks (e.g., Plug & Charge)
  • Share real-time availability data with navigation apps

Implementing OCPI 2.2.1 as a CPO (Charge Point Operator) role is the typical starting point.


10. Go-to-Market Considerations

Positioning Your White-Label Platform

  • Hardware-agnostic — Support any OCPP-compliant charger (this is your biggest selling point)
  • Self-serve onboarding — Let operators add charge points without calling support
  • Multi-currency billing — If targeting regional markets (Southeast Asia, EU, etc.)
  • Compliance-ready — OCPP conformance certification from OCA adds credibility

Pricing Models for Your Platform

  • Per charge point per month (most predictable)
  • Revenue share on session fees (aligns incentives)
  • One-time setup fee + annual license (enterprise)

Go-to-Market Roadmap

gantt
    title Platform Launch Roadmap
    dateFormat  YYYY-MM-DD
    section Phase 1 - Core
    OCPP 1.6 Central System     :p1, 2024-01-01, 30d
    Basic session management    :p2, after p1, 20d
    Operator dashboard v1       :p3, after p1, 30d
    section Phase 2 - Business
    Multi-tenancy and billing   :p4, after p2, 30d
    White-label theming         :p5, after p3, 20d
    RFID and driver auth        :p6, after p4, 15d
    section Phase 3 - Scale
    Smart charging OCPP 2.0.1   :p7, after p6, 30d
    OCPI roaming integration    :p8, after p6, 30d
    Kafka horizontal scaling    :p9, after p7, 20d
    section Phase 4 - Growth
    Mobile driver app           :p10, after p8, 45d
    Analytics and reporting     :p11, after p9, 20d

Common Early Customers

  • Real estate developers (condos, offices)
  • Fleet operators (logistics, rental cars)
  • Municipalities and parking operators
  • Hospitality (hotels, resorts)

Conclusion

Building a white-label OCPP platform is a significant engineering undertaking — but it’s also a massive competitive moat once it’s live. The key milestones are:

  1. Get a working OCPP 1.6 Central System running with BootNotification, Authorize, and transaction handling
  2. Add multi-tenancy at the data and routing layer early — retrofitting it later is painful
  3. Build the operator dashboard with live status, remote actions, and basic analytics
  4. Layer in billing, smart charging, and OCPI as the platform matures
  5. Invest in observability — operators will call you the moment a charger goes offline

The EV charging software space is still early. Teams that ship fast, stay hardware-agnostic, and own their customer relationships through white-labeling will be in a strong position as the market accelerates.


Built with OCPP 1.6 / 2.0.1 · Open Charge Alliance · Python · Node.js · PostgreSQL · Redis


Get in Touch with us

Chat with Us on LINE

iiitum1984

Speak to Us or Whatsapp

(+66) 83001 0222

Related Posts

Our Products