OCPI Tokens Module: Authentication Without Sharing Customer Data

How eMSPs grant their customers access to CPO stations via tokens — and how CPOs validate those tokens without ever seeing the underlying customer.

When a driver taps their app or RFID card at a charging station, something has to happen at the protocol level to say “yes, this person is authorized.” In OCPI, that something involves Tokens — digital identities that let CPOs and eMSPs share customer authorization without sharing the actual customer.

This is the privacy-preserving authentication layer of OCPI.

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’s owned by an eMSP (the issuer)
  • It’s known to CPOs (the validators) via OCPI publication
  • It maps to a real customer in the eMSP’s system, but the CPO doesn’t need to know who that customer is

This is the privacy model: the eMSP knows “this token belongs to John Smith with this payment method.” The CPO knows “token ABC123 is authorized to charge at our stations.” The CPO doesn’t see John Smith’s data; the eMSP doesn’t see the station’s internal operational details. OCPI is the contract layer between them.

The Token object

A typical Token looks like:

{
  "country_code": "NL",
  "party_id": "ABC",           // 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-ABC-C0123-X",  // the customer's contract identifier
  "visual_number": "1234 5678 9012",  // what's printed on the card (optional)
  "issuer": "ChargePoint",
  "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": "Greenchoice",  // required inside energy_contract
    "contract_id": "GC-8842-2026"    // optional inside energy_contract
  },
  "last_updated": "2026-07-03T09:00:00Z"
}

Key fields:

  • uid — what’s transmitted from the station (e.g., the RFID card’s chip ID) when a driver taps
  • type — what kind of token (physical RFID, app-based, ad-hoc payment, other)
  • valid — can be set to false to invalidate a token (lost card, expired contract, fraud)
  • whitelist — controls how aggressively CPOs cache/trust this token (more on this below)
  • contract_id — links the token to a customer contract; useful for billing reconciliation

Token types — what kind of authentication

The type field tells the CPO how the token will be presented:

  • RFID — a physical card. The chip ID is what the station reads.
  • APP_USER — an app-based authentication (e.g., “Start Session” tapped in the eMSP’s app)
  • AD_HOC_USER — a one-time, anonymous user (e.g., credit card swipe at the station, no account)
  • OTHER — anything else (e.g., a vehicle’s onboard contract via ISO 15118 Plug & Charge)

Different types have different security and trust profiles. AD_HOC_USER tokens, for example, typically have stricter validation requirements.

The whitelist field — the caching strategy

OCPI’s authentication has a latency tradeoff. Two options:

Option 1: 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 status (contract valid, payment method good, etc.) and responds yes or no. This is the most secure but slowest path.

Option 2: Pre-shared whitelist. The eMSP publishes a list of valid tokens to the CPO in advance. When a driver taps, the CPO checks the local whitelist instead of calling the eMSP. Fast, but the whitelist might be slightly stale.

The whitelist field tells the CPO which strategy to use:

  • ALWAYS — always validate from the whitelist. Don’t call back. Fastest, but the eMSP must update the whitelist aggressively.
  • NEVER — always call the eMSP live for every session. Slowest, but most accurate.
  • ALLOWED — try the whitelist first; if not present, call the eMSP. Hybrid.
  • ALLOWED_OFFLINE — only fall back to whitelist if the eMSP is unreachable.

In practice, ALWAYS is common for low-stakes, high-volume token types (basic RFID cards on prepaid accounts). NEVER or ALLOWED is common for higher-risk types (corporate accounts with credit limits).

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/TNM/TOKEN_42?type=RFID
Authorization: Token emsp-credentials-token

{...full Token object...}

Note the ?type=RFID query parameter — it’s 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/TNM/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 (cache miss), the CPO calls back to the eMSP for live authorization:

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 — session can proceed
  • BLOCKED — token is blocked (lost card, contract canceled)
  • EXPIRED — token’s validity period has ended
  • NO_CREDIT — customer is out of credit / payment method is bad
  • NOT_ALLOWED — denied for other reasons (location-specific restrictions, etc.)

The CPO honors whatever the eMSP says. If ALLOWED, the session starts. Otherwise, the driver gets a message.

The authorization_reference is a token that the CPO ties to the eventual Session and CDR — letting them link “this session was authorized by this specific authorization event.”

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 it must
# match the "type" in the body.
PUT /ocpi/cpo/2.2.1/tokens/NL/TNM/TOKEN_42?type=RFID 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

