Responses and errors

Admin API response and error contract

This file completes the response contract for every endpoint in Endpoint reference. Read the endpoint row for its success body and the tables below for every shared or endpoint-specific non-success body. By default, errors use several compatibility shapes; callers can opt into one universal envelope.

Optional structured error envelope

Send this request header on any endpoint:

X-Error-Format: envelope

The value comparison is trimmed and case-insensitive. With that exact value, every JSON response whose status is 400 or higher is converted to:

ErrorEnvelope = {
  error: {
    code: string,
    message: string,
    details: Record<string, unknown>,
    requestId: string
  }
}

Example conversion of a default validation body:

{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "Invalid request",
    "details": {
      "fields": {
        "name": ["String must contain at least 1 character(s)"]
      }
    },
    "requestId": "integration-42"
  }
}

Conversion rules are exact:

  • validationErrors becomes details.fields, and code becomes VALIDATION_FAILED even when the default body also had a domain code.
  • A default top-level code is retained when there are no validation fields.
  • Other top-level properties except error, message, validationErrors, and code move into details; for example report export limit becomes details.limit.
  • A string top-level error becomes message. If none exists, the status fallback is used.
  • Fallback codes are AUTH_REQUIRED (401), PERMISSION_DENIED (403), RESOURCE_NOT_FOUND (404), CONFLICT (409), VALIDATION_FAILED (422 and other unclassified 4xx), RATE_LIMITED (429), EXTERNAL_PROVIDER_FAILED (502), and INTERNAL_ERROR (other 5xx, including 503).
  • requestId is the exact X-Request-Id response header. A caller-supplied request ID is used only when it matches [A-Za-z0-9._:-]{1,128} after trimming; otherwise the server generates a UUID.

This transformation does not change the status or non-body headers. It also applies to idempotent replays according to the replay request's X-Error-Format header. Binary success responses remain binary; their JSON errors are enveloped. The remaining tables show the default bodies; applying the rules above gives the other possible response body for every row.

Response notation

ValidationError = {
  validationErrors: Record<string, string[]>,
  code?: string
}

CodedError = {
  error: string,
  code: string,
  // a route may add fields explicitly listed below
}

PlainError = { error: string }

error and validation-message text is descriptive, not a stable programmatic discriminator. Branch on code when present. Request validation returns 422 ValidationError; validation of a whole object can produce { "validationErrors": {} } when no individual field is identified. Invalid JSON and unexpected failures return the global 500 PlainError shown below.

All JSON examples are exact body shapes. Text shown as <message> is the returned validation or operation message and is a JSON string, not a literal angle-bracket value.

Request processing order

Requests are processed in this order:

  1. workspace-token configuration and authentication;
  2. host policy and token/workspace rate limits;
  3. current entitlement filtering;
  4. for every mutation, idempotency validation/claim;
  5. endpoint scope checks, request validation, and the requested operation.

Consequently, a mutation with a valid but under-scoped token is still required to send Idempotency-Key. A completed 403, 404, 409, or 422 mutation response is replayable for the same key/request.

Authentication, policy, and rate-limit responses

These responses can occur on every registered /v2/admin endpoint before endpoint-specific processing.

StatusWhenExact bodyRelevant headers
401Missing, duplicate, comma-joined, non-Bearer, malformed, unknown, expired, revoked, or otherwise invalid workspace token{ "error": "Unauthorized", "code": "INVALID_WORKSPACE_TOKEN" }WWW-Authenticate: Bearer, Cache-Control: no-store
404The token is valid but its workspace host does not have adminApiEnabled{ "error": "Not Found" }Cache-Control: no-store
429Per-token or per-workspace request bucket is exhausted{ "error": "Too many attempts. Please try again later." }Retry-After: <seconds>
503Workspace-token authentication is temporarily unavailable{ "error": "Admin API workspace-token configuration is unavailable", "code": "WORKSPACE_API_CONFIG_UNAVAILABLE" }
503Host-policy lookup or current entitlement authorization fails{ "error": "Workspace authorization is temporarily unavailable", "code": "WORKSPACE_AUTHORIZATION_UNAVAILABLE" }

Losing apiAccess removes all effective scopes; losing a feature entitlement removes the affected scopes. The resulting endpoint response is the scope response below, not a token refresh flow.

Scope, idempotency, parsing, ownership, and unexpected failures

