OCPP 2.1 Edition 2 Section S

Battery Swapping - CSMS Developer Guide

Based on OCPP 2.1 Edition 2 Specification (Part 2), Section S (Battery Swapping). This guide covers all battery swap flows (S01–S04), including local and remote authorization, battery in/out events, charging transactions, error handling, and state management from the CSMS perspective using OCPP-J (JSON over WebSocket).

11 Sections
4 Use Cases
S01 – S04

1. Overview

Introduction

Battery swapping is the process where an EV driver exchanges a near-empty battery for a charged one at a Battery Swap Station (BSS). The returned battery is placed in a dock and recharged. This section covers the CSMS implementation for managing the full battery swap lifecycle.

Key Concepts

Swap ≠ Charging Transaction

The swap action itself is recorded via BatterySwapRequest messages, not TransactionEvent messages. Pricing can be based on SoC differences, monthly fees, etc.

Charging IS a Transaction

The actual charging of returned batteries in docks is tracked via standard TransactionEventRequest messages, like regular EV charging.

Device Model Mapping

A BSS is modeled as a Charging Station where each battery slot equals one logical EVSE. Each EVSE powers one battery slot.

Slot Availability Semantics

Available = slot is empty, Occupied = slot has a battery, Unavailable = slot cannot be used.

Important: The swap can consist of a single battery or a set of batteries depending on the EV type. The default swap order is "In-Out" (old battery in first, new battery out second). The reverse ("Out-In") is also supported and reported via BatterySwapCtrlr.SwapOrder.

Messages Involved (CSMS Perspective)

Message Direction CSMS Role
AuthorizeRequest BSS → CSMS Respond (grant/deny swap)
RequestBatterySwapRequest CSMS → BSS Send (initiate remote swap)
RequestBatterySwapResponse BSS → CSMS Receive (confirmation)
BatterySwapRequest BSS → CSMS Respond (acknowledge in/out)
NotifyEventRequest BSS → CSMS Respond (slot state changes)
TransactionEventRequest BSS → CSMS Respond (charging transactions)

2. Device Model & Slot Mapping

Architecture

A Battery Swap Station (BSS) is represented in OCPP as a Charging Station where each battery slot maps to one logical EVSE. Each EVSE ID in battery swap messages refers to the slot number.

Slot Mapping

BSS Device Model Structure
Battery Swap Station (ChargingStation)
├── EVSE 1  (Battery Slot 1) ── Connector (ConnectorType = slot-specific)
├── EVSE 2  (Battery Slot 2) ── Connector (ConnectorType = slot-specific)
├── EVSE 3  (Battery Slot 3) ── Connector (ConnectorType = slot-specific)
└── ...

Note: Different battery slot types can be represented by specifying different ConnectorType values per EVSE in the Device Model.

CSMS Slot Tracking

The CSMS should track the following data per slot:

Field Type Description
evseId integer The slot number (= EVSE ID)
availabilityState string Available / Occupied / Unavailable
batterySerialNumber string Serial number of battery currently in slot (if occupied)
batterySoC number Current state of charge (0-100%)
batterySoH number State of health (0-100%)
chargingTransactionId string Active charging transaction ID (if battery is being charged)

3. Configuration Variables

Reference

The CSMS should be aware of these BSS configuration variables, readable/writable via GetVariables / SetVariables.

Variable Description
BatterySwapInTimeout Timeout (seconds) for the EV driver to insert a battery after authorization. If exceeded, BSS aborts the swap. No BatterySwapRequest is sent.
BatterySwapOutTimeout Timeout (seconds) for old batteries to be removed after insertion of new ones. If exceeded, BSS sends BatterySwapRequest with eventType = BatteryOutTimeout.
BatterySwapTargetSoc Target SoC at which a battery in a slot becomes eligible for swapping.
BatterySwapMaxSoc Maximum SoC to which a battery is charged. Must be >= BatterySwapTargetSoc.
BatterySwapIdtoken Predefined idToken value used in TransactionEventRequest for battery charging transactions. If set, uses idToken.type = "Central". If not set, uses idToken.type = "NoAuthorization".
BatterySwapCtrlr.SwapOrder "In-Out" (default) or "Out-In". Indicates whether BSS does old-battery-in-first or new-battery-out-first.
CustomImplementationEnabled Boolean. If true, BSS supports the custom BatterySwapResponse extension for rejecting batteries. Vendor ID: org.openchargealliance.batteryswapresponse
BatteryCartridgeSoC Component/variable to report battery SoC per slot (readable via GetVariablesRequest).
TxStartPoint Must be "EVConnected" for battery swap stations. Transaction starts when battery is inserted.
TxStopPoint Must be "EVConnected" for battery swap stations. Transaction ends when battery is removed.

4. S01 — Battery Swap Local Authorization

BSS-Initiated
Use Case ID S01
Direction BSS → CSMS (BSS initiates)
Trigger EV Driver presents RFID card at Battery Swap Station
OCPP Messages AuthorizeRequest / AuthorizeResponse

Flow Diagram

Sequence Diagram (CSMS perspective)
User              BSS                    CSMS
 |                 |                      |
 |--present RFID-->|                      |
 |                 |--AuthorizeRequest--->|
 |                 |                      |-- validate idToken
 |                 |<--AuthorizeResponse--|
 |<--accept swap---|                      |

CSMS Handler: AuthorizeRequest

