Admin API overview

Workspace Admin API

The Workspace Admin API lets server-to-server integrations administer one EVCharge workspace. This documentation is for workspace administrators and external API integrators. It uses revocable, workspace-bound access tokens and fine-grained scopes; customer login tokens and browser sessions are not accepted.

This directory is the canonical developer reference for the /v2/admin API implemented by app-api. It does not document customer/mobile routes or the admin portal's own HTTP routes.

Base URL and resource boundary

All routes are relative to the deployed app-api origin:

https://api.evcharge.mobi/v2/admin/...

The workspace ID is not accepted in the URL, query string, or request body. Each access token is bound to exactly one workspace, and the API derives cpoId from the authenticated token principal. If a nested site, charger, user, tariff, profile, assignment, or transaction does not belong to that workspace, the API returns the same non-enumerating 404 RESOURCE_NOT_FOUND response.

Getting an access token

A human workspace administrator creates tokens in Workspace Settings under API access. Token management is intentionally not exposed to workspace tokens.

  • The administrator must have workspace-edit permission and the apiAccess entitlement.
  • The workspace's host must enable the Admin API through adminApiEnabled; otherwise a valid token receives a non-enumerating 404 from app-api.
  • The administrator can grant only scopes allowed by their own current permissions and workspace entitlements.
  • New tokens may be non-expiring (the Workspace Settings default) or use an administrator-selected whole-day lifetime from 1 to 365 days; the server computes finite absolute expiries. Non-expiring tokens remain valid until revoked.
  • A workspace can have at most 50 active tokens.
  • The plaintext credential is shown once. Store it immediately in a secret manager.
  • Name, scopes, and expiry are immutable. To rotate, create a replacement, deploy it, verify it, and revoke the old token.
  • Revocation is immediate; revoked rows remain visible as immutable, redacted workspace history, and there is no bearer-token introspection or hard-delete endpoint.

app-api has no endpoint that issues, refreshes, lists, or revokes these credentials. Obtaining the token is therefore a prerequisite performed through the human admin portal, not another endpoint in this reference.

Never put the token in source control, URLs, query strings, cookies, logs, analytics, or error reports.

Authentication

Send exactly one RFC 6750-style header on every request:

Authorization: Bearer evc_ws_<selector>.<secret>

The exact opaque credential is evc_ws_ followed by a 16-character canonical base64url selector, ., and a 43-character canonical base64url secret. Send the complete credential verbatim; do not base64-encode it again.

The API rejects duplicate authorization headers, comma-joined values, other schemes, empty bearer values, customer JWTs, and credentials supplied anywhere except this header.

Malformed, unknown, expired, revoked, or otherwise invalid tokens all receive the same response:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer
Cache-Control: no-store
Content-Type: application/json

{
  "error": "Unauthorized",
  "code": "INVALID_WORKSPACE_TOKEN"
}

The service checks token status and current workspace entitlements on every request. Removing an entitlement can therefore disable affected scopes before the token expires.

Workspace tokens are opaque credentials, not JWTs. They have no refresh token and there is no refresh endpoint. A finite token stops authenticating at its absolute expiresAt; a non-expiring token remains valid until revoked. Rotation means creating a replacement credential in the portal, deploying it to the client, and revoking the old credential.

Request conventions

  • Use Content-Type: application/json for requests with a JSON body.
  • Optional X-Request-Id request values are trimmed and accepted only when they contain 1-128 characters from A-Z, a-z, 0-9, ., _, :, or -; otherwise the server generates a UUID. Every response returns the selected value as X-Request-Id.
  • Optional X-Error-Format: envelope converts every JSON error response to the structured envelope documented in Response and error contract. Any other value preserves the default compatibility body.
  • IDs in path parameters are positive integers. Access-token IDs returned by the portal are decimal strings because they are unsigned 64-bit database values.
  • Date-time values are RFC 3339 strings with an explicit offset, for example 2026-08-13T12:30:00Z.
  • Successful deletes normally return 204 No Content.
  • Create operations normally return 201 Created.
  • OCPP work that has only been queued may return 202 Accepted; do not interpret that as confirmation from the charger unless the response explicitly says so.
  • Authenticated responses use Cache-Control: no-store. QR and report-export endpoints return binary bodies with explicit content headers.
  • Allowed browser origins can read X-Request-Id, Idempotency-Replayed, Retry-After, and Content-Disposition through CORS. This lets browser clients correlate requests, detect idempotent replays, honor retry timing, and retain server-selected download filenames.

