# SajiloCharge OCPI partner integration — guide for AI coding agents You are integrating a charging-station operator (CPO) backend with the SajiloCharge roaming hub (Nepal) over OCPI 2.2.1. SajiloCharge drivers start charges on the partner's hardware; the partner issues CDRs which SajiloCharge bills to driver wallets (NPR) and settles monthly. Human version of this tutorial: https://sajilocharge.com/developers/ocpi/ Contact for registration tokens: partners@sajilocharge.com ## Choose the SDK matching the project you are working in - **Django** (Django ≥ 4.2 · Python ≥ 3.10): https://sajilocharge.com/static/downloads/sajilocharge-ocpi-django.zip — human tutorial https://sajilocharge.com/developers/ocpi/django/ - **Laravel** (Laravel 10 / 11 / 12 · PHP ≥ 8.1): https://sajilocharge.com/static/downloads/sajilocharge-ocpi-laravel.zip — human tutorial https://sajilocharge.com/developers/ocpi/laravel/ - **Node.js** (Node ≥ 18 · Express ≥ 4): https://sajilocharge.com/static/downloads/sajilocharge-ocpi-node.zip — human tutorial https://sajilocharge.com/developers/ocpi/node/ - **Spring Boot** (Spring Boot 3.2+ · Java 17+): https://sajilocharge.com/static/downloads/sajilocharge-ocpi-springboot.zip — human tutorial https://sajilocharge.com/developers/ocpi/springboot/ If the project uses none of these frameworks, implement the raw protocol described in "Wire contract" below. ## Django integration (Django ≥ 4.2 · Python ≥ 3.10) ### Install the SDK One download, one package-manager command. ``` curl -LO https://sajilocharge.com/static/downloads/sajilocharge-ocpi-django.zip pip install ./sajilocharge-ocpi-django.zip ``` ### Configure and mount Tell the SDK who you are and where your server is publicly reachable. This mounts the three /ocpi endpoints the hub calls on you — everything else is outbound from your side. ``` # settings.py INSTALLED_APPS += ['sajilocharge_ocpi'] SAJILOCHARGE_OCPI = { 'PARTY_ID': 'ABC', # your 3-char party id 'COUNTRY_CODE': 'NP', 'NAME': 'ABC Charging Pvt. Ltd.', 'BASE_URL': 'https://csms.example.com', # public URL of this app 'HANDLERS': 'myapp.ocpi.Handlers', } # urls.py urlpatterns += [path('ocpi/', include('sajilocharge_ocpi.urls'))] # then: # python manage.py migrate ``` ### Wire your two charger callbacks Every remote-start request becomes a RoamingCharge handed to your code. You implement exactly two methods; keep them fast and do the charger work asynchronously. ``` # myapp/ocpi.py — the only file you write from sajilocharge_ocpi.handlers import BaseHandlers class Handlers(BaseHandlers): def on_start_session(self, charge): # charge.location_id / charge.evse_uid say which charger my_csms.remote_start(charge.evse_uid, ref=str(charge.id)) return True # accepted; do the real work async def on_stop_session(self, charge): my_csms.remote_stop(ref=str(charge.id)) return True ``` ### Drive the charge lifecycle From your CSMS events, three calls push the matching OCPI Session objects — and the final CDR that bills the driver — to the hub. The CDR is idempotent: it goes out exactly once per charge. ``` from sajilocharge_ocpi.models import RoamingCharge charge = RoamingCharge.objects.get(pk=ref) charge.set_active() # energy started flowing charge.meter(kwh=4.2) # periodic session kWh charge.complete(kwh=18.5, total_cost=462.50) # NPR; issues the CDR ``` ### Join the network Request a one-shot registration token from partners@sajilocharge.com. Deploy first — the hub calls your /ocpi endpoints back during the handshake — then run the join. Tokens rotate on every re-registration; the registration token dies on first use. ``` python manage.py sajilocharge_join --token python manage.py sajilocharge_status # → Status: ACTIVE ``` ### Publish your stations Locations upsert by id — re-push whenever anything changes, keep live EVSE status fresh, and (optionally) verify drivers in real time for RFID or local starts. ``` from sajilocharge_ocpi.client import HubClient HubClient().push_location({ 'id': 'LOC-KTM-01', 'name': 'ABC Hub Thamel', 'address': 'Thamel Marg 12', 'city': 'Kathmandu', 'coordinates': {'latitude': '27.7154', 'longitude': '85.3123'}, 'publish': True, 'evses': [{'uid': 'EVSE-01', 'status': 'AVAILABLE', 'connectors': [{'id': '1', 'standard': 'IEC_62196_T2', 'format': 'SOCKET', 'power_type': 'AC_3_PHASE'}]}], }) # live availability + optional driver check HubClient().update_evse('LOC-KTM-01', 'EVSE-01', {'status': 'CHARGING'}) HubClient().authorize(token_uid) # ALLOWED | NO_CREDIT | BLOCKED ``` ## Laravel integration (Laravel 10 / 11 / 12 · PHP ≥ 8.1) ### Install the SDK One download, one package-manager command. ``` composer config repositories.sajilocharge '{"type": "artifact", "url": "./vendor-zips"}' mkdir -p vendor-zips curl -Lo vendor-zips/sajilocharge-ocpi-laravel.zip \ https://sajilocharge.com/static/downloads/sajilocharge-ocpi-laravel.zip composer require sajilocharge/ocpi-laravel ``` ### Configure and mount Tell the SDK who you are and where your server is publicly reachable. This mounts the three /ocpi endpoints the hub calls on you — everything else is outbound from your side. ``` php artisan sajilocharge:install # config + migrations + handlers stub # .env SAJILOCHARGE_PARTY_ID=ABC # your 3-char party id SAJILOCHARGE_COUNTRY_CODE=NP SAJILOCHARGE_NAME="ABC Charging Pvt. Ltd." SAJILOCHARGE_BASE_URL=https://csms.example.com SAJILOCHARGE_HANDLERS=App\Ocpi\OcpiHandlers ``` ### Wire your two charger callbacks Every remote-start request becomes a RoamingCharge handed to your code. You implement exactly two methods; keep them fast and do the charger work asynchronously. ``` // app/Ocpi/OcpiHandlers.php — generated for you; fill two TODOs public function onStartSession(RoamingCharge $charge): bool { // $charge->location_id / $charge->evse_uid say which charger MyCsms::remoteStart($charge->evse_uid, ref: $charge->id); return true; // accepted; queue the real work } public function onStopSession(RoamingCharge $charge): bool { MyCsms::remoteStop(ref: $charge->id); return true; } ``` ### Drive the charge lifecycle From your CSMS events, three calls push the matching OCPI Session objects — and the final CDR that bills the driver — to the hub. The CDR is idempotent: it goes out exactly once per charge. ``` $charge = RoamingCharge::find($ref); $charge->setActive(); // energy started flowing $charge->meter(4.2); // periodic session kWh $charge->complete(18.5, 462.50); // NPR; issues the CDR ``` ### Join the network Request a one-shot registration token from partners@sajilocharge.com. Deploy first — the hub calls your /ocpi endpoints back during the handshake — then run the join. Tokens rotate on every re-registration; the registration token dies on first use. ``` php artisan sajilocharge:join --token= php artisan sajilocharge:status # → Status: ACTIVE ``` ### Publish your stations Locations upsert by id — re-push whenever anything changes, keep live EVSE status fresh, and (optionally) verify drivers in real time for RFID or local starts. ``` use SajiloCharge\Ocpi\Facades\SajiloCharge; SajiloCharge::pushLocation([ 'id' => 'LOC-KTM-01', 'name' => 'ABC Hub Thamel', 'address' => 'Thamel Marg 12', 'city' => 'Kathmandu', 'coordinates' => ['latitude' => '27.7154', 'longitude' => '85.3123'], 'publish' => true, 'evses' => [['uid' => 'EVSE-01', 'status' => 'AVAILABLE', 'connectors' => [['id' => '1', 'standard' => 'IEC_62196_T2', 'format' => 'SOCKET', 'power_type' => 'AC_3_PHASE']]]], ]); // live availability + optional driver check SajiloCharge::updateEvse('LOC-KTM-01', 'EVSE-01', ['status' => 'CHARGING']); SajiloCharge::authorize($tokenUid); // ALLOWED | NO_CREDIT | BLOCKED ``` ## Node.js integration (Node ≥ 18 · Express ≥ 4) ### Install the SDK One download, one package-manager command. ``` npm install https://sajilocharge.com/static/downloads/sajilocharge-ocpi-node.zip ``` ### Configure and mount Tell the SDK who you are and where your server is publicly reachable. This mounts the three /ocpi endpoints the hub calls on you — everything else is outbound from your side. ``` const express = require('express'); const { createSajiloCharge } = require('sajilocharge-ocpi'); const sc = createSajiloCharge({ partyId: 'ABC', // your 3-char party id countryCode: 'NP', name: 'ABC Charging Pvt. Ltd.', baseUrl: 'https://csms.example.com', // public URL of this app storePath: './sajilocharge-ocpi.json', handlers, // written in the next step }); const app = express(); app.use('/ocpi', sc.router); app.listen(3000); ``` ### Wire your two charger callbacks Every remote-start request becomes a RoamingCharge handed to your code. You implement exactly two methods; keep them fast and do the charger work asynchronously. ``` const handlers = { async onStartSession(charge) { // charge.locationId / charge.evseUid say which charger await myCsms.remoteStart(charge.evseUid, { ref: charge.id }); return true; // accepted; do the real work async }, async onStopSession(charge) { await myCsms.remoteStop({ ref: charge.id }); return true; }, }; ``` ### Drive the charge lifecycle From your CSMS events, three calls push the matching OCPI Session objects — and the final CDR that bills the driver — to the hub. The CDR is idempotent: it goes out exactly once per charge. ``` const charge = sc.charges.get(ref); await charge.setActive(); // energy started flowing await charge.meter(4.2); // periodic session kWh await charge.complete(18.5, 462.50); // NPR; issues the CDR ``` ### Join the network Request a one-shot registration token from partners@sajilocharge.com. Deploy first — the hub calls your /ocpi endpoints back during the handshake — then run the join. Tokens rotate on every re-registration; the registration token dies on first use. ``` # create sajilocharge.config.json with the same fields, then: npx sajilocharge-ocpi join --token npx sajilocharge-ocpi status # → Status: ACTIVE ``` ### Publish your stations Locations upsert by id — re-push whenever anything changes, keep live EVSE status fresh, and (optionally) verify drivers in real time for RFID or local starts. ``` await sc.client.pushLocation({ id: 'LOC-KTM-01', name: 'ABC Hub Thamel', address: 'Thamel Marg 12', city: 'Kathmandu', coordinates: { latitude: '27.7154', longitude: '85.3123' }, publish: true, evses: [{ uid: 'EVSE-01', status: 'AVAILABLE', connectors: [{ id: '1', standard: 'IEC_62196_T2', format: 'SOCKET', power_type: 'AC_3_PHASE' }] }], }); // live availability + optional driver check await sc.client.updateEvse('LOC-KTM-01', 'EVSE-01', { status: 'CHARGING' }); await sc.client.authorize(tokenUid); // ALLOWED | NO_CREDIT | BLOCKED ``` ## Spring Boot integration (Spring Boot 3.2+ · Java 17+) ### Install the SDK One download, one package-manager command. ``` curl -LO https://sajilocharge.com/static/downloads/sajilocharge-ocpi-springboot.zip unzip sajilocharge-ocpi-springboot.zip && cd sajilocharge-ocpi-spring mvn install # pom.xml com.sajilocharge sajilocharge-ocpi-spring-boot-starter 0.1.0 ``` ### Configure and mount Tell the SDK who you are and where your server is publicly reachable. This mounts the three /ocpi endpoints the hub calls on you — everything else is outbound from your side. ``` # application.yml sajilocharge: party-id: ABC # your 3-char party id country-code: NP name: ABC Charging Pvt. Ltd. base-url: https://csms.example.com # public URL of this app ``` ### Wire your two charger callbacks Every remote-start request becomes a RoamingCharge handed to your code. You implement exactly two methods; keep them fast and do the charger work asynchronously. ``` @Component public class MyOcpiHandlers implements SajiloChargeHandlers { @Override public boolean onStartSession(RoamingCharge charge) { // charge.getLocationId() / charge.getEvseUid() say which charger myCsms.remoteStart(charge.getEvseUid(), charge.getId()); return true; // accepted; do the real work async } @Override public boolean onStopSession(RoamingCharge charge) { myCsms.remoteStop(charge.getId()); return true; } } ``` ### Drive the charge lifecycle From your CSMS events, three calls push the matching OCPI Session objects — and the final CDR that bills the driver — to the hub. The CDR is idempotent: it goes out exactly once per charge. ``` charges.setActive(ref); // energy started flowing charges.meter(ref, 4.2); // periodic session kWh charges.complete(ref, 18.5, 462.50); // NPR; issues the CDR ``` ### Join the network Request a one-shot registration token from partners@sajilocharge.com. Deploy first — the hub calls your /ocpi endpoints back during the handshake — then run the join. Tokens rotate on every re-registration; the registration token dies on first use. ``` # application.yml — one-shot; auto-joins on startup, then remove it sajilocharge: registration-token: ``` ### Publish your stations Locations upsert by id — re-push whenever anything changes, keep live EVSE status fresh, and (optionally) verify drivers in real time for RFID or local starts. ``` hubClient.pushLocation(Map.of( "id", "LOC-KTM-01", "name", "ABC Hub Thamel", "address", "Thamel Marg 12", "city", "Kathmandu", "coordinates", Map.of("latitude", "27.7154", "longitude", "85.3123"), "publish", true, "evses", List.of(Map.of("uid", "EVSE-01", "status", "AVAILABLE", "connectors", List.of(Map.of("id", "1", "standard", "IEC_62196_T2", "format", "SOCKET", "power_type", "AC_3_PHASE")))))); // live availability + optional driver check hubClient.updateEvse("LOC-KTM-01", "EVSE-01", Map.of("status", "CHARGING")); hubClient.authorize(tokenUid); // ALLOWED | NO_CREDIT | BLOCKED ``` ## Wire contract (framework-independent) All requests/responses use the OCPI envelope: `{"data": ..., "status_code": 1000, "status_message": "Success", "timestamp": ""}`. status_code >= 2000 is an error. Auth header on every call: `Authorization: Token ` (plain token also accepted). ### Endpoints the partner must expose (hub → partner) | Method | Path | Purpose | |---|---|---| | GET | /ocpi/versions | `[{"version": "2.2.1", "url": ".../ocpi/2.2.1"}]` | | GET | /ocpi/2.2.1 | `{"version": "2.2.1", "endpoints": [credentials SENDER, commands RECEIVER]}` | | POST/PUT/DELETE | /ocpi/2.2.1/credentials | registration handshake (hub-initiated variant) | | POST | /ocpi/2.2.1/commands/START_SESSION | body: `{response_url, token: {uid, contract_id, type}, location_id, evse_uid?, connector_id?}` — respond `{"result": "ACCEPTED", "timeout": 120}` in the envelope, then POST `{"result": "ACCEPTED"}` to response_url | | POST | /ocpi/2.2.1/commands/STOP_SESSION | body: `{response_url, session_id}` — same response pattern | ### Calls the partner makes (partner → hub) Discover concrete URLs from the hub version details at https://sajilocharge.com/ocpi/versions (never hardcode them). - Registration (partner-initiated): GET hub /versions and /2.2.1 with the one-shot registration token; mint an inbound token, store its SHA-256 and START ACCEPTING IT, then POST hub credentials `{token: , url: , roles: [{role: "CPO", party_id, country_code, business_details}]}`. The hub synchronously fetches the partner's /ocpi endpoints with that token during the POST. Response data.token is the token for all future partner→hub calls. The partner's public /ocpi must be reachable at this moment. - Locations push: PUT {locations RECEIVER}/{country_code}/{party_id}/{location_id} with an OCPI Location (id, name, address, city, coordinates {latitude, longitude}, publish, evses[{uid, status, connectors[...]}]). PATCH .../{evse_uid} for live status. - Session push: PUT {sessions RECEIVER}/{country_code}/{party_id}/{session_id} with `{id, status: PENDING|ACTIVE|COMPLETED|INVALID, kwh: , cdr_token: {uid, contract_id, type}, currency, start_date_time, last_updated}`. The hub matches sessions by id, falling back to cdr_token.uid. - CDR (the bill, send exactly once when the charge ends): POST {cdrs RECEIVER} with `{id, session_id, cdr_token: {uid}, currency: "NPR", total_energy: , total_cost: {excl_vat: }, cdr_location: {id, name}, start_date_time, end_date_time}`. Duplicate CDR ids are acknowledged, not double-billed. - Real-time authorize (optional): POST {tokens SENDER}/{token_uid}/authorize → data.allowed = ALLOWED | NO_CREDIT | BLOCKED. ### Rules - Amounts are NPR; total_cost is VAT-exclusive. - The hub throttles per-party (default 300 req/min); meter pushes every 30-60 s are sufficient. - Settlement is monthly netting minus the hub clearing fee; disputes: POST {hub}/ocpi/2.2.1/cdrs/{cdr_id}/dispute with {"reason": "..."}. - Keep tokens secret; hash inbound tokens at rest. ## Verification checklist for the agent 1. GET /ocpi/versions on the partner server with the minted inbound token returns the envelope with version 2.2.1. 2. The join step reports status ACTIVE and discovered hub endpoints. 3. A pushed location appears in the SajiloCharge app (ask the human to confirm, or GET the location back from the hub). 4. A test START_SESSION from the hub creates a charge, your handler runs, and the hub receives the CommandResult callback. 5. complete() pushes a COMPLETED session and exactly one CDR.