Applies toStatusExact bodyHeaders/notes
Every route after authentication403{ "error": "Insufficient workspace-token scope", "code": "INSUFFICIENT_SCOPE" }Returned when any required scope is absent.
Every mutation400{ "error": "Idempotency-Key is required for this operation", "code": "IDEMPOTENCY_KEY_REQUIRED" }No key supplied.
Every mutation400{ "error": "Exactly one Idempotency-Key is required", "code": "INVALID_IDEMPOTENCY_KEY" }Duplicate or comma-joined header.
Every mutation400{ "error": "Idempotency-Key must contain 1 to 255 visible characters", "code": "INVALID_IDEMPOTENCY_KEY" }Empty, too long, or contains a control character.
Every mutation409{ "error": "Idempotency-Key was already used with a different request", "code": "IDEMPOTENCY_CONFLICT" }Same token/method/path/key, different URL or body.
Every mutation409{ "error": "Request is already processing", "code": "IDEMPOTENT_REQUEST_PROCESSING" }Retry-After: 5.
Every mutation409{ "error": "Previous request outcome is unknown; verify its effects before attempting the operation again", "code": "IDEMPOTENCY_OUTCOME_UNKNOWN" }Prior processing lease expired, connection closed, or request completed with a 5xx.
Every mutation503{ "error": "Idempotency storage unavailable", "code": "IDEMPOTENCY_UNAVAILABLE" }The API cannot check or save the idempotency record.
Any endpoint with invalid query/body/path schema422{ "validationErrors": { "<field>": ["<message>"] } }Multiple fields/messages may be present. Callers cannot supply cpoId; the workspace always comes from the token.
Any nested workspace resource miss or cross-workspace ID404{ "error": "Workspace resource not found", "code": "RESOURCE_NOT_FOUND" }Used consistently unless a table below says otherwise.
Any endpoint with an unexpected failure500{ "error": "Something went wrong" }For mutations, retrying the same key later yields 409 IDEMPOTENCY_OUTCOME_UNKNOWN.

Resource path IDs must be positive numbers. A non-positive or non-numeric :siteId, :chargerId, :portId, :tariffId, :profileId, :assignmentId, :userId, :rfidId, :accessId, :limitId, or :localUserId returns 422 ValidationError, not 400.

Site and power-management errors

EndpointsStatusExact body
Site create/update; workspace field validation422{ "validationErrors": { "<field>": ["<message>"] } }
DELETE /v2/admin/sites/:siteId when the site has dependencies409{ "error": "<message>", "code": "SITE_NOT_EMPTY" }
GET /v2/admin/sites/geocode invalid normalized search400{ "error": "<message>", "code": "INVALID_GEOCODE_QUERY" }
Same, bounded provider queue full429{ "error": "<message>", "code": "GEOCODING_BUSY" }; includes Retry-After when supplied by the geocoder
Same, provider timeout/failure or unexpected geocoder error502{ "error": "<message>", "code": "GEOCODING_UNAVAILABLE" }; an unexpected error uses Address search is temporarily unavailable
PUT /v2/admin/sites/:siteId/power-management invalid topology/domain configuration422{ "error": "<message>", "code": "INVALID_POWER_MANAGEMENT_CONFIGURATION" }
Same, OCPP refusal/timeout, race, or reconciliation conflict409{ "error": "<message>", "code": "POWER_MANAGEMENT_CONFLICT" }

Charger errors

mapChargerErrors() is used by charger creation/update, credentials, connector settings, public listing, QR payload resolution, configuration mutation/reload, firmware start, all four basic commands, and composite-schedule request.

EndpointsStatusExact body
Any operation above with a domain validation failure422{ "validationErrors": { "<field>": ["<message>"] } }
Any operation above with a state, uniqueness, offline/OCPP, billing, tagless, connector, or readiness conflict409{ "error": "<message>", "code": "CHARGER_CONFLICT" }
GET /chargers/:chargerId/sessions, invalid defaulted/explicit time range422{ "validationErrors": { "from": ["from must be before or equal to to"] } } or { "validationErrors": { "from": ["Session range cannot exceed 90 days"] } }

Plain read operations return only the shared 401/403/404/429/500/503 responses plus 422 query/path validation. Charger deletion can return the shared 500 PlainError if deletion fails without a more specific error response.

Report errors

EndpointsStatusExact body
Data/export range where from >= to422{ "validationErrors": { "range": ["from must be earlier than to"] } }
Data/export range over 90 days422{ "validationErrors": { "range": ["Report ranges cannot exceed 90 days"] } }
Data/measurements invalid opaque cursor422{ "validationErrors": { "cursor": ["<message>"] } }
GET /reports/export, result exceeds 2,000 rows422{ "error": "<message>", "code": "REPORT_EXPORT_ROW_LIMIT", "limit": 2000 }
POST /reports/send, no saved schedule409{ "error": "A workspace report schedule must be saved before sending", "code": "REPORT_SCHEDULE_REQUIRED" }
Same, mailer rejects/fails502{ "error": "Report delivery failed", "code": "REPORT_DELIVERY_FAILED" }
Schedule/measurements owned resource absent404shared RESOURCE_NOT_FOUND body

The successful export is binary; all export errors remain JSON.

Tariff errors

EndpointsStatusExact body
Create/update/components/market-price/assignment domain validation422{ "validationErrors": { "<field>": ["<message>"] } }
Tariff delete or assignment state/dependency/uniqueness conflict409{ "error": "<message>", "code": "TARIFF_CONFLICT" }
Tariff, bidding zone, charger, connector, tariff assignment, or other nested ownership miss404shared RESOURCE_NOT_FOUND body

Charging-profile errors