Idempotency

All POST, PUT, PATCH, and DELETE admin requests must include one Idempotency-Key header. Keys contain 1-255 visible characters and are scoped to the token, method, and path.

Idempotency-Key: provisioning-2026-08-13-site-42

Reusing a completed key with the identical URL and body replays the stored response and adds:

Idempotency-Replayed: true

Reusing a key with a different request returns 409 IDEMPOTENCY_CONFLICT. A request still being processed returns 409 IDEMPOTENT_REQUEST_PROCESSING with Retry-After: 5.

Completed responses and terminal outcomes are retained for replay or reconciliation for seven days after their last update. After that retention period, the same key may be treated as a new command, so clients must use globally unique keys and must not intentionally recycle them.

If a processing lease expires, the connection closes after execution starts, or the operation returns a server error, the API cannot safely prove whether an external effect occurred. During the seven-day retention window, the key is terminally indeterminate and returns 409 IDEMPOTENCY_OUTCOME_UNKNOWN; it is not reclaimed or automatically executed again. Verify the operation's effects before taking manual recovery action, and do not submit the same operation under a new key until that reconciliation is complete.

All mutating admin endpoints, including POST /reports/send, require Idempotency-Key.

Rate limits

Limits are applied to both the individual token and its workspace. The committed defaults are:

LimitDefault
Window60 seconds
Per token300 requests
Per workspace1,000 requests

A limited request returns 429 Too Many Requests and a Retry-After header. Deployments can override these values.

Errors

JSON errors use a stable code where the caller needs to branch:

{
  "error": "Insufficient workspace-token scope",
  "code": "INSUFFICIENT_SCOPE"
}

Validation errors use field arrays:

{
  "validationErrors": {
    "name": ["String must contain at least 1 character(s)"]
  }
}

Common statuses are:

StatusMeaning
400Invalid idempotency/user cursor/geocoder contract; path/body/query schema failures are normally 422
401Invalid workspace token
403Valid token without the required effective scope or entitlement
404Workspace or nested resource is not visible to this token
409Resource state, dependency, concurrency, or idempotency conflict
422Field validation, bounded-range, or export-limit failure
429Token or workspace rate limit exceeded
502A downstream operation such as report delivery failed
503Authorization or idempotency storage is temporarily unavailable

Scope catalog

The following scope catalog is used when an administrator creates a token. A write or high-impact scope may imply the corresponding read scope, but read scopes never grant writes. Available scopes depend on the administrator's permissions and the workspace's enabled features.

DomainRead scopesWrite and high-impact scopesAdditional entitlement when applicable
Workspaceworkspaces:readworkspaces:update
Sitessites:readsites:create, sites:update, sites:delete, sites:power-managementdlmPowerManagement for power management
Chargerschargers:readchargers:create, chargers:update, chargers:delete, chargers:credentials, chargers:configuration, chargers:firmware, chargers:commands, chargers:public, chargers:connector-settings, chargers:local-users, chargers:tariff-assignments, chargers:profile-assignmentsfirmwareOperations, publicCharging, tariffs, or smartCharging for the matching capability
Usersusers:readusers:create, users:update, users:delete, users:invitations, users:rfid, users:access, users:limits
Charging profileschargingProfiles:readchargingProfiles:create, chargingProfiles:update, chargingProfiles:delete, chargingProfiles:assignsmartCharging
Tariffstariffs:readtariffs:create, tariffs:update, tariffs:delete, tariffs:components, tariffs:assigntariffs
Reportsreports:readreports:export, reports:schedules, reports:sendscheduledReports for schedules and send-now

Every token also requires the workspace's apiAccess entitlement.

Endpoint reference

The tables below list all 87 workspace-admin routes. Multiple scopes in a cell are all required.

Workspace

