Log inTalk to us
Charger management

Network import

Available: tenant fleet API. Move an existing network from another CSMS into a project without entering it by hand: its locations, its chargers (one station each), their EVSEs and connectors. Start in the Network import group of the API reference. Planning a move? Answer the migration discovery questionnaire first: its sites and charger answers feed this import.

StepContract
Get the CSV templateGET /v1/tenants/{tenant_id}/imports/template
Dry run (the default)POST /v1/tenants/{tenant_id}/imports with text/csv, or JSON with an OCPI document or feed (201, nothing written)
ReviewGET …/imports/{import_id} (every row: create, update, skip or error, with the reason)
ApplyPOST …/imports?apply=true&dry_run_id={import_id} with an Idempotency-Key (200, with the created IDs)
Move the chargersGET …/imports/{import_id}/move-plan (JSON, or ?format=csv)
UndoPOST …/imports/{import_id}/revert with an Idempotency-Key
HistoryGET …/imports

Moving the network as part of a migration workspace? Run the same import from the migration (/v1/migrations/{migration_id}/imports): its partners can run it too, and the applied stations join the migration's chargers as imported (Import the network).

Who runs an import:

CallerDry run, apply and readsRevert
A team owner or admin of the project, signed inYesYes
Ivora staff (signed in, or a staff key with stations:write)YesYes
A project API key holding migration:importYesNo
A team migration key holding migration:import, of an active migration of this projectYesNo
Any other key, developers, operators, viewersNo (reads with stations:read)No

