Endpoints

Solicitudes y respuestas de endpoints de Admin API del espacio de trabajo

Esta es la referencia de solicitudes y respuestas de cada endpoint /v2/admin. La autenticación, idempotencia, límites de solicitudes, creación de tokens y semántica de ámbitos se documentan en Workspace Admin API. Los tipos reutilizables de campos y los ejemplos están en Esquemas y ejemplos. Lee cada fila de endpoint junto con el Contrato de respuestas y errores, que enumera todos los posibles cuerpos compartidos y específicos de respuestas no satisfactorias.

Contrato común

Cada ruta comienza con el origen de app-api desplegado. El espacio de trabajo se obtiene exclusivamente del token autenticado; los clientes no deben enviar cpoId. Todos los valores de recurso :...Id documentados en la ruta son enteros decimales positivos. Cada solicitud requiere exactamente una:

Authorization: Bearer <workspace-access-token>

Las solicitudes mostradas con cuerpo JSON requieren Content-Type: application/json. Todas las modificaciones requieren Idempotency-Key (POST, PUT, PATCH y DELETE), incluida POST /v2/admin/reports/send; salvo que una fila indique lo contrario, el tipo de contenido correcto es application/json, las fechas JSON son cadenas RFC 3339 y los números son números JSON.

Cada solicitud puede incluir X-Request-Id y X-Error-Format: envelope; consulta el Contrato de respuestas y errores. Estas cabeceras no cambian los esquemas de respuesta correcta.

Notación utilizada a continuación:

  • none significa que no hay cuerpo de solicitud. Un DELETE que documenta un cuerpo JSON sigue requiriendo ese cuerpo.
  • ? marca una propiedad o parámetro de consulta opcional.
  • T | null significa que la propiedad está presente y admite null.
  • 204 empty significa que no hay cuerpo de respuesta.
  • CursorPage<T> significa { items: T[], nextCursor: number|string|null }, salvo que la fila nombre explícitamente un objeto page o pagination.
  • Todos los casos de recursos anidados no encontrados, incluidos los ID de otros espacios de trabajo, utilizan 404 { "error": "Workspace resource not found", "code": "RESOURCE_NOT_FOUND" }, salvo que una función documente un conflicto más específico.

Esquemas de solicitud compartidos

Entradas de espacios de trabajo y emplazamientos

WorkspacePatch = {
  name?: string(1..100), email?: email|null, website?: string|null,
  addressLine1?: string|null, addressLine2?: string|null,
  postalCode?: string|null, city?: string|null, state?: string|null,
  countryId?: positive integer
} // at least one property

SiteInput = {
  name: string(1..255), description?: string|null = null,
  addressLine1?: string|null = null, addressLine2?: string|null = null,
  postalCode?: string|null = null, city?: string|null = null, state?: string|null = null,
  countryId: positive integer, timezoneId: positive integer,
  latitude?: number[-90,90]|null = null, longitude?: number[-180,180]|null = null
}

Entrada de gestión de potencia

PowerConfigurationInput = { pools: PowerPoolInput[0..200] }
PowerPoolInput = {
  id?: positive integer|null, name?: string(0..255),
  limit: integer[0,100000000], meterSources?: ExternalMeterInput[0..16],
  children?: (PowerPoolInput | ChargerPoolChild)[0..1000]
}
ChargerPoolChild = { chargepointId: positive integer, priority?: integer[-1000000,1000000] }
ExternalMeterInput = {
  id?: positive integer|null, sourceId?: positive integer|null,
  provider?: "perific", name?: string(0..255), externalId: string(1..80),
  measurementMode?: "available_headroom"|"external_non_ev_load_only"|"total_site_load_including_ev",
  valueMode?: "perific_relative"|"perific_direct"|"perific_meter",
  reserveA?: integer[0,100000], offlineSafeA?: integer[0,100000],
  ttlSeconds?: integer[30,86400], enabled?: boolean
}

Entradas de cargadores

ChargerCreate = { name: string(1..40), identifier: string(1..40), password?: string(0..40) = "", siteId: positive integer }
ChargerPatch = {
  name?: string(1..40), siteId?: positive integer, invertedRfid?: boolean,
  smartMode?: integer[0,3], automationDisabled?: boolean
} // at least one
ConnectorPatch = {
  connectorFormatId: positive integer, connectorTypeId: positive integer,
  powerTypeId: positive integer, maxVoltage: integer[1,10000],
  maxAmperage: integer[1,10000], maxElectricPower: integer[1,100000000]
}