When received, validate the idToken for swap authorization.

Incoming Payload: AuthorizeRequest

Field Type Required Description
idToken IdTokenType Yes The token presented by the EV driver
idToken.idToken string (max 255) Yes The token value (case insensitive)
idToken.type string (max 20) Yes Token type (e.g., ISO14443, ISO15693, eMAID, Central)
idToken.additionalInfo AdditionalInfoType[] No Additional identification info
certificate string (max 10000) No X.509 PEM certificate chain (central contract validation)
iso15118CertificateHashData OCSPRequestDataType[] No OCSP data for certificate validation (max 4)

Response Payload: AuthorizeResponse

Field Type Required Description
idTokenInfo IdTokenInfoType Yes Authorization result
idTokenInfo.status AuthorizationStatusEnumType Yes See status table below
idTokenInfo.cacheExpiryDateTime dateTime No Token cache expiry
idTokenInfo.chargingPriority integer No Priority (-9 to 9, default 0)
idTokenInfo.groupIdToken IdTokenType No Group token
idTokenInfo.language1 string (max 8) No Preferred UI language (RFC 5646)
idTokenInfo.language2 string (max 8) No Second preferred language
idTokenInfo.evseId integer[] No Restrict to specific EVSEs/slots
idTokenInfo.personalMessage MessageContentType No Message to display
certificateStatus AuthorizeCertificateStatusEnumType No Certificate validation result
allowedEnergyTransfer EnergyTransferModeEnumType[] No Allowed energy modes
tariff TariffType No Applicable tariff

AuthorizationStatusEnumType Values

Value Description
Accepted Token is valid, swap authorized
Blocked Token is blocked
Expired Token has expired
Invalid Token is unknown/invalid
NoCredit No credit available
NotAllowedTypeEVSE Token not allowed for this EVSE type
NotAtThisLocation Token not valid at this location
NotAtThisTime Token not valid at this time
Unknown Token could not be validated (offline scenario)
ConcurrentTx Token already has an active transaction

Implementation Logic

CSMS handleAuthorizeRequest — Pseudocode
function handleAuthorizeRequest(stationId, request):
    idToken = request.idToken

    // 1. Look up the token in the authorization database
    tokenRecord = database.findToken(idToken.idToken, idToken.type)

    // 2. Validate the token
    if tokenRecord is null:
        return { idTokenInfo: { status: "Invalid" } }

    if tokenRecord.isBlocked:
        return { idTokenInfo: { status: "Blocked" } }

    if tokenRecord.expiryDate < now():
        return { idTokenInfo: { status: "Expired" } }

    if not tokenRecord.hasValidSwapContract:
        return { idTokenInfo: { status: "Invalid" } }

    // 3. Check location-based restrictions
    if not tokenRecord.isAllowedAtStation(stationId):
        return { idTokenInfo: { status: "NotAtThisLocation" } }

    // 4. Grant authorization
    return {
        idTokenInfo: {
            status: "Accepted",
            cacheExpiryDateTime: "2025-12-31T23:59:59Z",
            language1: tokenRecord.preferredLanguage
        }
    }

Requirements

The authorization for battery swapping follows the same process as use case C01 (EV Driver Authorization using RFID), but it is NOT used to start a charging transaction — it authorizes the swap service. Requirements about concurrent transactions from C01 do not apply since the swap authorization is not a transaction.

S01.FR.01 — Upon Accepted authorization AND enough batteries available, the BSS opens empty slots for battery insertion.

S01.FR.02 — If the EV driver does not insert a battery before BatterySwapInTimeout, the BSS ends authorization and aborts. No BatterySwapRequest is sent to CSMS.

5. S02 — Battery Swap Remote Start

CSMS-Initiated
Use Case ID S02
Direction CSMS → BSS (CSMS initiates)
Trigger EV Driver requests battery swap remotely (e.g., via smartphone app)
OCPP Messages RequestBatterySwapRequest / RequestBatterySwapResponse

Flow Diagram

Sequence Diagram (CSMS perspective)
User                          CSMS                    BSS
 |                             |                       |
 |--request via app---------->|                       |
 |                             |--RequestBatterySwap-->|
 |                             |   Request             |
 |                             |<--RequestBatterySwap--|
 |                             |   Response            |
 |<--------accept swap--------|---------------------->|

CSMS Action: Send RequestBatterySwapRequest

This is a message the CSMS sends TO the BSS (CSMS-initiated).

Outgoing Payload: RequestBatterySwapRequest

Field Type Required Description
idToken IdTokenType Yes Authorization token for the EV driver
idToken.idToken string (max 255) Yes Token value
idToken.type string (max 20) Yes Token type
idToken.additionalInfo AdditionalInfoType[] No Additional info
requestId integer Yes Unique request ID to correlate with subsequent BatterySwapRequest messages
RequestBatterySwapRequest — Example
{
  "idToken": {
    "idToken": "AABBCCDD11223344",
    "type": "ISO14443"
  },
  "requestId": 42
}

Handle Response: RequestBatterySwapResponse

Field Type Required Description
status GenericStatusEnumType Yes "Accepted" or "Rejected"
statusInfo StatusInfoType No Additional status details
statusInfo.reasonCode string (max 20) Yes* Predefined reason code (* if statusInfo present)
statusInfo.additionalInfo string (max 1024) No Human-readable details