EndpointsStatusExact body
Create/update/schedule/assignment domain validation422{ "validationErrors": { "<field>": ["<message>"] } }
Profile is referenced by a protected usage409{ "error": "<message>", "code": "CHARGING_PROFILE_IN_USE" }
OCPP synchronization, assignment, concurrency, or other profile conflict409{ "error": "<message>", "code": "CHARGING_PROFILE_CONFLICT" }
Profile, charger, connector, assignment, or other nested ownership miss404shared RESOURCE_NOT_FOUND body

User-domain errors

User validation errors that identify a field use ValidationError plus code; other user errors use CodedError. All missing-resource cases use the shared non-enumerating RESOURCE_NOT_FOUND response.

StatusCode(s)Body
400INVALID_USER_CURSOR{ "validationErrors": { "cursor": ["The user-list cursor is invalid for the selected status"] }, "code": "INVALID_USER_CURSOR" }
409USER_ALREADY_EXISTS, CUSTOMER_CONTRACT_MISSING, INVITATION_EMAIL_IN_USE, RFID_ALREADY_EXISTS, ACCESS_ALREADY_EXISTS, ACCESS_ALL_ALREADY_EXISTS, LIMIT_ALREADY_EXISTS, LOCAL_USER_ALREADY_EXISTS, PROTECTED_WORKSPACE_USER, TAGLESS_USER_PROTECTED, USER_RELATED_DATA{ "error": "<message>", "code": "<listed code>" }
422INVALID_EMAIL, INVALID_RFID, INVALID_LIMIT_TYPE, INVALID_LIMIT_VALUE{ "validationErrors": { "<field>": ["<message>"] }, "code": "<listed code>" }
422NO_CHARGERS_AVAILABLE{ "error": "No chargers are available", "code": "NO_CHARGERS_AVAILABLE" }
429INVITATION_RESEND_THROTTLED{ "error": "Invitation resend is temporarily unavailable", "code": "INVITATION_RESEND_THROTTLED" }; includes Retry-After
503USER_CURSOR_UNAVAILABLE{ "error": "User pagination is temporarily unavailable", "code": "USER_CURSOR_UNAVAILABLE" }
404RESOURCE_NOT_FOUND{ "error": "Workspace resource not found", "code": "RESOURCE_NOT_FOUND" }

POST /users/invitations can also return the shared 403 INSUFFICIENT_SCOPE after body parsing if users:access is missing, or if an RFID was supplied and users:rfid is missing. Local-user endpoints list every required scope in Endpoint reference.

The user error codes apply as follows:

Endpoint familyNon-404 user-domain codes in addition to shared responses
GET /usersINVALID_USER_CURSOR (400), USER_CURSOR_UNAVAILABLE (503)
POST /usersUSER_ALREADY_EXISTS, TAGLESS_USER_PROTECTED (409)
PATCH /users/:userIdTAGLESS_USER_PROTECTED (409)
DELETE /users/:userIdTAGLESS_USER_PROTECTED, PROTECTED_WORKSPACE_USER, USER_RELATED_DATA (409)
User detail, vehicles, RFID/access/limit readsno non-404 domain code
RFID createCUSTOMER_CONTRACT_MISSING, RFID_ALREADY_EXISTS, TAGLESS_USER_PROTECTED (409); INVALID_RFID (422)
RFID update/deleteTAGLESS_USER_PROTECTED (409)
Access replacementTAGLESS_USER_PROTECTED (409); NO_CHARGERS_AVAILABLE (422)
Access revokeTAGLESS_USER_PROTECTED (409)
Limit create/updateTAGLESS_USER_PROTECTED, LIMIT_ALREADY_EXISTS (409); INVALID_LIMIT_TYPE, INVALID_LIMIT_VALUE (422)
Limit deleteTAGLESS_USER_PROTECTED (409)
Workspace invitation createCUSTOMER_CONTRACT_MISSING, INVITATION_EMAIL_IN_USE, RFID_ALREADY_EXISTS, TAGLESS_USER_PROTECTED (409); INVALID_EMAIL/INVALID_RFID (422); INVITATION_RESEND_THROTTLED (429) when delivery cooldown applies
Workspace invitation resendINVALID_EMAIL (422), INVITATION_RESEND_THROTTLED (429)
Workspace invitation cancelINVALID_EMAIL (422)
Charger local-user create for existing customerLOCAL_USER_ALREADY_EXISTS, TAGLESS_USER_PROTECTED (409)
Charger local-user invitationLOCAL_USER_ALREADY_EXISTS, INVITATION_EMAIL_IN_USE, TAGLESS_USER_PROTECTED (409); INVALID_EMAIL (422); INVITATION_RESEND_THROTTLED (429) when delivery cooldown applies
Local-user invitation cancelINVALID_EMAIL (422)
Local-user update/deleteTAGLESS_USER_PROTECTED (409)

All ownership and missing-resource outcomes in those families, including an unregistered email on POST /users, use the shared 404 RESOURCE_NOT_FOUND body.

No-body and replay responses

A successful 204 response has no body. This applies to every documented delete that lists 204 empty. A completed idempotent replay sets Idempotency-Replayed: true and uses the stored status/body; for 204, the HTTP response remains bodyless. The API never returns a JSON success object for a documented 204 operation.