MethodPathRequired scopePurpose
GET/v2/adminworkspaces:readRead safe workspace metadata and lookups.
PATCH/v2/adminworkspaces:updateUpdate one or more workspace metadata fields.
GET/v2/admin/dashboardworkspaces:read + reports:readRead bounded workspace counts and 90-day operational aggregates.

Workspace patch fields: name, email, website, addressLine1, addressLine2, postalCode, city, state, and countryId. At least one field is required. Empty nullable strings normalize to null.

Sites and power management

MethodPathRequired scopePurpose
GET/v2/admin/sitessites:readList sites with bounded 90-day summaries and site lookups.
GET/v2/admin/sites/geocodesites:readSearch normalized site-address candidates through the shared throttled and cached geocoder.
POST/v2/admin/sitessites:createCreate a site.
GET/v2/admin/sites/:siteIdsites:read + reports:readRead site details, recent sessions, totals, and operational aggregates.
PATCH/v2/admin/sites/:siteIdsites:updateReplace the editable site document.
DELETE/v2/admin/sites/:siteIdsites:deleteDelete an empty site; dependencies return a conflict.
GET/v2/admin/sites/:siteId/power-managementsites:power-managementRead the site's dynamic load-management tree.
PUT/v2/admin/sites/:siteId/power-managementsites:power-managementAtomically replace the site's power-management configuration.

Site create and patch bodies use:

{
  "name": "Main depot",
  "description": null,
  "addressLine1": "1 Example Street",
  "addressLine2": null,
  "postalCode": "LV-1001",
  "city": "Riga",
  "state": null,
  "countryId": 1,
  "timezoneId": 1,
  "latitude": 56.9496,
  "longitude": 24.1052
}

Site geocoding accepts optional street, city, state, country, and two-letter countryCode query fields plus the Accept-Language request header. The combined street/city/state search must contain at least two characters. Responses contain only placeId, displayName, latitude, and longitude; provider credentials and provider-specific metadata are not exposed. The server applies the shared one-request-per-second provider queue, eight-second timeout, bounded 24-hour cache, and global workspace-token rate limits.

Power management accepts { "pools": [...] }. A pool has id, name, limit, optional meterSources, and recursive children. A charger child has chargepointId and priority. The API caps the tree and meter counts and performs two-phase OCPP clear/revalidation when replacing installed state. A refused, timed-out, or raced change returns 409 POWER_MANAGEMENT_CONFLICT while preserving the previous configuration.

Chargers