GenericStatusEnumType Values

Value Meaning
Accepted BSS accepts the swap request, enough batteries available
Rejected BSS rejects the swap request
RequestBatterySwapResponse — Success
{
  "status": "Accepted"
}
RequestBatterySwapResponse — Failure
{
  "status": "Rejected",
  "statusInfo": {
    "reasonCode": "NoBatteryAvailable",
    "additionalInfo": "All battery slots are empty or charging"
  }
}

Implementation Logic

CSMS initiateBatterySwapRemote — Pseudocode
function initiateBatterySwapRemote(stationId, userId):
    // 1. Validate user authorization
    user = database.findUser(userId)
    if not user.hasValidSwapContract():
        return error("User not authorized for battery swapping")

    // 2. Generate unique requestId
    requestId = generateUniqueRequestId()

    // 3. Store the pending swap request for correlation
    database.storePendingSwapRequest({
        requestId: requestId,
        stationId: stationId,
        userId: userId,
        idToken: user.idToken,
        createdAt: now(),
        status: "Pending"
    })

    // 4. Send the request to BSS
    response = sendToBSS(stationId, "RequestBatterySwapRequest", {
        idToken: {
            idToken: user.idToken.value,
            type: user.idToken.type
        },
        requestId: requestId
    })

    // 5. Handle response
    if response.status == "Accepted":
        database.updatePendingSwapRequest(requestId, { status: "Accepted" })
        // Now wait for BatterySwapRequest messages (S03)
    else:
        database.updatePendingSwapRequest(requestId, {
            status: "Rejected",
            reason: response.statusInfo?.reasonCode
        })
        notifyUser(userId, "Swap rejected: " + response.statusInfo?.additionalInfo)

Requirements

S02.FR.01 — The BSS accepts only if the idToken is authorized by CSMS AND enough batteries are available.

S02.FR.02 — The BSS uses the same requestId from this request in the subsequent BatterySwapRequest, allowing CSMS to correlate the remote start with the actual swap event.

S02.FR.03 — CSMS SHALL NOT send RequestBatterySwapRequest for an unauthorized idToken.

S02.FR.04 — If the BSS receives a request with an authorized idToken but has no batteries, it responds Rejected with reasonCode = "NoBatteryAvailable".

S02.FR.05 — If the EV driver does not insert a battery before BatterySwapInTimeout, the BSS aborts. No BatterySwapRequest is sent.

6. S03 — Battery Swap In/Out

BSS-Initiated
Use Case ID S03
Direction BSS → CSMS (BSS initiates)
Trigger After authorization (S01 or S02), BSS records battery insertions and removals
OCPP Messages BatterySwapRequest / BatterySwapResponse, NotifyEventRequest / NotifyEventResponse

Flow Diagram (Default "In-Out" Order)

Sequence Diagram (CSMS perspective)
User              BSS                              CSMS
 |                 |                                 |
 |  [User authorized for swapping - via S01 or S02]  |
 |                 |                                 |
 |--insert empty-->|                                 |
 |  batteries      |                                 |
 |                 |--BatterySwapRequest------------>|  eventType=BatteryIn
 |                 |  (requestId, idToken,            |  batteryData[] with
 |                 |   batteryData[])                 |  evseId, serialNumber,
 |                 |<--BatterySwapResponse-----------|  soC, soH
 |                 |                                 |
 |                 |  [for each inserted battery]     |
 |                 |--NotifyEventRequest------------>|  AvailabilityState
 |                 |  (Occupied)                      |  = "Occupied"
 |                 |<--NotifyEventResponse-----------|
 |                 |                                 |
 |                 |  [Start charging inserted battery]
 |                 |                                 |
 |<--present new---|                                 |
 |   batteries     |                                 |
 |                 |                                 |
 |--take out new-->|                                 |
 |   batteries     |                                 |
 |                 |                                 |
 |                 |  [If battery was still charging:] |
 |                 |--TransactionEventRequest------->|  eventType=Ended
 |                 |<--TransactionEventResponse------|
 |                 |                                 |
 |                 |--BatterySwapRequest------------>|  eventType=BatteryOut
 |                 |  (same requestId, idToken,       |  batteryData[] with
 |                 |   batteryData[])                 |  evseId, serialNumber,
 |                 |<--BatterySwapResponse-----------|  soC, soH
 |                 |                                 |
 |                 |  [for each extracted battery]    |
 |                 |--NotifyEventRequest------------>|  AvailabilityState
 |                 |  (Available)                     |  = "Available"
 |                 |<--NotifyEventResponse-----------|
 |                 |                                 |
 |--leaves-------->|                                 |

CSMS Handler: BatterySwapRequest

This is the primary battery swap message. The BSS sends it to report battery insertion and removal events.

Incoming Payload: BatterySwapRequest

Field Type Required Description
eventType BatterySwapEventEnumType Yes Type of swap event
requestId integer Yes Correlates BatteryIn/Out events and optional RequestBatterySwapRequest
idToken IdTokenType Yes The authorized EV driver's token
batteryData BatteryDataType[] (min 1) Yes Array of battery data, one entry per battery

BatterySwapEventEnumType Values

Value Description
BatteryIn Set of empty batteries has been inserted into the BSS
BatteryOut Set of charged batteries has been removed from the BSS
BatteryOutTimeout Old batteries were not removed within BatterySwapOutTimeout. CSMS receives an orphan BatteryIn without a matching BatteryOut.