Entradas de informes

ReportFilters = {
  from?: RFC3339, to?: RFC3339, siteId?: positive integer,
  chargerId?: positive integer, userId?: positive integer,
  anonymous?: true|false|1|0, active?: true|false|1|0
}
ReportScheduleInput = {
  enabled: boolean, recipientEmail: email(max 255; may be empty only when disabled), format: "csv"|"pdf",
  sendDay: integer[1,28], sendTime: UTC "HH:mm"
}

Los intervalos de informes deben cumplir from < to y no superar 90 días. Si se omiten, se usan los últimos 90 días. userId y anonymous=true se excluyen mutuamente. Las exportaciones programadas recuperan una ejecución perdida tras el minuto UTC previsto y utilizan reintentos limitados: espera de 5/10 minutos, concesión de 30 minutos para reservas obsoletas y tres intentos.

Entradas de tarifas

TariffInput = {
  name: string(1..64),
  exactly one of currencyId: positive integer, currencyCode: 3 letters, currency: 3 letters,
  biddingZoneId?: positive integer|null, tax?: finite number
}
TariffComponentInput = {
  typeId: positive integer, seasonId: positive integer, priority?: finite number,
  price: finite number, validFromMonth?: finite number, validToMonth?: finite number,
  validFromWeekDay?: finite number, validToWeekDay?: finite number,
  validFromHour?: finite number, validToHour?: finite number
}
TariffAssignmentCreateInput = { tariffId: positive integer, connectorId?: nonnegative integer }
TariffAssignmentUpdateInput = { tariffId: positive integer, connectorId: positive integer }

Entradas de perfiles de carga

ChargingProfileInput = {
  label: string(1..40), locationId: positive integer, stackLevel: nonnegative number,
  purposeId: positive integer, kindId: positive integer,
  recurrencyKindId?: positive integer|null, validFrom?: RFC3339|null,
  validTo?: RFC3339|null, startSchedule?: RFC3339|null,
  rateUnitId: positive integer, minChargingRate?: nonnegative number|null
}
ChargingProfileCreate = ChargingProfileInput & { schedule?: SchedulePeriod[0..256] }
SchedulePeriod = { startPeriod: nonnegative number, limit: nonnegative number, numberOfPhases?: positive integer|null }
ChargingScheduleInput = { rateUnitId: positive integer, periods: SchedulePeriod[1..256] }
ProfileAssignmentInput = { profileId: positive integer, connectorId?: nonnegative integer }

Entradas de usuarios

UserCreate = { email: email(max 255), name: string(1..255) }
InvitationInput = {
  email: email, name?: string|null, rfid?: string|null,
  allChargers?: boolean, chargerIds?: unique positive integer[0..500]
} // chargerIds must be non-empty unless allChargers=true
AccessInput = { allChargers?: boolean, chargerIds?: unique positive integer[0..500] }
LimitCreate = { limitTypeId: positive integer, energyValue?: nonnegative integer = 0, timeValue?: nonnegative integer = 0 }
LimitPatch = { limitTypeId?: positive integer, energyValue?: nonnegative integer, timeValue?: nonnegative integer }

Al menos uno de energyValue o timeValue debe ser mayor que cero al crear un límite. Se eliminan los espacios iniciales y finales de los correos y se convierten a minúsculas. Los UID RFID solo se aceptan al escribir y se enmascaran en las respuestas de lectura.

Resumen de DTO de respuesta

Las tablas de endpoints muestran todas las envolturas de respuesta de nivel superior. Estos son los principales DTO reutilizables dentro de ellas:

DTOCampos
Workspaceid, name, email, website, address { line1, line2, postalCode, city, state, countryId, countryCode, countryName }, createdAt, updatedAt
Siteid, name, description, address, timezone { id, name }, latitude, longitude, chargerCount; el detalle añade chargers normalizados y estado de conectores
GeocodeCandidateplaceId, displayName, latitude, longitude; nunca se devuelven credenciales ni metadatos específicos del proveedor
MetricsWindowfrom, to, límites/recuentos de sesiones y totalsCapped cuando corresponda
Chargerid, name, identifier, cpoId, site, status normalizado, isPublic, indicadores RFID/inteligente/automatización, provisioned, connection, evses, connectors, createdAt, updatedAt; el detalle añade configuration con datos sensibles ocultos, firmware y recuentos
ConnectorportId, connectorId, ID de referencia de conectores, maxVoltage, maxAmperage, maxElectricPower
ReportSessionid, cargador/emplazamiento, ID de puerto y conector, datos mínimos del usuario, active, energyWh, inicio/fin/duración y costes netos/impuestos/totales por moneda
ReportScheduleid, workspaceId, enabled, recipientEmail, format, sendDay/sendTime en UTC, metadatos de creación/actualización y de última entrega seguros, incluido el número de intentos
Tariffid, cpoId, name, objetos de tipo/emplazamiento/moneda/zona de oferta, tax, fallbackPrice, price efectivo, components, assignmentCount
ChargingProfileid, cpoId, label, description, stackLevel, objetos de ubicación/propósito/tipo/recurrencia/unidad de tasa, validez, programación, assignmentCount, marcas de tiempo
Userid, membershipId, name, email, rfids enmascarados, access del cargador, limitCount; el detalle añade phone, createdAt y limits. Nunca se devuelven credenciales de autorización OCPP como contractId.
RFIDid, maskedUid, name, type, valid; nunca se devuelve el UID sin enmascarar
AccessallChargers, chargerIds ordenados y items con metadatos de permiso/cargador/emplazamiento/caducidad
Limitid, limitTypeId, label, energyValue, timeValue
LocalUserDTO de lista: id, customerId, name, email, expiresAt, systemManaged. Las modificaciones de usuarios locales utilizan las estructuras diferenciadas LocalUserMutation/LocalUserUpdate de Esquemas y ejemplos.

Los objetos de referencia se devuelven en las rutas de lista y detalle para que los clientes descubran los ID válidos. Considera los nuevos campos desconocidos compatibles con versiones futuras y basa la lógica solo en los campos estables y códigos de error documentados.

Endpoints del espacio de trabajo

Método y rutaÁmbitoSolicitudRespuesta correcta
GET /v2/adminworkspaces:readCuerpo: ninguno200 { workspace: Workspace }
PATCH /v2/adminworkspaces:updateCuerpo: WorkspacePatch200 { workspace: Workspace }
GET /v2/admin/dashboardworkspaces:read + reports:readCuerpo/consulta: ninguno200 { window, stats { chargerCount, onlineChargerCount, siteCount, userCount }, sessions: DashboardSession[], analytics }; las sesiones están limitadas a 1.000 durante 90 días

Endpoints de emplazamientos y gestión de potencia

Método y rutaÁmbitoSolicitudRespuesta correcta
GET /v2/admin/sitessites:readCuerpo/consulta: ninguno200 { items: Site[], metricsWindow: MetricsWindow, lookups { countries, timezones } }
GET /v2/admin/sites/geocodesites:readConsulta: street opcional (máx. 160), city, state, country (máx. 100 cada uno), countryCode (máx. 2); cabecera Accept-Language opcional; se requieren al menos dos caracteres combinados de calle/ciudad/estado200 { results: GeocodeCandidate[] }; las búsquedas idénticas en curso se agrupan y el trabajo del proveedor utiliza una cola serializada acotada y una caché de servidor acotada; las búsquedas no válidas devuelven 400 INVALID_GEOCODE_QUERY, la cola saturada devuelve 429 GEOCODING_BUSY con Retry-After y los fallos del proveedor devuelven 502 GEOCODING_UNAVAILABLE
POST /v2/admin/sitessites:createCuerpo: SiteInput201 { site: Site }
GET /v2/admin/sites/:siteIdsites:read + reports:readCuerpo/consulta: ninguno200 { site: SiteDetail, recentSessions: SiteSession[], totals, metricsWindow }
PATCH /v2/admin/sites/:siteIdsites:updateCuerpo: SiteInput completo200 { site: Site }
DELETE /v2/admin/sites/:siteIdsites:deleteCuerpo: ninguno204 empty; un emplazamiento que no esté vacío devuelve 409 SITE_NOT_EMPTY
GET /v2/admin/sites/:siteId/power-managementsites:power-managementCuerpo/consulta: ninguno200 { configuration: PowerConfiguration } con el emplazamiento propio, cargadores/conectores, grupos, fuentes de contador, asignaciones y estado de compatibilidad
PUT /v2/admin/sites/:siteId/power-managementsites:power-managementCuerpo: PowerConfigurationInput200 { configuration: PowerConfiguration }; un rechazo OCPP, un tiempo de espera agotado o un cambio concurrente devuelve 409 POWER_MANAGEMENT_CONFLICT sin aplicar la sustitución