# 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; the path supplies exactly that triple.
{
  "country_code": "NL",             // required — ISO 3166-1 alpha-2 of the eMSP (2 chars)
  "party_id": "TNM",                // 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-TNM-C02451-9", // required — the driver's contract id (eMA / eMI3 form)
  "visual_number": "1234-5678",     // optional — what's printed on the card
  "issuer": "The New Motion",       // required — human-readable eMSP / brand name
  // group_id is optional — used to group tokens (e.g. a corporate fleet) so an
  // authorize decision on one can be reasoned about across the whole group.
  "group_id": "TNM-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 — the charging profile to apply by default:
  //   CHEAP | FAST | GREEN | REGULAR
  "default_profile_type": "REGULAR",
  // energy_contract is optional — the driver's own energy supplier, used for
  // Smart Charging / green-energy preferences. supplier_name is required inside
  // it; contract_id is optional.
  "energy_contract": {
    "supplier_name": "Greenchoice",     // required — name of the energy supplier
    "contract_id": "GC-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, 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) — always read the
  // envelope, never trust the HTTP status alone. 2xxx = client error, 3xxx = server.
  "status_code": 1000,
  "status_message": "Success",
  "timestamp": "2026-07-03T09:00:01Z"
}

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": "TNM",                // required
      "uid": "TOKEN_42",                // required
      "type": "RFID",                   // required
      "contract_id": "NL-TNM-C02451-9", // required
      "visual_number": "1234-5678",     // optional
      "issuer": "The New Motion",       // required
      "group_id": "TNM-FLEET-ACME",     // optional
      "valid": true,                    // required
      "whitelist": "ALLOWED",           // required
      "language": "en",                 // optional
      "default_profile_type": "REGULAR",// optional
      "energy_contract": {              // optional
        "supplier_name": "Greenchoice", // required inside energy_contract
        "contract_id": "GC-8842-2026"   // optional inside energy_contract
      },
      "last_updated": "2026-07-03T09:00:00Z"  // required
    },
    // location (optional) is a LocationReferences echoing WHERE this verdict
    // applies — 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 — is
    // 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 / 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 / NO_CREDIT verdict still comes back as HTTP 200 + status_code 1000
  // — the call SUCCEEDED; the answer just happens to be "no". Read data.allowed
  // for the verdict, and status_code for whether the call itself worked.
  "status_code": 1000,
  "status_message": "Success",
  "timestamp": "2026-07-03T09:05:00Z"
}

Token reservation

Some types of tokens can be reserved — meaning the eMSP can ask the CPO to hold a specific EVSE for a specific token for a window of time. This is the basis for in-app charger reservations.

Reservation flows are handled through the Commands module (covered in its own article — coming soon).

Common implementation gotchas

A few things that catch real-world implementations:

The mandatory ?type= query parameter — the #1 mistake

The single most common reject in the Tokens module is forgetting the ?type= query parameter. In OCPI 2.2.1 it is mandatory on the PUT, the PATCH, and the authorize call — omit it and a conformant receiver rejects the request outright:

                          ┌── mandatory in 2.2.1 ──┐
PUT      /ocpi/cpo/2.2.1/tokens/NL/TNM/TOKEN_42?type=RFID
PATCH    /ocpi/cpo/2.2.1/tokens/NL/TNM/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, or OTHER.
  • It must match the type field in the body on a PUT/PATCH.
  • The reason it exists: a uid is only unique within a given token type for an eMSP, so the type disambiguates which token the path refers to.

Two more traps in the same family:

  • uid is unique only within the eMSP. To reference a token globally you need country_code + party_id + uid — the exact triple the PUT/PATCH path carries. CPOs that key their cache on uid alone create cross-eMSP token collisions.
  • The whitelist value governs whether you must call authorize live. ALWAYS means trust the cache and never call back; NEVER means always call live; ALLOWED / ALLOWED_OFFLINE are the hybrids. Treat NEVER like ALWAYS and you’ll happily start sessions the eMSP would have blocked.

For the full field-by-field syntax — including every endpoint variant and the authorize flow — see the OCPI 2.2.1 specification, Tokens module.

Token uid uniqueness

A token’s uid is unique within one eMSP. To reference a token globally you need country_code + party_id + uid. CPOs that conflate these create cross-eMSP token collisions.

When tokens get invalidated

A valid: false token should be rejected. But what if a session was already in progress when the token was invalidated? Spec behavior: completed sessions stand. In-progress sessions continue (until they naturally end). Future sessions are blocked.

Whitelist staleness

A whitelist: ALWAYS token that gets invalidated needs to propagate to the CPO quickly. Updates can lag by minutes (network blip) or hours (worst case). CPOs that haven’t received the update will continue to allow sessions. This is an operational risk eMSPs accept when choosing ALWAYS.

Ad-hoc vs registered tokens

AD_HOC_USER tokens are anonymous one-time users (credit card at the station). They’re typically not pre-published; the station generates one on the fly. CPOs handle them differently from registered tokens.

Privacy and GDPR

A Token contains no PII by default (uid is opaque). But visual_number, issuer, and contract_id can leak information. eMSPs in GDPR jurisdictions should be deliberate about what’s published.

Three things to take from this

  1. Tokens are how eMSPs authorize their customers at CPO stations — without sharing the underlying customer identity. The CPO sees only the token, not the person.

  2. Whitelist strategy is a latency-vs-accuracy tradeoff. ALWAYS is fast but can be stale. NEVER is slow but accurate. ALLOWED is the practical hybrid.

  3. Token validity is mutable. valid: false blocks future use. Invalidations need to propagate quickly to CPOs to prevent fraud.

The Tokens module is small in spec terms but central in operations. Every successful charging session starts with a token validation. Get this right and customers can roam seamlessly.

Quick check

Q1. Who owns the OCPI Tokens that authorize drivers to charge?

Frequently asked questions

What is an OCPI Token?

A Token in OCPI represents a customer's right to charge at a station — it's like a digital RFID card. The eMSP issues tokens to its customers; CPOs validate them at session start. The actual customer identity stays with the eMSP.

Are OCPI Tokens the same as authentication tokens (for API auth)?

No — they're different. OCPI Tokens identify customers (drivers). OCPI authentication tokens (in the Credentials module) authenticate the API connection between parties. Confusingly named, but different things.

Found this useful? Share it.