BatteryDataType Fields

Field Type Required Description
evseId integer (>= 0) Yes Slot number where battery was inserted/removed
serialNumber string (max 50) Yes Battery serial number
soC number (0.0-100.0) Yes State of Charge at time of event
soH number (0.0-100.0) Yes State of Health
productionDate dateTime No Battery production date
vendorInfo string (max 500) No Vendor-specific info (free format)

Response Payload: BatterySwapResponse

Note: This is an empty acknowledgement — the request cannot be rejected via the standard response. See Error Handling for the customData extension rejection mechanism.

BatterySwapRequest — BatteryIn
{
  "eventType": "BatteryIn",
  "requestId": 42,
  "idToken": {
    "idToken": "AABBCCDD11223344",
    "type": "ISO14443"
  },
  "batteryData": [
    {
      "evseId": 3,
      "serialNumber": "BAT-2025-001234",
      "soC": 8.5,
      "soH": 95.2,
      "productionDate": "2024-06-15T00:00:00Z"
    },
    {
      "evseId": 7,
      "serialNumber": "BAT-2025-001235",
      "soC": 12.1,
      "soH": 97.8
    }
  ]
}
BatterySwapRequest — BatteryOut
{
  "eventType": "BatteryOut",
  "requestId": 42,
  "idToken": {
    "idToken": "AABBCCDD11223344",
    "type": "ISO14443"
  },
  "batteryData": [
    {
      "evseId": 1,
      "serialNumber": "BAT-2025-000987",
      "soC": 92.0,
      "soH": 96.5
    },
    {
      "evseId": 5,
      "serialNumber": "BAT-2025-000654",
      "soC": 88.3,
      "soH": 94.1
    }
  ]
}
BatterySwapResponse — Empty Acknowledgement
{}
BatterySwapResponse — With customData Rejection Extension
{
  "customData": {
    "vendorId": "org.openchargealliance.batteryswapresponse",
    "status": "Rejected",
    "statusInfo": {
      "reasonCode": "BatteryUnknown",
      "additionalInfo": "Not a battery of this CPO"
    }
  }
}

CSMS Handler: NotifyEventRequest (Slot State Changes)

After each battery insertion/removal, the BSS sends NotifyEventRequest messages to report the AvailabilityState change for each affected slot.

Relevant EventDataType Fields

Field Type Description
eventId integer (>= 0) Unique event identifier
timestamp dateTime When the event occurred
trigger EventTriggerEnumType "Delta" for state change
actualValue string (max 2500) The new value: "Occupied" or "Available"
component.name string "Connector"
component.evse.id integer Slot number
variable.name string "AvailabilityState"
NotifyEventRequest — Slot becomes Occupied (battery inserted)
{
  "generatedAt": "2025-12-03T14:30:00Z",
  "seqNo": 0,
  "eventData": [
    {
      "eventId": 101,
      "timestamp": "2025-12-03T14:30:00Z",
      "trigger": "Delta",
      "actualValue": "Occupied",
      "eventNotificationType": "HardWiredNotification",
      "component": {
        "name": "Connector",
        "evse": { "id": 3 }
      },
      "variable": {
        "name": "AvailabilityState"
      }
    }
  ]
}

Note: StatusNotificationRequest can also be used instead of NotifyEventRequest for AvailabilityState, but StatusNotificationRequest has been deprecated. Prefer handling NotifyEventRequest.

Implementation Logic

CSMS handleBatterySwapRequest — Pseudocode
function handleBatterySwapRequest(stationId, request):
    eventType = request.eventType
    requestId = request.requestId
    idToken = request.idToken
    batteries = request.batteryData

    switch eventType:

        case "BatteryIn":
            // 1. Record the battery insertion event
            swapRecord = database.findOrCreateSwapRecord(requestId, stationId)
            swapRecord.idToken = idToken
            swapRecord.batteryInTimestamp = now()
            swapRecord.insertedBatteries = []

            for battery in batteries:
                swapRecord.insertedBatteries.push({
                    evseId: battery.evseId,
                    serialNumber: battery.serialNumber,
                    soC: battery.soC,
                    soH: battery.soH,
                    productionDate: battery.productionDate
                })

                // 2. Update slot inventory
                database.updateSlot(stationId, battery.evseId, {
                    status: "Occupied",
                    batterySerial: battery.serialNumber,
                    batterySoC: battery.soC,
                    batterySoH: battery.soH
                })

            database.saveSwapRecord(swapRecord)
            return {}   // Empty acknowledgement

        case "BatteryOut":
            // 1. Record the battery removal event
            swapRecord = database.findSwapRecord(requestId, stationId)
            swapRecord.batteryOutTimestamp = now()
            swapRecord.removedBatteries = []

            for battery in batteries:
                swapRecord.removedBatteries.push({
                    evseId: battery.evseId,
                    serialNumber: battery.serialNumber,
                    soC: battery.soC,
                    soH: battery.soH
                })

                // 2. Update slot inventory
                database.updateSlot(stationId, battery.evseId, {
                    status: "Available",
                    batterySerial: null,
                    batterySoC: null,
                    batterySoH: null
                })

            // 3. Calculate swap billing
            billing.calculateSwapCost(swapRecord)
            database.saveSwapRecord(swapRecord)
            return {}

        case "BatteryOutTimeout":
            // The old batteries were not removed in time
            swapRecord = database.findSwapRecord(requestId, stationId)
            swapRecord.status = "TimedOut"
            swapRecord.timeoutBatteries = batteries

            for battery in batteries:
                database.updateSlot(stationId, battery.evseId, {
                    status: "Available"
                })

            database.saveSwapRecord(swapRecord)
            return {}