Endpoints de cargadores

Método y rutaÁmbitoSolicitudRespuesta correcta
GET /v2/admin/chargerschargers:readCuerpo/consulta: ninguno200 { items: Charger[], metricsWindow: MetricsWindow, connectorLookups { formats, types, powerTypes } }
POST /v2/admin/chargerschargers:createCuerpo: ChargerCreate; el identificador se convierte a mayúsculas201 { charger: Charger }; nunca se devuelve la contraseña
GET /v2/admin/chargers/:chargerIdchargers:readCuerpo/consulta: ninguno200 { charger: ChargerDetail }
PATCH /v2/admin/chargers/:chargerIdchargers:updateCuerpo: ChargerPatch200 { charger: ChargerDetail }
DELETE /v2/admin/chargers/:chargerIdchargers:deleteCuerpo: ninguno204 empty
PATCH /v2/admin/chargers/:chargerId/credentialschargers:credentialsCuerpo: { password: string(0..40) }200 { success: true }
PATCH /v2/admin/chargers/:chargerId/connectors/:portIdchargers:connector-settingsCuerpo: ConnectorPatch200 { connector: Connector }
PATCH /v2/admin/chargers/:chargerId/public-listingchargers:publicCuerpo: { isPublic: boolean }200 { isPublic: boolean }; si no se cumplen los requisitos de preparación, devuelve 409 CHARGER_CONFLICT
GET /v2/admin/chargers/:chargerId/public-readinesschargers:readCuerpo/consulta: ninguno200 { readiness: { ready, missingRequirements, connectorIds, missingTariffConnectorIds, ...commercialState } }
GET /v2/admin/chargers/:chargerId/logschargers:readConsulta: cursor?, limit?=50 (1..200), from?, to?, action?, direction?, levelId?200 { items: RedactedOcppLog[], nextCursor: number or null }
GET /v2/admin/chargers/:chargerId/sessionschargers:read + reports:readConsulta: page?=1, limit?=25 (1..100), from?, to?, customerId?: positive integer or "unknown"200 { items: ChargerSession[], pagination { page, limit, total, totalPages }, filters { customers }, totals, totalsCapped }
GET /v2/admin/chargers/:chargerId/qrchargers:readConsulta: connector?: integer[0,255]200 image/png binario, 300 px, Content-Disposition: inline, Cache-Control: no-store
GET /v2/admin/chargers/:chargerId/composite-scheduleschargers:readConsulta: cursor?, limit?=20 (1..50), connectorId?200 { items: CompositeSchedule[], nextCursor: number or null }
GET /v2/admin/chargers/:chargerId/configurationchargers:readCuerpo/consulta: ninguno200 { items: ChargerConfiguration[] }; los valores de tipo secreto son [REDACTED]
PATCH /v2/admin/chargers/:chargerId/configurationchargers:configurationCuerpo: { key: string(1..50), value: string(0..500) }200 { status: string }, donde status es el resultado del cargador/OCPP
POST /v2/admin/chargers/:chargerId/configuration/reloadchargers:configurationCuerpo: ninguno200 { status: string }
GET /v2/admin/chargers/:chargerId/firmwarechargers:readCuerpo/consulta: ninguno200 { firmware: { status, notifiedAt, currentVersion, bootNotifiedAt } }
POST /v2/admin/chargers/:chargerId/firmwarechargers:firmwareCuerpo: { url: HTTP-or-HTTPS URL, max 2048 characters }202 { status: string }
POST /v2/admin/chargers/:chargerId/commands/resetchargers:commandsCuerpo: { resetType: "Soft" or "Hard" }200 { status: string }
POST /v2/admin/chargers/:chargerId/commands/availabilitychargers:commandsCuerpo: { availabilityType: "Operative" or "Inoperative" }200 { status: string }
POST /v2/admin/chargers/:chargerId/commands/triggerchargers:commandsCuerpo: { messageType: "BootNotification", "Heartbeat", "StatusNotification", or "MeterValues" }200 { status: string, connectorResults: { connectorId, status }[] }
POST /v2/admin/chargers/:chargerId/commands/unlockchargers:commandsCuerpo: { connectorId: positive integer }200 { status: string }
POST /v2/admin/chargers/:chargerId/commands/composite-schedulechargers:commandsCuerpo: { connectorId: nonnegative integer, durationSeconds?: integer[60,604800], chargingRateUnit?: "W" or "A" }200 { status: string, connectorId: number, compositeSchedule: CompositeSchedule or null }

