ChargingProfiles is one of the two new modules OCPI 2.2 introduced (the other being HubClientInfo). It defines a way for an eMSP to ask a CPO to shape the power profile of an ongoing or upcoming charging session — limiting maximum power, scheduling charging within a time window, even (in theory) requesting bidirectional flows.
It is also, today, one of the least-implemented modules in production. This article explains what it does, why it matters, and the realistic state of its adoption.
The use case it solves
Smart charging is a real problem with real money behind it. The classic scenarios:
- Demand-response participation. A grid operator pays your eMSP to reduce load during peak hours. Your eMSP wants to pause or throttle EV charging across its user base during that window.
- Time-of-use optimization. An EV plugged in at 6pm doesn’t need to be at 100% until 7am. Charging slowly through the night (or only during cheap-electricity hours) saves money and reduces grid stress.
- Renewable matching. A user wants their car to charge when local solar output is high. The eMSP can adjust the charging rate against local generation forecasts.
- Vehicle-to-grid (V2G) and vehicle-to-home (V2H). The car discharges back to the grid or building during peak hours, then refills at off-peak.
All of these need a way for the entity managing the user’s preferences (the eMSP) to influence what the entity controlling the hardware (the CPO) actually delivers to the session. If the CPO/eMSP roles here are unfamiliar, the OCPI primer covers the actors and the four-corner model. Before OCPI 2.2, there was no standard for this — it happened, but only through bilateral integrations or vertically-integrated single-operator deployments.
ChargingProfiles is the standardization layer.
What ChargingProfiles actually defines
The module has three main operations:
- PUT a charging profile for a specific session or for a location’s general default.
- GET the active charging profile for a session.
- DELETE (clear) a charging profile.
Plus an async notification flow for the CPO to inform the eMSP when a profile takes effect, is updated, or is rejected.
The data model — the ChargingProfile object — is heavily inspired by OCPP’s smart-charging structure (which makes sense, because the CPO is ultimately translating this into an OCPP SetChargingProfile message to the actual charge point; the OCPI vs OCPP vs ISO 15118 breakdown covers where each protocol sits in the stack). The key fields:
- start_date_time — when the profile takes effect.
- duration — how long it applies.
- charging_rate_unit —
W(watts) orA(amps). Pick one and stay consistent. - min_charging_rate — lower bound on power (optional).
- charging_profile_period — an array of
{start_period, limit}pairs describing the schedule (e.g. “from t+0 charge at 11kW, from t+3600s charge at 7kW, from t+10800s charge at 22kW”).
A typical request might look like:
{
"start_date_time": "2026-06-26T22:00:00Z",
"duration": 28800,
"charging_rate_unit": "W",
"charging_profile_period": [
{ "start_period": 0, "limit": 7000 },
{ "start_period": 14400, "limit": 11000 },
{ "start_period": 25200, "limit": 22000 }
]
}
That says: “Starting at 10pm UTC, charge at 7kW for the first 4 hours, then 11kW for the next 3 hours, then 22kW for the final hour.” If we assume a typical 70kWh EV at 40% state of charge, that profile delivers roughly the energy needed to reach 90% by 6am — but does so by drawing low power during the system peak hours and ramping up overnight.
The session-specific flow
A typical interaction:
- User plugs in at a CPO-operated charger.
- CPO sends Session START to the user’s eMSP (via the OCPI Sessions module).
- eMSP recognizes “this is one of our users — what should the profile be?” and looks up the user’s preferences plus current grid conditions.
- eMSP PUTs a ChargingProfile for that session ID to the CPO.
- CPO translates that into an OCPP SetChargingProfile message to the actual charge point.
- Charge point applies the profile. Charging rate now follows the schedule.
- eMSP can update the profile mid-session if conditions change (price spike, user override).
- When the session ends, the profile is naturally cleared.
sequenceDiagram
participant U as Driver
participant CP as Charge Point
participant CPO as CPO
participant EMSP as eMSP
U->>CP: Plug in
CP->>CPO: Session start
CPO->>EMSP: Session START
EMSP->>EMSP: Look up prefs<br/>and grid signals
EMSP->>CPO: PUT ChargingProfile
CPO->>CP: OCPP SetChargingProfile
CP-->>CPO: Applied
CPO-->>EMSP: Profile active
Note over EMSP,CP: eMSP may PUT<br/>updates mid-session
The flow looks clean on paper. In practice it has many failure modes.
A complete worked example
The request snippet above is trimmed to the profile object. Here is the full
round trip for each direction — the eMSP setting a profile, the CPO’s two-part
answer, and the eMSP asking for the active profile — with the wire-level details
that trip people up called out in the comments. (Comments use // and # for
teaching; real JSON has no comments.)
ChargingProfiles is asynchronous, exactly like the Commands module. Every call
is keyed by session_id, and every call carries a response_url. The response
you get on the HTTP connection is only an acknowledgement; the real outcome
arrives later as a separate POST to that response_url.
1. The eMSP sets a profile (PUT to the CPO)
# ── eMSP / SCSP ──▶ CPO ─────────────────────────────────────────────────────
# The Sender here is the eMSP (or a Smart Charging Service Provider acting for
# it). It PUTs the profile to the CPO's Receiver Interface, keyed by session_id.
# The path id is the SAME session_id the CPO gave you in the Sessions module.
PUT /ocpi/2.2.1/chargingprofiles/1a2b3c-session-9f8e HTTP/1.1
Host: cpo.example.com
# Auth scheme is the literal word "Token" — NOT "Bearer" — then the credentials
# token the CPO issued to THIS eMSP during the Credentials handshake.
Authorization: Token <token-the-CPO-issued-to-this-eMSP>
Content-Type: application/json
# Recommended so both sides can trace one call across their logs; echo them back.
X-Request-ID: 12345
X-Correlation-ID: 67890
{
// Where the CPO POSTs the ASYNC result once the charger has (or hasn't)
// applied the profile. This must be an endpoint the eMSP exposes and can
// match back to this session. The sync response below is only an ACK.
"response_url": "https://emsp.example.com/ocpi/2.2.1/chargingprofiles/results/1a2b3c-session-9f8e",
"charging_profile": {
"start_date_time": "2026-07-04T22:00:00Z", // RFC 3339, UTC ("Z"). Optional.
"duration": 28800, // seconds the profile applies (8h). Optional.
// THE unit gotcha: every "limit" below is interpreted in THIS unit. "W" =
// watts, "A" = amps. A limit of 16 means 16 W with unit "W" but 16 A
// (~11 kW at 400 V 3-phase) with unit "A". Send the wrong unit and you
// either throttle the charger to a trickle or flood it — pick one and be
// sure it matches what your limits actually mean.
"charging_rate_unit": "W", // "W" | "A"
"min_charging_rate": 1400, // optional lower bound, same unit as above
"charging_profile_period": [ // schedule as {start_period, limit} pairs
{ "start_period": 0, "limit": 7000 }, // start_period = SECONDS from
{ "start_period": 14400, "limit": 11000 }, // start_date_time, NOT a clock
{ "start_period": 25200, "limit": 22000 } // time. 0 = at the start.
]
}
}
# ── CPO ──▶ eMSP (the SYNC response — only an ACK) ──────────────────────────
HTTP/1.1 200 OK
Content-Type: application/json
X-Request-ID: 12345
X-Correlation-ID: 67890
{
// data is a ChargingProfileResponse. result is ONLY whether the CPO accepted
// the request for processing — NOT whether the charger applied the profile.
"data": {
"result": "ACCEPTED", // ACCEPTED | NOT_SUPPORTED | REJECTED | TOO_OFTEN | UNKNOWN_SESSION
"timeout": 30 // seconds to wait for the async result below
},
// THE key OCPI gotcha: HTTP 200 only means the HTTP call arrived. You must
// ALSO check status_code INSIDE the envelope. 1000 = success. You can get
// HTTP 200 with status_code 2001 (invalid parameters) — always read the
// envelope, never trust the HTTP status alone. 2xxx = client error, 3xxx = server.
"status_code": 1000,
"status_message": "Success",
"timestamp": "2026-07-04T21:59:31Z"
}
# ── CPO ──▶ eMSP (the ASYNC result, seconds later) ─────────────────────────
# This is the REAL outcome. The CPO POSTs it to the response_url from the PUT,
# after it has translated the profile into an OCPP SetChargingProfile and heard
# back from the charge point. Miss this and you never learn if the profile
# actually took effect — the async trap that catches Commands users too.
POST /ocpi/2.2.1/chargingprofiles/results/1a2b3c-session-9f8e HTTP/1.1
Host: emsp.example.com
# Opposite direction, opposite token: the one the eMSP issued to the CPO.
Authorization: Token <token-the-eMSP-issued-to-this-CPO>
Content-Type: application/json
{
// data is a ChargingProfileResult.
"data": {
"result": "ACCEPTED" // ACCEPTED | REJECTED | UNKNOWN
},
"status_code": 1000,
"status_message": "Success",
"timestamp": "2026-07-04T21:59:36Z"
}
# ── eMSP ──▶ CPO (the eMSP's ACK of the async POST) ────────────────────────
HTTP/1.1 200 OK
Content-Type: application/json
{
"data": {}, // nothing to return; the eMSP just ACKs
"status_code": 1000, // again: check this, not just the HTTP 200
"status_message": "Success",
"timestamp": "2026-07-04T21:59:36Z"
}
2. The eMSP reads the active profile (GET from the CPO)
# ── eMSP ──▶ CPO ────────────────────────────────────────────────────────────
# Same async pattern: the GET carries its own response_url. The sync answer is
# just an ACCEPTED ACK; the actual active profile comes back to response_url.
GET /ocpi/2.2.1/chargingprofiles/1a2b3c-session-9f8e?duration=900&response_url=https%3A%2F%2Femsp.example.com%2Focpi%2F2.2.1%2Fchargingprofiles%2Fresults%2F1a2b3c-session-9f8e HTTP/1.1
Host: cpo.example.com
Authorization: Token <token-the-CPO-issued-to-this-eMSP>
# Query params:
# duration → how many seconds of the active profile you want reported
# response_url → URL-encoded; where the async ActiveChargingProfileResult lands
# ── CPO ──▶ eMSP (the SYNC response — only an ACK) ──────────────────────────
HTTP/1.1 200 OK
Content-Type: application/json
{
"data": {
"result": "ACCEPTED", // ACCEPTED | NOT_SUPPORTED | REJECTED | TOO_OFTEN | UNKNOWN_SESSION
"timeout": 30
},
"status_code": 1000,
"status_message": "Success",
"timestamp": "2026-07-04T22:15:00Z"
}
# ── CPO ──▶ eMSP (the ASYNC result, seconds later) ─────────────────────────
# POSTed to the response_url. data is an ActiveChargingProfileResult carrying
# the profile the charger is actually running right now.
POST /ocpi/2.2.1/chargingprofiles/results/1a2b3c-session-9f8e HTTP/1.1
Host: emsp.example.com
Authorization: Token <token-the-eMSP-issued-to-this-CPO>
Content-Type: application/json
{
"data": {
// data is an ActiveChargingProfileResult. result* is required; profile is
// present only when result is ACCEPTED.
"result": "ACCEPTED", // required: ACCEPTED | REJECTED | UNKNOWN
"profile": { // optional: an ActiveChargingProfile
// required: when the charger CALCULATED this active profile (RFC 3339, UTC "Z")
"start_date_time": "2026-07-04T22:00:00Z",
"charging_profile": { // required: the ChargingProfile now running
// optional: absolute start of THIS profile. Absent → relative to charging start.
"start_date_time": "2026-07-04T22:00:00Z",
"duration": 28800, // optional: seconds the profile applies (8h)
"charging_rate_unit": "W", // required: "W" | "A" — same unit rules on the way back
"min_charging_rate": 1400, // optional: lower bound, same unit as above
"charging_profile_period": [ // optional: {start_period*, limit*} pairs, both required
{ "start_period": 0, "limit": 7000 }, // start_period = SECONDS from
{ "start_period": 14400, "limit": 11000 }, // start_date_time, NOT a clock
{ "start_period": 25200, "limit": 22000 } // time. 0 = at the start.
]
}
}
},
"status_code": 1000,
"status_message": "Success",
"timestamp": "2026-07-04T22:15:04Z"
}
To clear a profile, the eMSP sends DELETE /ocpi/2.2.1/chargingprofiles/{session_id}?response_url=….
It follows the identical two-step shape: a sync ACCEPTED ACK, then the real
outcome POSTed to the response_url.
Where reality complicates things
A list of things that make ChargingProfiles harder to deploy than the spec suggests.
Charge-point smart-charging support is uneven. Many older charge points run OCPP 1.6 and don’t implement the smart-charging profile feature, or implement it incorrectly. Sending a ChargingProfile to a CPO whose hardware can’t honor it just produces a rejection.
OCPP-version mismatches. OCPP 2.0.1 has clean smart-charging primitives. OCPP 1.6 added smart-charging as an extension. Translation between the OCPI ChargingProfile and the OCPP version actually deployed at the charger is the CPO’s problem, but the abstraction leaks (some profiles are expressible in 2.0.1 but not 1.6, etc.).
Local energy management. Many sites have local energy management systems (DLM — dynamic load management) that already adjust charger output based on building load. A ChargingProfile request from an eMSP might be overridden by local logic. Who wins? Implementation-defined.
Timing precision. “Start at 22:00:00 UTC” is precise on paper. In practice the CPO’s network has latency, the charger’s clock may drift, and the actual ramp may happen plus or minus minutes. For pure billing purposes this is fine; for fast-acting grid services it’s a real problem.
User overrides. If a user wants to override the profile (e.g. “I need to leave in an hour, charge as fast as you can”), most charger UIs and apps don’t expose a way to do this cleanly. The eMSP would need to push an updated profile, which requires the eMSP’s app to be involved — and if the user is on the CPO’s drive-up payment, the eMSP isn’t in the loop at all.
Reservation interactions. If a user reserved the charger, was their profile set at reservation time or session start time? Both options have semantics. The spec gives flexibility; implementations vary.
Common implementation gotchas
A few things that catch real-world implementations.
The async trap and the W-vs-A ambiguity — the #1 mistake
The single most common ChargingProfiles bug is treating the synchronous
response as the answer. It is not. Like the Commands module, ChargingProfiles is
asynchronous: the ChargingProfileResponse you get back on the PUT (or GET,
or DELETE) is only an acknowledgement that the CPO accepted the request for
processing. The real outcome — whether the charger actually applied the profile —
arrives later as a separate POST to the response_url you supplied:
# The sync ACK — do NOT treat this as "the profile is applied":
{ "data": { "result": "ACCEPTED", "timeout": 30 }, "status_code": 1000, "status_message": "Success", "timestamp": "..." }
# The async result, POSTed to your response_url seconds later — THIS is the outcome:
{ "data": { "result": "ACCEPTED" }, "status_code": 1000, "status_message": "Success", "timestamp": "..." }
Note the two different result value sets, and don’t mix them up: the sync
ChargingProfileResponse.result is ACCEPTED | NOT_SUPPORTED | REJECTED | TOO_OFTEN | UNKNOWN_SESSION, while the async ChargingProfileResult.result is
ACCEPTED | REJECTED | UNKNOWN. A sync ACCEPTED followed by an async REJECTED is
normal — the request was well-formed but the charger couldn’t honor it. If you
never stand up the response_url receiver, you will believe every profile
succeeded when many silently didn’t.
The second half of the same mistake is the charging_rate_unit ambiguity.
Every limit in the profile is interpreted in whatever charging_rate_unit you
set — W (watts) or A (amps) — and nothing in the number itself tells you
which. A limit of 16 means 16 watts under "W" (a trickle that effectively
stops charging) but 16 amps under "A" (roughly 11 kW on a 400 V three-phase
supply). Set the wrong unit and you either throttle the session to nothing or
push the charger past what you intended. Pick the unit deliberately, use it
consistently across every period and min_charging_rate, and confirm it matches
what your limit numbers actually mean.
For the authoritative field-by-field definitions, see the ChargingProfiles module in the OCPI 2.2.1 specification.
session_id is the key — and it must be a live session
Every ChargingProfiles call is keyed by session_id, and it must be the same id
the CPO issued in the Sessions module. Target a session the CPO doesn’t
recognize (already ended, never existed, wrong id) and the sync response comes
back UNKNOWN_SESSION. There is no location-wide “set a default for everything”
call in this flow — the profile attaches to one running session.
response_url must be reachable and matchable
The response_url is not optional plumbing. It must be an endpoint your side
actually exposes, reachable from the CPO, and constructed so you can match the
incoming async POST back to the request that triggered it (embedding the
session_id in the path, as the examples do, is the common approach). A
response_url that 404s or that you can’t correlate leaves you with the ACK and
nothing else.
start_period is seconds, not a clock time
Inside charging_profile_period, start_period is the number of seconds from
start_date_time, not a wall-clock time. start_period: 14400 means “four
hours after the profile starts,” not “at 14:40.” Treating it as a timestamp
scrambles the whole schedule.
last_updated and timestamps are RFC 3339 UTC
As everywhere in OCPI, start_date_time, timestamp, and any last_updated
are RFC 3339 in UTC with a trailing Z. Send a local-time or offset value and
the schedule drifts by your timezone.
A sync ACCEPTED can still be overridden on-site
Even a fully successful round trip doesn’t guarantee the charger holds your profile. Local dynamic load management (DLM) may override an eMSP profile, and the spec does not mandate who wins — the resolution is implementation-defined. Treat a delivered profile as a request the site may adjust, not a guarantee.
A realistic view of adoption
A few honest observations about where ChargingProfiles is in 2026.
- Most production CPOs do not support it. They support Locations, Sessions, CDRs, Tokens, Commands — the core modules. ChargingProfiles is on roadmaps.
- The eMSPs pushing for it are the ones running demand-response pilots with utilities, often in jurisdictions with active grid-flexibility programs (California, Texas/ERCOT, Ontario, the UK, Germany, and the Netherlands).
- Within single-operator vertically-integrated stacks (eMSP and CPO owned by the same parent), smart charging works fine — but it’s done via private APIs, not OCPI. The interest in OCPI ChargingProfiles is from operators who want this across organizational boundaries.
- V2G via OCPI ChargingProfiles is theoretical today. The module can carry negative limits, but the hardware, the vehicle support (ISO 15118-20), and the regulatory frameworks are all still maturing.
This is not a criticism of the module — it’s a healthy state for a new optional feature. The standard is in place; adoption follows when commercial pressure justifies the engineering investment.
When to implement ChargingProfiles
If you’re a CPO:
- Skip it for now unless you have a partner specifically asking for it or you’re operating in a jurisdiction where grid services are paying real money.
- When you do implement it, do it after your underlying OCPP smart-charging story is solid. There’s no point in receiving OCPI ChargingProfiles you can’t translate to the charger.
- Validate against the hardware mix. If 60% of your fleet is OCPP 1.6 without smart-charging extensions, your real coverage is 40%.
If you’re an eMSP:
- Implement it when you have a business case. A demand-response program with a utility, a TOU-optimization product for users, or a V2G pilot.
- Be prepared for low coverage. Even with full OCPI ChargingProfiles implementation, the number of CPO-served sessions where it actually works is small. Manage expectations.
- Build the user-facing logic first. Knowing how to ask for a profile is easy. Knowing what profile to ask for, given a user’s car/destination/cost preferences/grid signals, is where the real product value is.
The relationship to V2G
A quick note on V2G specifically because it gets asked about often.
ChargingProfiles in OCPI 2.2 can carry negative limits, so the roaming layer can, in principle, describe discharge. On the CPO-to-charger side, native bidirectional charging is an OCPP 2.1 capability (2.1 added a dedicated bidirectional/V2G functional block; OCPP 2.0.1 has no built-in bidirectional blocks). ISO 15118-20 on the vehicle side can negotiate bidirectional flow. So the pieces of a standards stack are coming together.
What’s missing is:
- Vehicles in the field that support 15118-20 bidirectional (most don’t yet).
- Charger hardware that supports bidirectional flow (most don’t).
- Tariffs and grid-service compensation frameworks that make V2G economically attractive to users (still maturing).
By the time the rest of the stack catches up, OCPI ChargingProfiles will be the standard way the eMSP coordinates the V2G action. For now, the production deployments of V2G are mostly bilateral integrations rather than ChargingProfiles-mediated roaming.
The honest summary
ChargingProfiles is a well-designed module solving a real problem that hasn’t yet become a widespread commercial reality. The spec is sound, the data model is sensible, and the abstractions match the underlying OCPP capabilities. The reasons most teams haven’t adopted it are not technical — they’re commercial. As demand-response programs, V2G pilots, and time-of-use optimization products mature, ChargingProfiles will become more relevant. Until then, it’s worth understanding but not worth rushing to implement unless you have a specific use case in hand.