CSMS handleNotifyEventRequest — Pseudocode
function handleNotifyEventRequest(stationId, request):
    for event in request.eventData:
        component = event.component
        variable = event.variable

        // Filter for battery slot state changes
        if component.name == "Connector" and variable.name == "AvailabilityState":
            slotId = component.evse.id
            newState = event.actualValue   // "Occupied" or "Available"

            database.updateSlotAvailability(stationId, slotId, newState)

    // Always return empty acknowledgement
    return {}

Requirements

S03.FR.01BatteryIn event includes the requestId, idToken, and batteryData with evseId set to slot number where each battery was inserted.

S03.FR.02 — After BatteryIn, BSS reports each affected connector as Occupied via NotifyEventRequest.

S03.FR.03BatteryOut event uses the same requestId as the BatteryIn event. This is how CSMS correlates the two halves of a swap.

S03.FR.04 — After BatteryOut, BSS reports each affected connector as Available via NotifyEventRequest.

S03.FR.05 — If BSS has no (or not enough) batteries available when the EV driver authorizes, the BSS refuses the swap operation locally.

S03.FR.06 — If old batteries are not removed within BatterySwapOutTimeout, BSS sends BatteryOutTimeout. CSMS ends up with an orphan BatteryIn without a corresponding BatteryOut.

S03.FR.07 — If the BSS uses reverse swap order ("Out-In"), it reports BatterySwapCtrlr.SwapOrder = "Out-In".

7. S04 — Battery Swap Charging

BSS-Initiated
Use Case ID S04
Direction BSS → CSMS (BSS initiates)
Trigger After empty batteries are inserted into the BSS (via S03), the BSS charges them
OCPP Messages TransactionEventRequest / TransactionEventResponse

The CSMS handles TransactionEventRequest for battery charging the same way as normal EV charging, with battery-swap-specific considerations for token identification and trigger values.

Flow Diagram

Sequence Diagram (CSMS perspective)
BSS                                    CSMS
 |                                      |
 |  [Battery inserted, start charging]   |
 |                                      |
 |--TransactionEventRequest----------->|  eventType=Started
 |  (triggerReason=CablePluggedIn,      |  idToken.type=NoAuthorization
 |   chargingState=EVConnected)         |  OR idToken=BatterySwapIdtoken
 |<--TransactionEventResponse----------|
 |                                      |
 |--TransactionEventRequest----------->|  eventType=Updated
 |  (triggerReason=ChargingStateChanged |  chargingState=Charging
 |   chargingState=Charging)            |
 |<--TransactionEventResponse----------|
 |                                      |
 |  [loop: periodically send SoC]       |
 |--TransactionEventRequest----------->|  eventType=Updated
 |  (triggerReason=MeterValuePeriodic,  |  measurand=SoC
 |   meterValue=[SoC: XX])             |
 |<--TransactionEventResponse----------|
 |                                      |
 |  [When SoC reaches BatterySwapMaxSoc]|
 |--TransactionEventRequest----------->|  eventType=Updated
 |  (triggerReason=EnergyLimitReached,  |  chargingState=SuspendedEVSE
 |   chargingState=SuspendedEVSE)      |
 |<--TransactionEventResponse----------|
 |                                      |
 |  [When battery is extracted]          |
 |--TransactionEventRequest----------->|  eventType=Ended
 |  (triggerReason=EVCommunicationLost, |  stoppedReason=EVDisconnected
 |   chargingState=Idle,                |
 |   stoppedReason=EVDisconnected)      |
 |<--TransactionEventResponse----------|

Transaction Started

Identifying a battery swap charging transaction:

With BatterySwapIdtoken configured

idToken.idToken = value of BatterySwapIdtoken
idToken.type = "Central"

Without BatterySwapIdtoken

idToken.idToken = "" (empty string)
idToken.type = "NoAuthorization"

Important: The BatterySwapRequest idToken belongs to the EV Driver. The TransactionEventRequest idToken is NOT the EV driver — it is either the BatterySwapIdtoken or NoAuthorization. These are different identity contexts.

TransactionEventRequest — Started (with BatterySwapIdtoken)
{
  "eventType": "Started",
  "timestamp": "2025-12-03T14:31:00Z",
  "triggerReason": "CablePluggedIn",
  "seqNo": 0,
  "transactionInfo": {
    "transactionId": "tx-bss-slot3-20251203",
    "chargingState": "EVConnected"
  },
  "evse": { "id": 3 },
  "idToken": {
    "idToken": "BSS-INTERNAL-TOKEN-001",
    "type": "Central"
  }
}
TransactionEventRequest — Started (without BatterySwapIdtoken)
{
  "eventType": "Started",
  "timestamp": "2025-12-03T14:31:00Z",
  "triggerReason": "CablePluggedIn",
  "seqNo": 0,
  "transactionInfo": {
    "transactionId": "tx-bss-slot3-20251203",
    "chargingState": "EVConnected"
  },
  "evse": { "id": 3 },
  "idToken": {
    "idToken": "",
    "type": "NoAuthorization"
  }
}