Endpoints de informes

Método y rutaÁmbitoSolicitudRespuesta correcta
GET /v2/admin/reports/datareports:readConsulta: ReportFilters, cursor? opaco, limit?=50 (1..100)200 { items: ReportSession[], pageTotals, page { limit, nextCursor }, range { from, to, toExclusive: true } }
GET /v2/admin/reports/schedulereports:readCuerpo/consulta: ninguno200 { schedule: ReportSchedule or null }
PUT /v2/admin/reports/schedulereports:schedulesCuerpo: ReportScheduleInput estricto200 { schedule: ReportSchedule }
POST /v2/admin/reports/sendreports:sendCuerpo: ninguno; la cabecera Idempotency-Key es obligatoria200 { sent: true, format: "csv" or "pdf", rowCount: number, range { from, to } }; si no hay programación guardada, devuelve 409 REPORT_SCHEDULE_REQUIRED
GET /v2/admin/reports/measurementsreports:readConsulta: transactionLogId obligatorio, cursor? opaco, limit?=500 (1..1000)200 { transactionLogId, measurements: { name, unit, lines[] }[], page { limit, nextCursor, sampleValuesTruncated, sampleLimitPerMeterValue: 64 } }
GET /v2/admin/reports/exportreports:exportConsulta: ReportFilters, format?="csv" or "pdf"200 binario CSV/PDF con nombre de archivo adjunto, Cache-Control: private, no-store, X-Content-Type-Options: nosniff; más de 2.000 filas devuelve 422 REPORT_EXPORT_ROW_LIMIT

Endpoints de tarifas

Método y rutaÁmbitoSolicitudRespuesta correcta
GET /v2/admin/tariffstariffs:readCuerpo/consulta: ninguno200 { items: Tariff[], lookups { currencies, biddingZones, currencyRates, componentTypes, seasons, tariffTypes } }
POST /v2/admin/tariffstariffs:createCuerpo: TariffInput201 { tariff: Tariff }
GET /v2/admin/tariffs/:tariffIdtariffs:readCuerpo/consulta: ninguno200 { tariff: Tariff, lookups }
PATCH /v2/admin/tariffs/:tariffIdtariffs:updateCuerpo: TariffInput completo200 { tariff: Tariff }
PUT /v2/admin/tariffs/:tariffId/componentstariffs:componentsCuerpo: { components: TariffComponentInput[0..256] }200 { tariff: Tariff, components: TariffComponent[] }
DELETE /v2/admin/tariffs/:tariffIdtariffs:deleteCuerpo: ninguno204 empty; las referencias de registros de carga devuelven 409 TARIFF_CONFLICT
GET /v2/admin/tariffs/:tariffId/market-pricestariffs:readConsulta: startsAt obligatorio, marca de tiempo Unix no negativa200 { items: { id, value, currencyId, startsAt, endsAt, startsAtIso, endsAtIso, intervalMinutes }[], truncated: boolean }; como máximo 10.000 filas
GET /v2/admin/chargers/:chargerId/tariff-assignmentschargers:tariff-assignments + tariffs:readCuerpo/consulta: ninguno200 { items: TariffAssignment[], tariffs: Tariff[] }
POST /v2/admin/chargers/:chargerId/tariff-assignmentschargers:tariff-assignments + tariffs:assignCuerpo: TariffAssignmentCreateInput; omitir connectorId o indicar connectorId=0 significa todos los conectores201 { assignments: TariffAssignment[] }
PATCH /v2/admin/chargers/:chargerId/tariff-assignments/:assignmentIdchargers:tariff-assignments + tariffs:assignCuerpo: TariffAssignmentUpdateInput; el conector es obligatorio y no puede ser 0200 { assignment: TariffAssignment }
DELETE /v2/admin/chargers/:chargerId/tariff-assignments/:assignmentIdchargers:tariff-assignments + tariffs:assignCuerpo: ninguno204 empty

TariffAssignment contiene id, chargerId, connectorId, un resumen seguro de tarifa/moneda, createdAt y updatedAt.

Endpoints de perfiles de carga

