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.
| Step | Contract |
|---|---|
| Get the CSV template | GET /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) |
| Review | GET …/imports/{import_id} (every row: create, update, skip or error, with the reason) |
| Apply | POST …/imports?apply=true&dry_run_id={import_id} with an Idempotency-Key (200, with the created IDs) |
| Move the chargers | GET …/imports/{import_id}/move-plan (JSON, or ?format=csv) |
| Undo | POST …/imports/{import_id}/revert with an Idempotency-Key |
| History | GET …/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:
| Caller | Dry run, apply and reads | Revert |
|---|---|---|
| A team owner or admin of the project, signed in | Yes | Yes |
Ivora staff (signed in, or a staff key with stations:write) | Yes | Yes |
A project API key holding migration:import | Yes | No |
A team migration key holding migration:import, of an active migration of this project | Yes | No |
| Any other key, developers, operators, viewers | No (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 get403 forbiddenfor 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 gets403 migration_key_refusedhere: 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_limitedwithRetry-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,
| Column | Required | Meaning |
|---|---|---|
location_ref | yes | Your 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_zone | yes | country is ISO 3166 alpha-3 (USA); time_zone is IANA (America/New_York) |
address, city, state, postal_code | no | The site's address |
charger_id | yes | The charger's own OCPP identity (its charge point ID). See charger IDs |
ocpp_version | no | 1.6, 2.0.1 or 2.1; the move plan uses it to pick the instructions |
evse_number | yes | 1–99 on the charger |
evse_id, physical_reference | no | The EVSE's eMI3 ID (for roaming) and the label printed on it |
connector_number | no | 1–8 on the EVSE; rows in order when omitted |
connector_type | yes | An 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_type | no | Socket or Cable; AC1Phase, AC2Phase, AC2PhaseSplit, AC3Phase or DC |
max_voltage_v, max_amperage_a, max_power_w | no | In 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_level | no | The 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}}}
| OCPI | Ivora |
|---|---|
Location id, name, address, city, postal_code, state, country, coordinates, time_zone | The location; id becomes its external_reference |
EVSE uid | A charger: see charger_map below |
EVSE evse_id, physical_reference, floor_level, coordinates, capabilities, parking_restrictions | The EVSE's ID (move plan), label, and the station's metadata |
Connector standard, format, power_type, max_voltage, max_amperage, max_electric_power | The connector's type, format, power type and ratings (V, A, W) |
EVSE status REMOVED | Not imported (a skip row) |
Location opening_times | Read 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
httpswithout 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.
tokenis sent asAuthorization: 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_idis unique within the project. The station's name isp<tenant_id>-<charger_id>and the charger connects at its project URL, which ends in/18/CP001for chargerCP001of project 18. A charger ID has 1–40 letters, digits,_and-, and at most 36 characters together withp<tenant_id>-. - Per-project charger URLs off:
charger_idis a global OCPP connection name, 3–36 characters, unique across Ivora. A name another project uses is acharger_id_collisionrow. Once the global charger URL is closed to new registrations, a charger to create under a global name is aglobal_charger_url_retiredrow and apply answers409 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": []}
action | Meaning |
|---|---|
create | New in the project |
update | Exists (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 |
skip | Exists and is identical, or an EVSE the source marks removed |
error | Nothing 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:
| Field | Meaning |
|---|---|
connection_url | Where 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, username | What to configure on a charger that appends its own identity, and its identity (the HTTP Basic authentication username) |
security_profile | 2: HTTP Basic authentication over TLS (wss) |
credential_steps | Choose 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 |
ocpp16 | OCPP 1.6: ChangeConfiguration of AuthorizationKey (the password) and SecurityProfile, and the central system URL in the vendor's key or tool, then a reset |
ocpp201 | OCPP 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_ids | The 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.