MethodPathRequired scopePurpose or input
GET/v2/admin/chargerschargers:readList chargers with status and bounded energy/cost summaries.
POST/v2/admin/chargerschargers:createCreate from name, identifier, optional password, and siteId.
GET/v2/admin/chargers/:chargerIdchargers:readRead charger, EVSE, connector, lookup, and status data.
PATCH/v2/admin/chargers/:chargerIdchargers:updateUpdate name, siteId, invertedRfid, smartMode, or automationDisabled.
DELETE/v2/admin/chargers/:chargerIdchargers:deleteDelete a charger subject to dependency and billing rules.
PATCH/v2/admin/chargers/:chargerId/credentialschargers:credentialsReplace the charger password with { "password": "..." }.
PATCH/v2/admin/chargers/:chargerId/connectors/:portIdchargers:connector-settingsUpdate connector format/type, power type, voltage, amperage, and power limits.
PATCH/v2/admin/chargers/:chargerId/public-listingchargers:publicSet `{ "isPublic": true
GET/v2/admin/chargers/:chargerId/public-readinesschargers:readRead public-charging readiness and missing requirements.
GET/v2/admin/chargers/:chargerId/logschargers:readRead cursor-paginated, redacted OCPP logs.
GET/v2/admin/chargers/:chargerId/sessionschargers:read + reports:readRead paginated charging sessions and totals.
GET/v2/admin/chargers/:chargerId/qrchargers:readRender a 300-pixel PNG QR; optional connector=0..255.
GET/v2/admin/chargers/:chargerId/composite-scheduleschargers:readRead persisted composite-schedule history.
GET/v2/admin/chargers/:chargerId/configurationchargers:readRead OCPP configuration with sensitive values redacted.
PATCH/v2/admin/chargers/:chargerId/configurationchargers:configurationSend a configuration change with key and value.
POST/v2/admin/chargers/:chargerId/configuration/reloadchargers:configurationRequest a fresh charger configuration.
GET/v2/admin/chargers/:chargerId/firmwarechargers:readRead current firmware and update status.
POST/v2/admin/chargers/:chargerId/firmwarechargers:firmwareQueue an update from an HTTP(S) { "url": "..." }.
POST/v2/admin/chargers/:chargerId/commands/resetchargers:commandsSend `{ "resetType": "Soft"
POST/v2/admin/chargers/:chargerId/commands/availabilitychargers:commandsSend `{ "availabilityType": "Operative"
POST/v2/admin/chargers/:chargerId/commands/triggerchargers:commandsTrigger BootNotification, Heartbeat, StatusNotification, or MeterValues.
POST/v2/admin/chargers/:chargerId/commands/unlockchargers:commandsUnlock a positive connectorId.
POST/v2/admin/chargers/:chargerId/commands/composite-schedulechargers:commandsRequest a composite schedule for connectorId, durationSeconds, and chargingRateUnit.

Charger log query parameters are cursor, limit (default 50, maximum 200), from, to, action, direction, and levelId. Session parameters are page, limit (default 25, maximum 100), from, to, and customerId (a positive ID or unknown). Composite-schedule history uses cursor, limit (default 20, maximum 50), and optional connectorId.

Reports

MethodPathRequired scopePurpose
GET/v2/admin/reports/datareports:readRead filtered, cursor-paginated charging-session report data.
GET/v2/admin/reports/measurementsreports:readRead bounded meter values for an owned transaction.
GET/v2/admin/reports/exportreports:exportDownload a CSV or PDF export.
GET/v2/admin/reports/schedulereports:readRead the workspace-owned export schedule, or null.
PUT/v2/admin/reports/schedulereports:schedulesCreate or replace the workspace-owned export schedule.
POST/v2/admin/reports/sendreports:sendEmail the previous UTC calendar month's saved export; idempotency key required.

Report data and export filters are from, to, siteId, chargerId, userId, anonymous, and active. from/to default to the latest 90-day window and may not span more than 90 days. userId cannot be combined with anonymous=true.

  • Data pagination: opaque cursor, default limit=50, maximum 100.
  • Measurements: required transactionLogId, opaque cursor, default limit=500, maximum 1,000.
  • Export: format=csv|pdf, default csv, hard limit 2,000 rows.
  • Export responses use Content-Disposition: attachment, Cache-Control: private, no-store, and X-Content-Type-Options: nosniff.

Schedule body:

{
  "enabled": true,
  "recipientEmail": "reports@example.com",
  "format": "csv",
  "sendDay": 5,
  "sendTime": "08:30"
}

sendDay is 1-28 and sendTime is interpreted in UTC. The scheduler catches up after a missed scheduled minute during the current delivery month. Failed pre-handoff builds retry after 5 and 10 minutes, an interrupted building claim is reclaimable after 30 minutes, and automatic delivery stops after three attempts. The scheduler persists indeterminate immediately before SMTP handoff; both indeterminate and the legacy sending status are terminal because automatically retrying either could send a duplicate attachment. Operators must reconcile those outcomes manually before changing the schedule state. Each tick selects at most ten schedules, stops starting work after a 45-second budget, and uses bounded SMTP timeouts. The job is disabled by default and must be explicitly enabled after SMTP and capacity validation. The API schedule is owned by the workspace rather than borrowing the identity of the human who created the token.

Tariffs

MethodPathRequired scopePurpose
GET/v2/admin/tariffstariffs:readList tariffs and lookups.
POST/v2/admin/tariffstariffs:createCreate a tariff.
GET/v2/admin/tariffs/:tariffIdtariffs:readRead one owned tariff and its components.
PATCH/v2/admin/tariffs/:tariffIdtariffs:updateUpdate the tariff definition.
PUT/v2/admin/tariffs/:tariffId/componentstariffs:componentsAtomically replace up to 256 components.
DELETE/v2/admin/tariffs/:tariffIdtariffs:deleteDelete an unused tariff.
GET/v2/admin/tariffs/:tariffId/market-pricestariffs:readRead prices for the tariff's owned bidding zone from numeric startsAt.
GET/v2/admin/chargers/:chargerId/tariff-assignmentschargers:tariff-assignments + tariffs:readList charger tariff assignments.
POST/v2/admin/chargers/:chargerId/tariff-assignmentschargers:tariff-assignments + tariffs:assignAssign a tariff.
PATCH/v2/admin/chargers/:chargerId/tariff-assignments/:assignmentIdchargers:tariff-assignments + tariffs:assignReplace an assignment target.
DELETE/v2/admin/chargers/:chargerId/tariff-assignments/:assignmentIdchargers:tariff-assignments + tariffs:assignRemove an assignment.

A tariff body contains name, exactly one of currencyId, currencyCode, or currency, optional biddingZoneId, and tax. An assignment body is:

{ "tariffId": 17, "connectorId": 0 }

For assignment creation, an omitted connectorId or 0 applies the tariff to every physical connector. Assignment updates target one persisted assignment and therefore require an explicit positive physical connectorId; 0 is rejected.

A component contains typeId, seasonId, optional priority, price, and optional month, weekday, and hour validity bounds.

Charging profiles

MethodPathRequired scopePurpose
GET/v2/admin/charging-profileschargingProfiles:readList profiles and owned lookups.
POST/v2/admin/charging-profileschargingProfiles:createCreate a profile with an optional schedule.
GET/v2/admin/charging-profiles/:profileIdchargingProfiles:readRead one owned profile.
PATCH/v2/admin/charging-profiles/:profileIdchargingProfiles:updateReplace profile metadata.
PUT/v2/admin/charging-profiles/:profileId/schedulechargingProfiles:updateReplace the schedule with 1-256 periods.
DELETE/v2/admin/charging-profiles/:profileIdchargingProfiles:deleteClear installed assignments and delete only after accepted OCPP results and revalidation.
GET/v2/admin/chargers/:chargerId/profile-assignmentschargers:profile-assignments + chargingProfiles:readList charger profile assignments.
POST/v2/admin/chargers/:chargerId/profile-assignmentschargers:profile-assignments + chargingProfiles:assignAssign and synchronize a profile.
PATCH/v2/admin/chargers/:chargerId/profile-assignments/:assignmentIdchargers:profile-assignments + chargingProfiles:assignReplace and synchronize an assignment.
DELETE/v2/admin/chargers/:chargerId/profile-assignments/:assignmentIdchargers:profile-assignments + chargingProfiles:assignClear and remove an assignment.

Profile metadata fields are label, locationId, stackLevel, purposeId, kindId, optional recurrencyKindId, validFrom, validTo, startSchedule, rateUnitId, and optional minChargingRate. A schedule period contains startPeriod, limit, and optional numberOfPhases. An assignment body contains profileId and connectorId (default 0).

Profile deletes and power-management replacement may wait for charger acknowledgement. A rejection, timeout, or concurrent state change returns a conflict and retains the database configuration rather than pretending the charger was cleared.

Users, invitations, RFID, access, and limits

MethodPathRequired scopePurpose
GET/v2/admin/usersusers:readList members and invitations.
POST/v2/admin/usersusers:createAttach an already registered customer by email and workspace name.
POST/v2/admin/users/invitationsusers:invitations + users:access; also users:rfid when RFID is suppliedInvite an unknown email or attach an existing customer.
POST/v2/admin/users/invitations/resendusers:invitationsResend using { "email": "..." }, subject to recipient cooldown.
DELETE/v2/admin/users/invitationsusers:invitationsCancel an invitation; email stays in the JSON body.
GET/v2/admin/users/:userIdusers:read + reports:readRead user details and bounded session totals.
PATCH/v2/admin/users/:userIdusers:updateSet the workspace-specific display name.
DELETE/v2/admin/users/:userIdusers:deleteRemove the member and related workspace access subject to conflicts.
GET/v2/admin/users/:userId/vehiclesusers:readList owned vehicles.
GET/v2/admin/users/:userId/rfidsusers:readList RFID metadata with masked UIDs.
POST/v2/admin/users/:userId/rfidsusers:rfidAdd uid and optional name; responses expose only the masked UID.
PATCH/v2/admin/users/:userId/rfids/:rfidIdusers:rfidUpdate the RFID label.
DELETE/v2/admin/users/:userId/rfids/:rfidIdusers:rfidDelete an RFID and synchronize affected chargers.
GET/v2/admin/users/:userId/accessusers:readRead charger-access grants.
PUT/v2/admin/users/:userId/accessusers:accessReplace access using allChargers and unique chargerIds.
DELETE/v2/admin/users/:userId/access/:accessIdusers:accessRevoke one access row.
GET/v2/admin/users/:userId/limitsusers:readList charging limits.
POST/v2/admin/users/:userId/limitsusers:limitsCreate limitTypeId, energyValue, and/or timeValue.
PATCH/v2/admin/users/:userId/limits/:limitIdusers:limitsUpdate one or more limit fields.
DELETE/v2/admin/users/:userId/limits/:limitIdusers:limitsDelete a limit.
GET/v2/admin/chargers/:chargerId/local-userschargers:read + users:readList charger-local users and pending invitations.
POST/v2/admin/chargers/:chargerId/local-userschargers:local-users + users:accessAdd an existing customerId with optional expiresAt.
POST/v2/admin/chargers/:chargerId/local-users/invitationschargers:local-users + users:invitations + users:accessAdd or invite by email.
DELETE/v2/admin/chargers/:chargerId/local-users/invitationschargers:local-users + users:invitationsCancel a local-user invitation by email.
PATCH/v2/admin/chargers/:chargerId/local-users/:localUserIdchargers:local-users + users:accessSet nullable expiresAt.
DELETE/v2/admin/chargers/:chargerId/local-users/:localUserIdchargers:local-users + users:accessRemove local access and synchronize OCPP state.

Vehicle pagination and status=members user pagination use a positive numeric cursor. Invitation pages use an opaque i2.* cursor, while status=all uses an opaque a2.* composite cursor after the first page (a numeric cursor remains valid only as a legacy member-stream continuation). User search is supplied in the X-Admin-User-Search request header so email addresses are not placed in URLs or access logs. limit defaults to 50 and is capped at 100; user listing supports status=all|members|invitations. Treat all returned cursors as opaque and return them unchanged.

Direct user creation does not create a password, login identity, or session. It only attaches an already registered customer. Use the invitation endpoint for an unknown email. The protected Tagless system user cannot be modified through these routes.

Examples

Set values in your shell without printing the token:

export ADMIN_API_BASE_URL="https://api.example.com"
export WORKSPACE_TOKEN="<read from secret manager>"

List sites:

curl --fail-with-body \
  -H "Authorization: Bearer ${WORKSPACE_TOKEN}" \
  "${ADMIN_API_BASE_URL}/v2/admin/sites"

Create a site safely:

curl --fail-with-body \
  -X POST \
  -H "Authorization: Bearer ${WORKSPACE_TOKEN}" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: site-main-depot-v1" \
  --data '{
    "name":"Main depot",
    "description":null,
    "addressLine1":"1 Example Street",
    "addressLine2":null,
    "postalCode":"LV-1001",
    "city":"Riga",
    "state":null,
    "countryId":1,
    "timezoneId":1,
    "latitude":56.9496,
    "longitude":24.1052
  }' \
  "${ADMIN_API_BASE_URL}/v2/admin/sites"

Download a CSV report for an explicit UTC range:

curl --fail-with-body \
  -H "Authorization: Bearer ${WORKSPACE_TOKEN}" \
  -o report.csv \
  "${ADMIN_API_BASE_URL}/v2/admin/reports/export?format=csv&from=2026-07-01T00%3A00%3A00Z&to=2026-08-01T00%3A00%3A00Z"

Deliberate exclusions

The workspace-token API does not expose operations that require a human principal or broader authority. These include:

  • creating, revealing, rotating, or revoking tokens with another token;
  • login, password reset, sessions, human role or privilege delegation, and workspace ownership changes;
  • workspace creation/deletion and billing-account membership;
  • payment settings, bank data, Stripe onboarding, plans, invoices, and host billing;
  • host-wide support operations and provider webhooks;
  • human notifications, navigation redirects, branding assets, and development/test routes.