Método y rutaÁmbitoSolicitudRespuesta correcta
GET /v2/admin/charging-profileschargingProfiles:readCuerpo/consulta: ninguno200 { items: ChargingProfile[], lookups { locations, purposes, kinds, recurrencyKinds, rateUnits, allowedRateUnits } }
POST /v2/admin/charging-profileschargingProfiles:createCuerpo: ChargingProfileCreate201 { profile: ChargingProfile }
GET /v2/admin/charging-profiles/:profileIdchargingProfiles:readCuerpo/consulta: ninguno200 { profile: ChargingProfile, lookups, isEditBlocked: boolean, usage { simpleChargingSchedule, smartSchedule, chargePoolChargepoint, isUsed } }
PATCH /v2/admin/charging-profiles/:profileIdchargingProfiles:updateCuerpo: ChargingProfileInput completo200 { profile: ChargingProfile, ocppSync: OcppCommandResult[] }
PUT /v2/admin/charging-profiles/:profileId/schedulechargingProfiles:updateCuerpo: ChargingScheduleInput200 { profile: ChargingProfile, schedule: SchedulePeriod[], ocppSync: OcppCommandResult[] }
DELETE /v2/admin/charging-profiles/:profileIdchargingProfiles:deleteCuerpo: ninguno204 empty solo después de que todos los cargadores asignados acepten el borrado y se vuelva a validar el estado; en otro caso, 409 y se conserva el perfil
GET /v2/admin/chargers/:chargerId/profile-assignmentschargers:profile-assignments + chargingProfiles:readCuerpo/consulta: ninguno200 { items: ProfileAssignment[], profiles: ChargingProfile[] }
POST /v2/admin/chargers/:chargerId/profile-assignmentschargers:profile-assignments + chargingProfiles:assignCuerpo: ProfileAssignmentInput201 { assignment: ProfileAssignment, ocpp: OcppCommandResult }
PATCH /v2/admin/chargers/:chargerId/profile-assignments/:assignmentIdchargers:profile-assignments + chargingProfiles:assignCuerpo: ProfileAssignmentInput200 { assignment: ProfileAssignment, ocpp: OcppCommandResult }
DELETE /v2/admin/chargers/:chargerId/profile-assignments/:assignmentIdchargers:profile-assignments + chargingProfiles:assignCuerpo: ninguno204 empty después del borrado sincronizado

ProfileAssignment contiene su ID de asignación, los ID de cargador/conector y un resumen seguro del perfil. Los objetos de resultado OCPP muestran información estable de solicitud/estado y nunca credenciales.

Endpoints de usuarios, RFID, acceso, límites y usuarios locales