Migration tools and partners (ivora-migrate, the pilot proxy host, a field engineer's script) import with a key, so nobody has to stay signed in:

  • Only the team's owners and admins, and Ivora administrators, create a project key with migration:import (POST /v1/tenants/{tenant_id}/api-keys). Developers manage the project's other keys but get 403 forbidden for this scope, and no key can create a key.
  • A team migration key imports while its creator's current role in the migration holds migration:import (the client's owners and admins, and partners) over every site: a participant narrowed to some sites cannot. Its migration must be active. A team migration key made before migration keys reached these routes gets 403 migration_key_refused here: make a new one (it still imports through the migration's own routes).
  • A key applies only a dry run the project already has (dry_run_id), as everyone does: review the dry run before you apply it.
  • Only the dry run's maker applies it: the key that made it, or, for a dry run made signed in, a signed-in member of the same team (Ivora staff for their own). Anyone else gets 403 forbidden, so a partner's key never applies the client's dry run, nor the client a partner's: dry-run the import again.
  • A key makes at most 10 dry runs and applies a minute (429 rate_limited with Retry-After).
  • Every dry run, apply and revert is in the project's audit trail (network_import.dry_run, network_import.applied, network_import.reverted), with the key and the person who made it.
  • Revoking the key stops it on its next request.

Applying is project setup, so it needs a usable payment method for the usage fee like registering a station.

Important

Nothing here contacts a charger. The import creates records in Ivora; your chargers keep talking to the incumbent CSMS until you change their configuration as the move plan describes.

Limits

  • At most 5,000 EVSEs per import and an 8 MiB body. Split a larger network by region or site and import the parts one after the other.
  • At most 20,000 source rows (CSV rows, or OCPI Locations and EVSEs together), and at most 5,500 rows that cannot be read or are skipped. A file past either limit is refused as a whole (422 import_too_large).
  • A charger has at most 99 EVSEs and an EVSE at most 8 connectors.
  • RFID tokens are a separate step: import them with POST /v1/tenants/{tenant_id}/tokens/import. Tariffs are not imported: create them and assign them to the connectors afterwards.

Prepare the source

CSV

One row per connector. A location's fields repeat on each of its rows, a charger's rows share its charger_id, and an EVSE's connectors share its evse_number:

location_ref,location_name,address,city,state,postal_code,country,latitude,longitude,time_zone,charger_id,ocpp_version,evse_number,evse_id,physical_reference,connector_number,connector_type,connector_format,power_type,max_voltage_v,max_amperage_a,max_power_w,floor_level
SITE-001,Harbor Garage,1 Harbor Way,Boston,MA,02110,USA,42.351200,-71.044800,America/New_York,CP001,1.6,1,US*ABC*E1001*1,A,1,IEC62196T1,Cable,AC1Phase,240,32,7680,2
SITE-001,Harbor Garage,1 Harbor Way,Boston,MA,02110,USA,42.351200,-71.044800,America/New_York,CP001,1.6,2,US*ABC*E1001*2,B,1,IEC62196T1,Cable,AC1Phase,240,32,7680,2
SITE-002,Main Street Plaza,200 Main St,Cambridge,MA,02142,USA,42.362500,-71.086400,America/New_York,DCFC-07,2.0.1,1,,,1,IEC62196T1COMBO,Cable,DC,920,200,150000,
SITE-002,Main Street Plaza,200 Main St,Cambridge,MA,02142,USA,42.362500,-71.086400,America/New_York,DCFC-07,2.0.1,1,,,2,CHAdeMO,Cable,DC,500,125,62500,
ColumnRequiredMeaning
location_refyesYour identifier for the site; it becomes the location's external_reference, so a later import finds the same location
location_name, country, latitude, longitude, time_zoneyescountry is ISO 3166 alpha-3 (USA); time_zone is IANA (America/New_York)
address, city, state, postal_codenoThe site's address
charger_idyesThe charger's own OCPP identity (its charge point ID). See charger IDs
ocpp_versionno1.6, 2.0.1 or 2.1; the move plan uses it to pick the instructions
evse_numberyes1–99 on the charger
evse_id, physical_referencenoThe EVSE's eMI3 ID (for roaming) and the label printed on it
connector_numberno1–8 on the EVSE; rows in order when omitted
connector_typeyesAn OCPP 2.x name (IEC62196T1), an OCPI name (IEC_62196_T1_COMBO) or a common one (J1772, CCS1, CCS2, Type2, NACS). See connector types
connector_format, power_typenoSocket or Cable; AC1Phase, AC2Phase, AC2PhaseSplit, AC3Phase or DC
max_voltage_v, max_amperage_a, max_power_wnoIn V, A and W (7680, not 7.68). A DC connector rated under 1,000 W carries the warning dc_max_power_under_1000_w: the value is likely in kW
floor_levelnoThe level the charger is on

The file is UTF-8 with a header row; cells with commas or quotes are quoted as usual. Download the same template from GET …/imports/template.

OCPI 2.2.1 Locations

Send JSON with "source": "ocpi_locations" and either a document (an OCPI Locations response {"data": [...]}, a list of Location objects, or one) or a feed, the incumbent's Locations module:

{"source": "ocpi_locations",
 "feed": {"url": "https://ocpi.incumbent.example/cpo/2.2.1/locations", "token": "<the token the incumbent issued you>"},
 "charger_map": {"EVSE-UID-1": {"charger_id": "CP001", "evse_number": 1, "ocpp_version": "1.6"},
                 "EVSE-UID-2": {"charger_id": "CP001", "evse_number": 2}}}
OCPIIvora
Location id, name, address, city, postal_code, state, country, coordinates, time_zoneThe location; id becomes its external_reference
EVSE uidA charger: see charger_map below
EVSE evse_id, physical_reference, floor_level, coordinates, capabilities, parking_restrictionsThe EVSE's ID (move plan), label, and the station's metadata
Connector standard, format, power_type, max_voltage, max_amperage, max_electric_powerThe connector's type, format, power type and ratings (V, A, W)
EVSE status REMOVEDNot imported (a skip row)
Location opening_timesRead but not stored: locations do not carry opening hours yet (a warning on the row)

OCPI has no charger (charging station) object, and an EVSE's uid is rarely the charger's OCPP identity. Map each EVSE uid to its charger with charger_map: a charger ID, or {"charger_id", "evse_number", "ocpp_version"}. EVSEs mapped to the same charger become one station with several EVSEs. An EVSE without an entry becomes a charger of its own with its uid as charger_id, and its row carries the warning charger_id_from_uid: check those before applying.

Note

An OCPI import normally needs a charger_map. Many networks use eMI3-style uids such as US*ABC*E1*1, and * is not allowed in a charger ID, so without a map those EVSEs are charger_id_invalid rows. Export the uid-to-charger list from the incumbent CSMS (or its charger list) and send it as charger_map.

Read the incumbent's OCPI feed

With feed, Ivora reads the Locations module with GET requests only, following its Link: <…>; rel="next" pages:

  • The URL must be https without credentials in it. Ivora connects only to public addresses: private, loopback, link-local and cloud metadata addresses are refused (feed_refused), including after a redirect. At most 3 redirects per page and 200 pages; a next link must stay on the feed's host.
  • Each page may be up to 8 MiB and the feed 32 MiB; a page has 30 seconds and the whole feed 120 seconds.
  • token is sent as Authorization: Token <base64 of the token> (OCPI 2.2.1; "token_encoding": "plain" sends it as given). It is used for this request alone, sent only to the feed's own host, and never stored or logged. Ask the incumbent for a read-only token and revoke it after the migration.

Connector types

connector_type (CSV) and the OCPI connector standard take the OCPP 2.x and OCPI 2.2.1 names. TESLA_R and TESLA_S are stored as Tesla Roadster and Tesla (Model S) connectors. SAE J3400, the North American Charging Standard (SAE_J3400, J3400 or NACS), is the same connector as OCPI 2.2.1's TESLA_S: it is stored as TeslaS and the charger's row carries the warning connector_type_j3400_as_tesla_s. Any other unknown type is a connector_type_unknown error for that charger.

Charger IDs

Chargers keep their own OCPP identity, so you do not need to touch each one's ID.

  • Per-project charger URLs on (the default where offered): charger_id is unique within the project. The station's name is p<tenant_id>-<charger_id> and the charger connects at its project URL, which ends in /18/CP001 for charger CP001 of project 18. A charger ID has 1–40 letters, digits, _ and -, and at most 36 characters together with p<tenant_id>-.
  • Per-project charger URLs off: charger_id is a global OCPP connection name, 3–36 characters, unique across Ivora. A name another project uses is a charger_id_collision row. Once the global charger URL is closed to new registrations, a charger to create under a global name is a global_charger_url_retired row and apply answers 409 global_charger_url_retired: ask Ivora to turn per-project charger URLs on for the project, then run a new dry run. Existing chargers can still be updated or skipped.

The dry run states which applies in identity (project or global). See per-project charger URLs.

Review the dry run

curl -s -X POST "$IVORA/v1/tenants/18/imports" -H "Authorization: Bearer $KEY" \
  -H "Content-Type: text/csv" --data-binary @network.csv

The 201 response is the plan. counts sums the rows, totals the source, and each row is one location, one charger, or a source row that could not be read:

{"item": "S2", "kind": "station", "action": "error", "state": "planned", "source": "lines 4-5",
 "charger_id": "DCFC-07", "name": "p18-DCFC-07", "location_ref": "SITE-002", "evses": 1, "connectors": 2,
 "code": "connector_type_unknown", "message": "line 5: Connector type SCHUKO_X is not one Ivora stores …", "changes": [], "warnings": []}
actionMeaning
createNew in the project
updateExists (a location by its reference, a charger by its ID) and differs: changes lists the fields, such as evse 1 connector 1 max_power_w or location_id
skipExists and is identical, or an EVSE the source marks removed
errorNothing is written for it; code and message give the reason

Row codes include duplicate_in_file (the same row, connector, EVSE uid or Location twice), location_conflict and charger_location_conflict (rows that disagree), coordinates_missing, time_zone_missing, connector_type_unknown, charger_id_invalid, charger_id_collision, charger_id_claimed (a registration of that charger is in progress), station_layout_differs (an existing charger has other EVSEs or connector counts: edit it, then dry-run again), location_invalid (its location has an error) and invalid. A charger whose rows disagree is refused as a whole, so nothing half-described is created.

Rows also carry warnings that do not stop the row: charger_id_from_uid, connector_type_j3400_as_tesla_s, dc_max_power_under_1000_w, opening_times_not_stored, name_truncated, capability_ignored and evse_coordinates_ignored.

Fix the source and dry-run again until the plan is what you want. A dry run is cheap and writes nothing.

Apply

curl -s -X POST "$IVORA/v1/tenants/18/imports?apply=true&dry_run_id=imp_…" \
  -H "Authorization: Bearer $KEY" -H "Idempotency-Key: migration-2026-10-16-a"

Apply writes exactly what the dry run showed. First it plans the project again: if anything a row depends on changed since (a charger registered meanwhile, a location edited, per-project charger URLs switched), it answers 409 import_stale with the rows in details.items and writes nothing. Run a new dry run then.

Rows are written in batches; each batch of locations, and each batch of stations with their EVSEs and connectors, is stored together. The answer is the import with each row's state (done, failed or skipped), the new location_id, station_id and connector_ids, and status applied, or partial while rows remain. Each written location and station is also an operation in the project's operations list (GET /v1/tenants/{tenant_id}/operations: location.create, station.create, station.update, …), next to the station.import operation of the apply itself.

The same Idempotency-Key replays the result. If the answer is 503 outcome_unknown, or the import is partial, apply again with a new key: it resumes where it stopped, completes a station whose batch was interrupted, and never creates a charger twice. Only the dry run's maker resumes it, as for the first apply: the same key, or a signed-in member of the same team. A dry run applies once: then 409 import_applied.

Applying creates records only. Every station starts offline until its charger connects.

Move the chargers

GET …/imports/{import_id}/move-plan lists, per charger:

FieldMeaning
connection_urlWhere the charger connects in Ivora: its project URL where per-project charger URLs are on, else the global URL (closed to new registrations once frozen)
central_system_url, usernameWhat to configure on a charger that appends its own identity, and its identity (the HTTP Basic authentication username)
security_profile2: HTTP Basic authentication over TLS (wss)
credential_stepsChoose a password, store it with PUT /v1/tenants/{tenant_id}/stations/{station_id}/credentials (an offline station keeps it for its first connection), then set it on the charger
ocpp16OCPP 1.6: ChangeConfiguration of AuthorizationKey (the password) and SecurityProfile, and the central system URL in the vendor's key or tool, then a reset
ocpp201OCPP 2.0.1/2.1: SetVariables of SecurityCtrlr.BasicAuthPassword, SetNetworkProfile into a free slot with the new URL and security profile, SetVariables of OCPPCommCtrlr.NetworkConfigurationPriority, then a reset
evse_idsThe EVSE IDs the source carried

?format=csv downloads the same as CSV for a field team. A cell that starts with =, +, -, @, a tab or a carriage return gets a leading ', so a spreadsheet shows it as text rather than running it as a formula. ocpi_evse_ids lists the source's EVSE IDs of applied stations in the body PUT /v1/tenants/{tenant_id}/ocpi/evse-ids takes, so roaming partners keep seeing the same IDs once your own party is approved (OCPI).

The instructions are text: Ivora sends no configuration to any charger for you. Make the change through the incumbent CSMS or the charger's installer tool, start with a few chargers, and confirm each one shows online in its station read before moving the rest. Client certificates (security profile 3) are not supported on per-project charger URLs.

Revert

POST …/imports/{import_id}/revert (with an Idempotency-Key) deletes the stations the import created that never connected and have no charging or OCPP message history, then the locations it created that no station is at. Anything else stays, with state kept and the reason (station_connected, station_has_history, location_in_use). Each batch of stations is checked right before it is deleted, so a charger that connects while the revert runs is kept. Updates of existing locations and stations are not undone. The import's status becomes reverted, or partially_reverted when something was kept. Reverted charger IDs can be imported again. Stations a migration's import added leave that migration.