Periodic SoC Updates

During charging, the BSS sends periodic meter values with measurand = "SoC".

TransactionEventRequest — Updated (periodic SoC)
{
  "eventType": "Updated",
  "timestamp": "2025-12-03T14:45:00Z",
  "triggerReason": "MeterValuePeriodic",
  "seqNo": 3,
  "transactionInfo": {
    "transactionId": "tx-bss-slot3-20251203",
    "chargingState": "Charging"
  },
  "evse": { "id": 3 },
  "meterValue": [
    {
      "timestamp": "2025-12-03T14:45:00Z",
      "sampledValue": [
        {
          "value": 45.0,
          "measurand": "SoC"
        }
      ]
    }
  ]
}

Max SoC Reached (Suspend)

When the battery reaches BatterySwapMaxSoc, charging is suspended but the transaction stays alive.

TransactionEventRequest — Updated (energy limit reached)
{
  "eventType": "Updated",
  "timestamp": "2025-12-03T16:20:00Z",
  "triggerReason": "EnergyLimitReached",
  "seqNo": 15,
  "transactionInfo": {
    "transactionId": "tx-bss-slot3-20251203",
    "chargingState": "SuspendedEVSE"
  },
  "evse": { "id": 3 }
}

Important: The transaction is kept alive (not ended) even after reaching max SoC. This allows energy transfer to be restarted (e.g., for V2X operations). The transaction only ends when the battery is physically extracted.

Battery Extracted (End)

When the battery is removed from the slot (given to an EV driver during a swap), the charging transaction ends.

TransactionEventRequest — Ended
{
  "eventType": "Ended",
  "timestamp": "2025-12-03T18:00:00Z",
  "triggerReason": "EVCommunicationLost",
  "seqNo": 20,
  "transactionInfo": {
    "transactionId": "tx-bss-slot3-20251203",
    "chargingState": "Idle",
    "stoppedReason": "EVDisconnected",
    "timeSpentCharging": 12540
  },
  "evse": { "id": 3 }
}

Implementation Logic

CSMS handleTransactionEventRequest — Pseudocode (Battery Charging Context)
function handleTransactionEventRequest(stationId, request):
    txId = request.transactionInfo.transactionId
    evseId = request.evse?.id

    switch request.eventType:

        case "Started":
            isBatterySwapCharging = isBatterySwapStation(stationId)
                && (request.idToken?.type == "NoAuthorization"
                    || isBatterySwapIdToken(stationId, request.idToken))

            database.createTransaction({
                transactionId: txId,
                stationId: stationId,
                evseId: evseId,
                startTime: request.timestamp,
                isBatterySwapCharging: isBatterySwapCharging,
                chargingState: request.transactionInfo.chargingState
            })

            // Link to slot if this is a BSS
            if isBatterySwapCharging:
                database.updateSlot(stationId, evseId, {
                    chargingTransactionId: txId
                })

            return {}  // Empty response is valid

        case "Updated":
            tx = database.findTransaction(txId)
            tx.chargingState = request.transactionInfo.chargingState
            tx.lastUpdated = request.timestamp

            // Track SoC from meter values
            if request.meterValue:
                for mv in request.meterValue:
                    for sv in mv.sampledValue:
                        if sv.measurand == "SoC":
                            tx.currentSoC = sv.value
                            database.updateSlot(stationId, evseId, {
                                batterySoC: sv.value
                            })

            // Detect suspension at max SoC
            if request.triggerReason == "EnergyLimitReached":
                tx.chargingState = "SuspendedEVSE"
                database.updateSlot(stationId, evseId, {
                    swapEligible: true
                })

            database.saveTransaction(tx)
            return {}

        case "Ended":
            tx = database.findTransaction(txId)
            tx.endTime = request.timestamp
            tx.stoppedReason = request.transactionInfo.stoppedReason
            tx.timeSpentCharging = request.transactionInfo.timeSpentCharging
            tx.status = "Completed"

            // Clear slot charging transaction link
            database.updateSlot(stationId, evseId, {
                chargingTransactionId: null
            })

            database.saveTransaction(tx)
            return {}

Requirements

S04.FR.01 — BSS initiates charging when a battery is inserted. Actual energy transfer may be delayed (e.g., until after BatterySwapResponse, or when cheap energy is available).

S04.FR.02 — If BatterySwapIdtoken is set, the Started event uses idToken.type = "Central".

S04.FR.03 — If BatterySwapIdtoken is NOT set, idToken.type = "NoAuthorization".

S04.FR.04 — BSS sends periodic Updated events with measurand = "SoC" during charging.

S04.FR.05 — At BatterySwapTargetSoc, the slot becomes eligible for swapping.

S04.FR.06 — At BatterySwapMaxSoc, charging is suspended ( chargingState = SuspendedEVSE, triggerReason = EnergyLimitReached). Transaction stays alive.

S04.FR.07 — When the battery is removed, BSS sends Ended with triggerReason = EVCommunicationLost, stoppedReason = EVDisconnected.

S04.FR.08TxStartPoint SHALL be "EVConnected" (transaction starts when battery is inserted).

S04.FR.09TxStopPoint SHALL be "EVConnected" (transaction ends when battery is removed).

S04.FR.10BatterySwapMaxSoc >= BatterySwapTargetSoc.