Método y rutaÁmbitoSolicitudRespuesta correcta
GET /v2/admin/usersusers:readConsulta: cursor? acorde con el estado (members: numérico positivo; invitations: i2.* opaco autenticado; all: continuación numérica del flujo de miembros heredado o compuesto a2.* opaco autenticado), limit?=50 (1..100), status?="all", "members", or "invitations"; cabecera X-Admin-User-Search opcional (máx. 100)200 { items: User[], invitations: Invitation[], chargers: ChargerSummary[], page { nextCursor, memberNextCursor, invitationNextCursor, limit } }; las invitaciones se paginan y se completan usando la misma expresión de correo normalizada para que las mayúsculas o espacios heredados no oculten filas; los cursores están vinculados al espacio de trabajo y al filtro de búsqueda, no contienen correos legibles y deben devolverse sin cambios; los cursores all avanzan de forma independiente por los flujos de miembros e invitaciones
POST /v2/admin/usersusers:createCuerpo: UserCreate; el cliente ya debe estar registrado201 { user: User }; un correo desconocido devuelve el cuerpo 404 RESOURCE_NOT_FOUND, que no revela la existencia del recurso, y se debe utilizar una invitación
POST /v2/admin/users/invitationsusers:invitations + users:access; además users:rfid si se proporciona rfidCuerpo: InvitationInput200 or 201 { result }; el resultado vinculado es { kind:"attached", user, accessChanged, rfidChanged }; el pendiente es { kind:"invited", email, name, maskedRfid, chargerIds, createdCount, emailSent, emailRetryReady, alreadyInvited }
POST /v2/admin/users/invitations/resendusers:invitationsCuerpo: { email: email }200 { result: { targetedChargers, emailSent, emailRetryReady } }; se aplica un tiempo de espera global por destinatario
DELETE /v2/admin/users/invitationsusers:invitationsCuerpo: { email: email }204 empty
GET /v2/admin/users/:userIdusers:read + reports:readCuerpo/consulta: ninguno200 { user: UserDetail, sessions: UserSession[], totals { count, energyWh, durationSeconds }, window, limitTypes, chargers }; como máximo 200 sesiones durante 90 días
PATCH /v2/admin/users/:userIdusers:updateCuerpo: { name: string(1..255) }200 { user: User }
DELETE /v2/admin/users/:userIdusers:deleteCuerpo: ninguno204 empty
GET /v2/admin/users/:userId/vehiclesusers:readConsulta: cursor? numérico, limit?=50 (1..100)200 { items: Vehicle[], page { nextCursor, limit } }; el DTO del vehículo incluye campos seguros de proveedor/estado/modelo/telemetría
GET /v2/admin/users/:userId/rfidsusers:readCuerpo/consulta: ninguno200 { items: RFID[] }; los UID están enmascarados
POST /v2/admin/users/:userId/rfidsusers:rfidCuerpo: { uid: string(1..36), name?: string or null }201 { rfid: RFID }; no se devuelve el UID sin enmascarar
PATCH /v2/admin/users/:userId/rfids/:rfidIdusers:rfidCuerpo: { name: string or null }200 { rfid: RFID }
DELETE /v2/admin/users/:userId/rfids/:rfidIdusers:rfidCuerpo: ninguno204 empty
GET /v2/admin/users/:userId/accessusers:readCuerpo/consulta: ninguno200 { access: Access }
PUT /v2/admin/users/:userId/accessusers:accessCuerpo: AccessInput200 { access: Access }
DELETE /v2/admin/users/:userId/access/:accessIdusers:accessCuerpo: ninguno204 empty
GET /v2/admin/users/:userId/limitsusers:readCuerpo/consulta: ninguno200 { items: Limit[] }
POST /v2/admin/users/:userId/limitsusers:limitsCuerpo: LimitCreate201 { limit: Limit }
PATCH /v2/admin/users/:userId/limits/:limitIdusers:limitsCuerpo: LimitPatch, al menos una propiedad200 { limit: Limit }
DELETE /v2/admin/users/:userId/limits/:limitIdusers:limitsCuerpo: ninguno204 empty
GET /v2/admin/chargers/:chargerId/local-userschargers:read + users:readCuerpo/consulta: ninguno200 { charger: ChargerSummary, items: LocalUser[], invitations: { email }[], availableMembers: [] }; la API omite deliberadamente el directorio amplio de miembros disponibles del portal
POST /v2/admin/chargers/:chargerId/local-userschargers:local-users + users:accessCuerpo: { customerId: positive integer, expiresAt?: RFC3339 or null }201 { localUser: LocalUserMutation }
POST /v2/admin/chargers/:chargerId/local-users/invitationschargers:local-users + users:invitations + users:accessCuerpo: { email: email }200 or 201 { result }; cliente existente: { invited:false, localUser: LocalUserMutation }; invitación: { invited:true, alreadyInvited, emailSent, emailRetryReady }
DELETE /v2/admin/chargers/:chargerId/local-users/invitationschargers:local-users + users:invitationsCuerpo: { email: email }204 empty
PATCH /v2/admin/chargers/:chargerId/local-users/:localUserIdchargers:local-users + users:accessCuerpo: { expiresAt: RFC3339 or null }200 { localUser: LocalUserUpdate }
DELETE /v2/admin/chargers/:chargerId/local-users/:localUserIdchargers:local-users + users:accessCuerpo: ninguno204 empty

El usuario de sistema de Tagless está protegido frente a estas operaciones. Los fallos de correo de invitación devuelven un estado de entrega seguro sin revelar detalles SMTP; los clientes pueden reintentar cuando emailRetryReady sea verdadero.

Respuestas binarias

Dos familias de endpoints no devuelven JSON cuando la operación tiene éxito:

EndpointTipo de contenidoCabeceras importantes
QR del cargadorimage/pngCache-Control: no-store, Content-Disposition: inline, X-Content-Type-Options: nosniff
Exportación de informestext/csv; charset=utf-8 o application/pdfCache-Control: private, no-store, Content-Disposition: attachment, X-Content-Type-Options: nosniff

Los errores de los endpoints binarios siguen siendo JSON.