When a driver taps their app or RFID card at a charging station, something at the protocol level has to say “yes, this person is authorized.” In OCPI that something is a Token: a digital identity that lets a CPO and an eMSP share an authorization decision without sharing the customer behind it.
This is the privacy-preserving authentication layer of OCPI. If the two roles are new to you, what a CPO and an eMSP each do is the prerequisite reading.
What a Token represents
A Token represents one customer’s right to charge at participating CPO stations. Think of it as the digital version of an RFID card or an app login:
- Each Token has a unique identifier (the
uid) - It is owned by an eMSP (the issuer)
- It is known to CPOs (the validators) via OCPI publication
- It maps to a real customer in the eMSP’s system, but the CPO never needs to know who that customer is
That last bullet is the whole privacy model. The eMSP knows “this token belongs to John Smith, with this payment method on file.” The CPO knows “token ABC123 is authorized to charge at our stations.” The CPO does not see John Smith. The eMSP does not see the station’s internal operational state. OCPI is the contract layer between the two views.
The Token object
A typical Token looks like:
{
"country_code": "NL",
"party_id": "EMO", // the eMSP's party ID
"uid": "TOKEN_42", // unique identifier within the eMSP
"type": "RFID", // RFID, APP_USER, AD_HOC_USER, OTHER
"contract_id": "NL-EMO-C0123-X", // the customer's contract identifier
"visual_number": "1234 5678 9012", // what's printed on the card (optional)
"issuer": "Example Mobility",
"group_id": null, // for grouping (e.g., corporate accounts)
"valid": true, // is this token currently valid?
"whitelist": "ALWAYS", // ALWAYS, NEVER, ALLOWED, ALLOWED_OFFLINE
"language": "en", // preferred language for display
"default_profile_type": "REGULAR",
"energy_contract": { // optional, for energy mix tracking
"supplier_name": "Example Energy", // required inside energy_contract
"contract_id": "EE-8842-2026" // optional inside energy_contract
},
"last_updated": "2026-07-03T09:00:00Z"
}
Key fields:
uid: what the station transmits when a driver taps. For an RFID card, that is the chip ID.type: which kind of token this is, which changes how it can be presented and how much the CPO can assumevalid: set it to false to kill a token (lost card, expired contract, fraud)whitelist: how aggressively a CPO may cache and trust this token, covered in detail belowcontract_id: links the token to a customer contract, which is what makes billing reconciliation possible later
Token types and what they authenticate
The type field tells the CPO how the token will arrive:
- RFID: a physical card. The chip ID is what the station reads.
- APP_USER: app-based authentication, such as “Start Session” tapped in the eMSP’s app
- AD_HOC_USER: a one-time user with no standing account, such as a card payment at the terminal
- OTHER: anything else, including a vehicle’s onboard contract certificate under ISO 15118 Plug & Charge
The type is not cosmetic. It changes what the CPO can safely assume about the tap. An RFID chip ID arrives over a contactless read with no cryptographic proof that the card is genuine, while a Plug & Charge contract certificate arrives with a signature chain behind it. RFID vs Plug & Charge vs app authentication (coming soon) walks through where each one is strong and where it is not.
The whitelist field and the caching strategy
OCPI authentication carries a latency tradeoff, and the spec resolves it per token rather than per party. The two ends of the range:
Live validation. Every time a driver plugs in, the CPO sends a real-time authorization request to the eMSP. The eMSP checks the customer’s current state (contract valid, payment method good) and answers yes or no. Most accurate, slowest.
Pre-shared whitelist. The eMSP publishes valid tokens to the CPO in advance. When a driver taps, the CPO checks its local copy instead of calling out. Fast, but the copy can be stale.
The whitelist field on each Token tells the CPO which strategy applies:
- ALWAYS: validate from the whitelist and never call back. Fastest, and it puts the burden on the eMSP to push updates promptly.
- NEVER: call the eMSP live for every session. Slowest, always current.
- ALLOWED: try the whitelist first, call the eMSP on a miss. The hybrid.
- ALLOWED_OFFLINE: call live, and fall back to the cached copy only when the eMSP is unreachable.
Pick the value by asking how much the answer can change between two taps. A prepaid RFID card sitting on a topped-up balance barely moves, so ALWAYS buys real latency for very little exposure. A corporate fleet card drawing on a shared credit limit can flip from good to bad between one driver’s session and the next, which is exactly what NEVER exists for. ALLOWED_OFFLINE is the choice when you would rather ask live but cannot let a dead link take a site out of service.
If this shape looks familiar from the charger side, it should. It is the same bargain as OCPP’s Local Authorization List: cache the answer near the point of use, and accept staleness in exchange for still working when the network does not.
flowchart TD
Tap[Driver taps card at station] --> CPO[CPO looks up token]
CPO --> WL{whitelist value?}
WL -->|ALWAYS| Cache[Check local cache only]
WL -->|NEVER| Live[Call eMSP live: authorize]
WL -->|ALLOWED| Try[Try cache first]
WL -->|ALLOWED_OFFLINE| Fallback[Call eMSP; use cache only if unreachable]
Try --> Hit{Cache hit?}
Hit -->|Yes| Cache
Hit -->|No| Live
Cache --> Result[Return decision]
Live --> Result
Fallback --> Result
style Cache fill:#dcfce7,stroke:#16a34a
style Live fill:#fef3c7,stroke:#d97706
The publishing pattern
eMSPs own Tokens. They expose a Sender Interface; CPOs expose a Receiver Interface.
Initial token push
When a new token is created (a new customer signs up, a new RFID card is issued):
PUT /ocpi/cpo/2.2.1/tokens/NL/EMO/TOKEN_42?type=RFID
Authorization: Token emsp-credentials-token
{...full Token object...}
Note the ?type=RFID query parameter. It is mandatory in 2.2.1 on every PUT, PATCH, and authorize call. The full round trip is in the worked example below.
Updates via PATCH
When something changes (token invalidated, contract type changed):
PATCH /ocpi/cpo/2.2.1/tokens/NL/EMO/TOKEN_42?type=RFID
{
"valid": false,
"last_updated": "2026-07-03T14:22:00Z" // PATCH sends only changed fields, plus last_updated
}
Bulk pull
CPOs can also pull a snapshot of all tokens from an eMSP. This is useful for initial integration and for periodic reconciliation:
GET /ocpi/emsp/2.2.1/tokens?date_from=...&offset=0&limit=50
Authorization: Token cpo-credentials-token
→ returns paginated list of all tokens this CPO is authorized to validate
Real-time authorization (when whitelist isn’t enough)
For tokens with whitelist: NEVER, or ALLOWED with a cache miss, the CPO calls back to the eMSP for a live decision:
POST /ocpi/emsp/2.2.1/tokens/TOKEN_42/authorize?type=RFID
Authorization: Token cpo-credentials-token
{
"location_id": "LOC1", // optional context (LocationReferences)
"evse_uids": ["3256"]
}
The eMSP responds:
{
"allowed": "ALLOWED", // ALLOWED, BLOCKED, EXPIRED, NO_CREDIT, NOT_ALLOWED
"token": {...}, // current token state
"authorization_reference": "AUTH_REF_98765"
}
The ?type=RFID query parameter is mandatory here too. The full authorize round trip, request and response, is in the worked example below.
allowed returns one of:
- ALLOWED: the session can proceed
- BLOCKED: the token is blocked (lost card, contract canceled)
- EXPIRED: the token’s validity period has ended
- NO_CREDIT: the customer is out of credit, or the payment method is bad
- NOT_ALLOWED: denied for some other reason, such as a location-specific restriction
The CPO honors whatever the eMSP says. If ALLOWED, the session starts. Otherwise the driver gets a message.
The authorization_reference is the string that ties this decision to the eventual Session and CDR, so that “this session was authorized by this specific authorization event” is answerable months later during a dispute. How CDRs carry that chain through to settlement is where it pays off.
A complete worked example
The snippets above are trimmed for clarity. Here is the full round trip in both
directions the Tokens module actually runs: the eMSP pushing a Token to the CPO,
and the CPO calling the eMSP back for a live authorization. Watch the direction.
In Tokens the eMSP is the Sender and the CPO is the Receiver, the reverse
of Locations, Sessions, and CDRs. (Comments use // and # for teaching; real
JSON has no comments.)
1. The eMSP pushes a Token (PUT to the CPO)
# ── eMSP ──▶ CPO ────────────────────────────────────────────────────────────
# The eMSP owns the driver's token, so it PUSHES the full object to the CPO's
# Receiver Interface. PUT = "here is the complete, authoritative Token; replace
# whatever you hold for this uid." Use PATCH when only one field changed
# (e.g. flipping valid to false on a lost card).
# The ?type= query param is MANDATORY in 2.2.1. Omit it and a conformant CPO
# rejects the call. Values: RFID | APP_USER | AD_HOC_USER | OTHER, and the value
# must match the "type" in the body.
PUT /ocpi/cpo/2.2.1/tokens/NL/EMO/TOKEN_42?type=RFID HTTP/1.1
Host: cpo.example.com
# Auth scheme is the literal word "Token", NOT "Bearer", followed by 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
# The URL path carries {country_code}/{party_id}/{token_uid}. They MUST equal the
# country_code / party_id / uid in the body; a mismatch is a common reject. Note
# uid is unique only WITHIN this eMSP, so it is scoped globally by country_code +
# party_id + uid, and the path supplies exactly that triple.
{
"country_code": "NL", // required: ISO 3166-1 alpha-2 of the eMSP (2 chars)
"party_id": "EMO", // required: the eMSP's party id (3 chars)
"uid": "TOKEN_42", // required: unique WITHIN this eMSP only, not globally
"type": "RFID", // required: RFID | APP_USER | AD_HOC_USER | OTHER; must match ?type=
"contract_id": "NL-EMO-C02451-9", // required: the driver's contract id (eMA / eMI3 form)
"visual_number": "1234-5678", // optional: what's printed on the card
"issuer": "Example Mobility", // required: human-readable eMSP / brand name
// group_id is optional. It groups tokens (e.g. a corporate fleet) so an
// authorize decision on one can be reasoned about across the whole group.
"group_id": "EMO-FLEET-ACME", // optional: shared id across a group of tokens
"valid": true, // required: false invalidates the token (lost/expired)
// whitelist (required) drives the CPO's live-vs-cache decision at tap time:
// ALWAYS → trust the cached token, never call authorize
// ALLOWED → try cache, call authorize on a miss
// ALLOWED_OFFLINE → call authorize live; use cache only if eMSP unreachable
// NEVER → always call authorize live (see step 2)
"whitelist": "ALLOWED",
"language": "en", // optional: preferred display language (2 chars)
// default_profile_type is optional. It is the charging profile to apply by
// default: CHEAP | FAST | GREEN | REGULAR
"default_profile_type": "REGULAR",
// energy_contract is optional. It names the driver's own energy supplier, used
// for Smart Charging and green-energy preferences. supplier_name is required
// inside it; contract_id is optional.
"energy_contract": {
"supplier_name": "Example Energy", // required: name of the energy supplier
"contract_id": "EE-8842-2026" // optional: the supplier's contract id
},
// RFC 3339, UTC ("Z"). Bump last_updated on EVERY change so the CPO can detect
// real changes and resolve out-of-order updates (newest wins). Forget to bump
// it and the CPO silently keeps a stale token, a classic and painful bug.
"last_updated": "2026-07-03T09:00:00Z" // required
}
# ── CPO ──▶ eMSP (the response) ─────────────────────────────────────────────
HTTP/1.1 200 OK
Content-Type: application/json
X-Request-ID: 12345
X-Correlation-ID: 67890
{
"data": {}, // for a PUT, receivers usually echo nothing
// 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), so always read the
// envelope and never trust the HTTP status alone. 2xxx = client error,
// 3xxx = server error.
"status_code": 1000,
"status_message": "Success",
"timestamp": "2026-07-03T09:00:01Z"
}
That double-layer result is not specific to Tokens; every module works this way. OCPI error responses explained (coming soon) maps the full status_code ranges if you are building the handler once for all of them.
2. The CPO authorizes a token live (POST to the eMSP)
This is the Tokens module’s signature flow. When a driver taps a token whose
whitelist is NEVER (or ALLOWED and the CPO has no cache hit), the CPO asks
the eMSP, in real time, “can this token charge here, right now?”
# ── CPO ──▶ eMSP ────────────────────────────────────────────────────────────
# Now the direction flips: the CPO calls the eMSP's Sender Interface. Note there
# is NO country_code / party_id in this path, just the token_uid, but the
# ?type= query param is STILL mandatory.
POST /ocpi/emsp/2.2.1/tokens/TOKEN_42/authorize?type=RFID 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
X-Request-ID: 22334
X-Correlation-ID: 55667
# Body is an optional LocationReferences object telling the eMSP WHERE the driver
# is trying to charge, so it can apply location-specific rules. Omit the whole
# body to ask "is this token valid at all?"
{
"location_id": "LOC1", // the Location the tap happened at
"evse_uids": ["3256"] // array: the specific EVSE uid(s) in question
}
# ── eMSP ──▶ CPO (the response) ─────────────────────────────────────────────
HTTP/1.1 200 OK
Content-Type: application/json
X-Request-ID: 22334
X-Correlation-ID: 55667
{
// data is an AuthorizationInfo object, the live verdict:
"data": {
// allowed (required) is the decision the CPO MUST honor:
// ALLOWED | BLOCKED | EXPIRED | NO_CREDIT | NOT_ALLOWED
"allowed": "ALLOWED",
// token (required) is the eMSP's current, authoritative token state: the
// COMPLETE Token object, same shape as the PUT in step 1.
"token": {
"country_code": "NL", // required
"party_id": "EMO", // required
"uid": "TOKEN_42", // required
"type": "RFID", // required
"contract_id": "NL-EMO-C02451-9", // required
"visual_number": "1234-5678", // optional
"issuer": "Example Mobility", // required
"group_id": "EMO-FLEET-ACME", // optional
"valid": true, // required
"whitelist": "ALLOWED", // required
"language": "en", // optional
"default_profile_type": "REGULAR",// optional
"energy_contract": { // optional
"supplier_name": "Example Energy", // required inside energy_contract
"contract_id": "EE-8842-2026" // optional inside energy_contract
},
"last_updated": "2026-07-03T09:00:00Z" // required
},
// location (optional) is a LocationReferences echoing WHERE this verdict
// applies; it mirrors the request body. location_id is required inside it,
// evse_uids is an optional array of the specific EVSE uid(s).
"location": {
"location_id": "LOC1", // required inside location
"evse_uids": ["3256"] // optional: the EVSE uid(s) this covers
},
// authorization_reference (optional): tie this string to the resulting
// Session and CDR so the whole chain (auth → session → billing) stays
// traceable end to end.
"authorization_reference": "AUTH_REF_98765",
// info (optional) is a multilingual DisplayText to show the driver, e.g. the
// reason for a BLOCKED or NO_CREDIT verdict. Both fields are required when
// present. Shown here on a clean ALLOWED; often omitted entirely on success.
"info": {
"language": "en", // required inside info
"text": "Welcome, charging authorized." // required inside info
}
},
// Same rule as before: this is the OCPI-level result, not the HTTP status.
// A BLOCKED or NO_CREDIT verdict still comes back as HTTP 200 with
// status_code 1000. The call SUCCEEDED; the answer just happens to be "no".
// Read data.allowed for the verdict, status_code for whether the call worked.
"status_code": 1000,
"status_message": "Success",
"timestamp": "2026-07-03T09:05:00Z"
}
Token reservation
A token can also be the subject of a reservation, where the eMSP asks the CPO to hold a specific EVSE for a specific token for a window of time. That is what sits under in-app charger reservations.
The reservation itself does not live in Tokens. It runs through the Commands module, which is where RESERVE_NOW and its cancellation counterpart are defined. Tokens supplies the identity the reservation is held against.
Common implementation gotchas
The mandatory ?type= query parameter
The ?type= parameter is the easiest thing in this module to leave out, because
it looks decorative. In most REST APIs a query string is optional refinement. In
OCPI 2.2.1 it is mandatory on the PUT, the PATCH, and the authorize call, and
a conformant receiver rejects the request outright when it is missing:
┌── mandatory in 2.2.1 ──┐
PUT /ocpi/cpo/2.2.1/tokens/NL/EMO/TOKEN_42?type=RFID
PATCH /ocpi/cpo/2.2.1/tokens/NL/EMO/TOKEN_42?type=RFID
POST /ocpi/emsp/2.2.1/tokens/TOKEN_42/authorize?type=RFID
- The value must be one of
RFID,APP_USER,AD_HOC_USER, orOTHER. - It must match the
typefield in the body on a PUT or PATCH. - The reason it exists: a
uidis unique only within a given token type for an eMSP, sotypeis part of what identifies the token, not metadata about it.
uid is unique only within one eMSP
To reference a token globally you need country_code + party_id + uid, which is
exactly the triple the PUT and PATCH paths carry. Key your cache on uid alone
and two eMSPs will eventually hand you the same string, at which point one
customer’s authorization state starts answering for another’s. The failure mode
is nasty because it stays invisible with a single roaming partner and appears the
week you add the second.
Treating whitelist as a hint
whitelist is an instruction, not advice. Serve a NEVER token from cache and
you will start sessions the eMSP would have blocked, and you will discover it at
settlement when the CDR is disputed and you are the party holding the bag.
When a token is invalidated mid-session
A valid: false token has to be rejected at the next authorization. The harder
question is what happens to a session already running when the flag flipped.
Invalidation governs future authorizations rather than past ones: the running
session was authorized against a token that was valid when the driver tapped, and
its CDR still settles on those terms. What changes is the next tap. Building it
that way also spares you the temptation to hard-stop a live session, which
strands a driver mid-charge over a billing state change.
Whitelist staleness
An ALWAYS token that gets invalidated is only as blocked as the CPO’s last
update. Until the PATCH lands, the CPO’s cache still says the token is good and
it will keep starting sessions on it. Nothing in the module closes that window,
which is the point: ALWAYS is the eMSP explicitly buying latency by accepting a
period of exposure. When the exposure per session outweighs a second at the plug,
that is the argument for ALLOWED or NEVER.
Ad-hoc tokens are not published in advance
AD_HOC_USER covers a driver with no standing contract, paying at the terminal. There is nothing to pre-publish, because the token does not exist until the payment authorization does. Sessions started this way still produce a Token and a CDR, but the whitelist question never arises: the trust came from the payment rail, not from an eMSP’s customer record.
Privacy and GDPR
The uid itself is opaque and carries no PII. The fields around it are less
careful. visual_number reproduces what is printed on the card, issuer names
the eMSP, and contract_id is often structured enough to be linkable back to an
account. Under GDPR the decision about which optional fields go over the wire is
worth making deliberately, because every roaming partner that receives a Token
keeps a copy of it.
For the field-by-field syntax, including every endpoint variant and the full authorize flow, see the OCPI specification, Tokens module.
The part that is easy to miss
The Tokens module puts a caching policy inside the data. whitelist is a field
on the Token, set by the eMSP, and it tells the CPO how far it may trust its own
copy. That is an unusual arrangement. Normally a system picks its cache strategy
from its own latency and correctness needs. Here the party carrying the risk (the
eMSP, which eats the bad debt) sets the policy, and the party carrying the
latency (the CPO, whose driver is standing at the plug) has to live with it.
That inversion explains a lot of the friction in real integrations. A CPO measuring slow authorizations cannot fix them by caching harder, because tokens marked NEVER forbid exactly that. An eMSP tightening fraud controls cannot simply flip everything to NEVER, because it would be spending someone else’s response time to do it. What looks like a single enum value on a token is really a commercial negotiation that got serialized into JSON.
It also explains why the authorize call deserves more engineering attention than its size suggests. It is one of the very few OCPI calls with a human standing there while it runs. A Locations push can retry for a minute and nobody notices. A CDR can settle overnight. The authorize round trip happens with a driver’s hand on the cable, so the timeout you put on it is a product decision at least as much as an infrastructure one.