S04.FR.11 — On BSS reboot, the BSS starts new charging transactions for all slots that have a battery, including those already at max SoC.

S04.FR.12 — BSS reports battery SoC per slot via BatteryCartridgeSoC (queryable via GetVariablesRequest).

8. Message Schemas Reference

Reference

BatterySwapRequest Schema

Direction: BSS → CSMS

BatterySwapRequest — Complete JSON Schema
{
  "type": "object",
  "required": ["eventType", "requestId", "idToken", "batteryData"],
  "properties": {
    "eventType": {
      "type": "string",
      "enum": ["BatteryIn", "BatteryOut", "BatteryOutTimeout"]
    },
    "requestId": {
      "type": "integer",
      "description": "Correlates BatteryIn/Out events and optional RequestBatterySwapRequest"
    },
    "idToken": {
      "type": "object",
      "required": ["idToken", "type"],
      "properties": {
        "idToken":        { "type": "string", "maxLength": 255 },
        "type":           { "type": "string", "maxLength": 20 },
        "additionalInfo": {
          "type": "array",
          "items": {
            "type": "object",
            "required": ["additionalIdToken", "type"],
            "properties": {
              "additionalIdToken": { "type": "string", "maxLength": 255 },
              "type":              { "type": "string", "maxLength": 50 }
            }
          }
        }
      }
    },
    "batteryData": {
      "type": "array",
      "minItems": 1,
      "items": {
        "type": "object",
        "required": ["evseId", "serialNumber", "soC", "soH"],
        "properties": {
          "evseId":         { "type": "integer", "minimum": 0 },
          "serialNumber":   { "type": "string", "maxLength": 50 },
          "soC":            { "type": "number", "minimum": 0.0, "maximum": 100.0 },
          "soH":            { "type": "number", "minimum": 0.0, "maximum": 100.0 },
          "productionDate": { "type": "string", "format": "date-time" },
          "vendorInfo":     { "type": "string", "maxLength": 500 }
        }
      }
    }
  }
}

BatterySwapResponse Schema

Direction: CSMS → BSS

BatterySwapResponse — Complete JSON Schema
{
  "type": "object",
  "description": "Empty acknowledgement. The request cannot be rejected via standard fields.",
  "properties": {
    "customData": {
      "type": "object",
      "required": ["vendorId"],
      "properties": {
        "vendorId": { "type": "string", "maxLength": 255 }
      }
    }
  }
}

Note: The BatterySwapResponse has NO required fields. An empty JSON {} is a valid response. See Error Handling for the customData rejection extension.

RequestBatterySwapRequest Schema

Direction: CSMS → BSS

RequestBatterySwapRequest — Complete JSON Schema
{
  "type": "object",
  "required": ["requestId", "idToken"],
  "properties": {
    "requestId": {
      "type": "integer",
      "description": "Request ID to match with BatterySwapRequest"
    },
    "idToken": {
      "type": "object",
      "required": ["idToken", "type"],
      "properties": {
        "idToken":        { "type": "string", "maxLength": 255 },
        "type":           { "type": "string", "maxLength": 20 },
        "additionalInfo": {
          "type": "array",
          "items": {
            "type": "object",
            "required": ["additionalIdToken", "type"],
            "properties": {
              "additionalIdToken": { "type": "string", "maxLength": 255 },
              "type":              { "type": "string", "maxLength": 50 }
            }
          }
        }
      }
    }
  }
}

RequestBatterySwapResponse Schema

Direction: BSS → CSMS

RequestBatterySwapResponse — Complete JSON Schema
{
  "type": "object",
  "required": ["status"],
  "properties": {
    "status": {
      "type": "string",
      "enum": ["Accepted", "Rejected"]
    },
    "statusInfo": {
      "type": "object",
      "required": ["reasonCode"],
      "properties": {
        "reasonCode":     { "type": "string", "maxLength": 20 },
        "additionalInfo": { "type": "string", "maxLength": 1024 }
      }
    }
  }
}

9. Error Handling & Custom Data Extensions

Reference

Rejecting an Inserted Battery

The standard BatterySwapResponse is an empty acknowledgement with no rejection mechanism. To reject a battery, CSMS must use a customData extension.

Prerequisite: The BSS must support this extension. Check: CustomizationCtrlr.CustomImplementationEnabled["org.openchargealliance.batteryswapresponse"] = true

customData Extension Schema

BatterySwapResponse — customData Extension
{
  "customData": {
    "vendorId": "org.openchargealliance.batteryswapresponse",
    "status": "Accepted | Rejected",
    "statusInfo": {
      "reasonCode": "<reason>",
      "additionalInfo": "<human-readable explanation>"
    }
  }
}

Predefined Reason Codes for Battery Rejection

reasonCode Description
BatterySoHLow Battery state of health is too low
BatterySoC Battery state of charge issue
BatteryDamaged Battery is damaged
BatteryUnknown Battery is not recognized (not from this CPO)
BatteryType Wrong battery type
NoBatteryAvailable No batteries available for swap
Rejecting a BatteryIn
{
  "customData": {
    "vendorId": "org.openchargealliance.batteryswapresponse",
    "status": "Rejected",
    "statusInfo": {
      "reasonCode": "BatteryUnknown",
      "additionalInfo": "Not a battery of this CPO"
    }
  }
}
Accepting a BatteryIn
{
  "customData": {
    "vendorId": "org.openchargealliance.batteryswapresponse",
    "status": "Accepted"
  }
}

BatteryOutTimeout Handling

When the BSS sends eventType = BatteryOutTimeout, it means:

  • The EV driver inserted batteries (CSMS received BatteryIn) but never took the charged batteries out.
  • CSMS now has an orphan BatteryIn without a matching BatteryOut.
  • The CSMS should handle this as an incomplete swap for billing/audit purposes.

BSS Reboot During Charging

Per S04.FR.11: When a BSS reboots, it starts new charging transactions for all EVSEs that have a battery in the slot. This includes batteries that have already reached BatterySwapMaxSoc. The CSMS should:

  • Accept these new TransactionEventRequest(Started) messages.
  • For batteries already at max SoC, immediately expect a TransactionEventRequest(Updated) with triggerReason = EnergyLimitReached and chargingState = SuspendedEVSE.

10. CSMS State Management

Architecture

Recommended data models for tracking battery swap sessions, battery records, and slot status on the CSMS side.

SwapSession Model

SwapSession Data Model
SwapSession {
    id:                  UUID
    stationId:           string        // Charging Station identity
    requestId:           integer       // Correlation ID across all messages in this swap
    idToken:             IdTokenType   // EV driver's token
    initiatedBy:         "Local" | "Remote"
    remoteRequestSentAt: datetime?     // If remote (S02)

    // BatteryIn event
    batteryInTimestamp:  datetime?
    insertedBatteries:   BatteryRecord[]

    // BatteryOut event
    batteryOutTimestamp: datetime?
    removedBatteries:    BatteryRecord[]

    // Status
    status:              "Pending" | "InProgress" | "Completed" | "TimedOut" | "Rejected"

    // Billing
    swapCost:            decimal?
    billingCalculated:   boolean
}
BatteryRecord Data Model
BatteryRecord {
    evseId:          integer
    serialNumber:    string
    soC:             number
    soH:             number
    productionDate:  datetime?
    vendorInfo:      string?
}

SlotStatus Model

SlotStatus Data Model
SlotStatus {
    stationId:              string
    evseId:                 integer
    availabilityState:      "Available" | "Occupied" | "Unavailable"
    batterySerialNumber:    string?
    batterySoC:             number?
    batterySoH:             number?
    swapEligible:           boolean     // SoC >= BatterySwapTargetSoc
    chargingTransactionId:  string?
    lastUpdated:            datetime
}

End-to-End Flow Correlation

The requestId is the primary correlation key across all battery swap messages:

requestId Correlation
RequestBatterySwapRequest.requestId  ──┐
                                       ├── Same requestId
BatterySwapRequest(BatteryIn).requestId ──┤
                                       │
BatterySwapRequest(BatteryOut).requestId ─┘

Note: For locally authorized swaps (S01), there is no RequestBatterySwapRequest, but the requestId still correlates the BatteryIn and BatteryOut events.

11. Message Direction Summary

Reference

Incoming to CSMS (BSS → CSMS)

Incoming Message Handler Action Response
AuthorizeRequest Validate idToken for swap eligibility AuthorizeResponse with idTokenInfo.status
BatterySwapRequest (BatteryIn) Record inserted batteries, update slot inventory BatterySwapResponse (empty or with customData rejection)
BatterySwapRequest (BatteryOut) Record removed batteries, complete swap, trigger billing BatterySwapResponse (empty)
BatterySwapRequest (BatteryOutTimeout) Mark swap as timed out, handle orphan BatteryIn BatterySwapResponse (empty)
NotifyEventRequest (AvailabilityState) Update slot availability state NotifyEventResponse (empty)
TransactionEventRequest (Started) Create charging transaction for battery in slot TransactionEventResponse
TransactionEventRequest (Updated) Update SoC, detect charging state changes TransactionEventResponse
TransactionEventRequest (Ended) Close charging transaction TransactionEventResponse
RequestBatterySwapResponse Process acceptance/rejection of remote swap N/A (this IS the response)

Outgoing from CSMS (CSMS → BSS)

Outgoing Message When to Send Expected Response
RequestBatterySwapRequest User requests remote swap via app RequestBatterySwapResponse (Accepted/Rejected)

Quick Reference — All Enums

BatterySwapEventEnumType

BatteryIn | BatteryOut | BatteryOutTimeout

GenericStatusEnumType

Accepted | Rejected

AuthorizationStatusEnumType

Accepted | Blocked | ConcurrentTx | Expired | Invalid | NoCredit | NotAllowedTypeEVSE | NotAtThisLocation | NotAtThisTime | Unknown

TransactionEventEnumType

Started | Updated | Ended

TriggerReasonEnumType (S04)

CablePluggedIn | ChargingStateChanged | MeterValuePeriodic | EnergyLimitReached | EVCommunicationLost

ChargingStateEnumType (S04)

EVConnected | Charging | SuspendedEVSE | SuspendedEV | Idle

Battery Rejection Reason Codes

BatterySoHLow | BatterySoC | BatteryDamaged | BatteryUnknown | BatteryType | NoBatteryAvailable

stoppedReason (S04)

EVDisconnected

OCPP 2.1 Battery Swapping Flows (S01–S04) - CSMS Developer Guide. Based on OCPP 2.1 Edition 2 Specification (Part 2), Section S.