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:
nonesignifica que no hay cuerpo de solicitud. UnDELETEque documenta un cuerpo JSON sigue requiriendo ese cuerpo.?marca una propiedad o parámetro de consulta opcional.T | nullsignifica que la propiedad está presente y admite null.204 emptysignifica que no hay cuerpo de respuesta.CursorPage<T>significa{ items: T[], nextCursor: number|string|null }, salvo que la fila nombre explícitamente un objetopageopagination.- 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:
| DTO | Campos |
|---|---|
Workspace | id, name, email, website, address { line1, line2, postalCode, city, state, countryId, countryCode, countryName }, createdAt, updatedAt |
Site | id, name, description, address, timezone { id, name }, latitude, longitude, chargerCount; el detalle añade chargers normalizados y estado de conectores |
GeocodeCandidate | placeId, displayName, latitude, longitude; nunca se devuelven credenciales ni metadatos específicos del proveedor |
MetricsWindow | from, to, límites/recuentos de sesiones y totalsCapped cuando corresponda |
Charger | id, 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 |
Connector | portId, connectorId, ID de referencia de conectores, maxVoltage, maxAmperage, maxElectricPower |
ReportSession | id, 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 |
ReportSchedule | id, 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 |
Tariff | id, cpoId, name, objetos de tipo/emplazamiento/moneda/zona de oferta, tax, fallbackPrice, price efectivo, components, assignmentCount |
ChargingProfile | id, cpoId, label, description, stackLevel, objetos de ubicación/propósito/tipo/recurrencia/unidad de tasa, validez, programación, assignmentCount, marcas de tiempo |
User | id, 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. |
RFID | id, maskedUid, name, type, valid; nunca se devuelve el UID sin enmascarar |
Access | allChargers, chargerIds ordenados y items con metadatos de permiso/cargador/emplazamiento/caducidad |
Limit | id, limitTypeId, label, energyValue, timeValue |
LocalUser | DTO 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 | Ámbito | Solicitud | Respuesta correcta |
|---|---|---|---|
GET /v2/admin | workspaces:read | Cuerpo: ninguno | 200 { workspace: Workspace } |
PATCH /v2/admin | workspaces:update | Cuerpo: WorkspacePatch | 200 { workspace: Workspace } |
GET /v2/admin/dashboard | workspaces:read + reports:read | Cuerpo/consulta: ninguno | 200 { 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 | Ámbito | Solicitud | Respuesta correcta |
|---|---|---|---|
GET /v2/admin/sites | sites:read | Cuerpo/consulta: ninguno | 200 { items: Site[], metricsWindow: MetricsWindow, lookups { countries, timezones } } |
GET /v2/admin/sites/geocode | sites:read | Consulta: 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/estado | 200 { 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/sites | sites:create | Cuerpo: SiteInput | 201 { site: Site } |
GET /v2/admin/sites/:siteId | sites:read + reports:read | Cuerpo/consulta: ninguno | 200 { site: SiteDetail, recentSessions: SiteSession[], totals, metricsWindow } |
PATCH /v2/admin/sites/:siteId | sites:update | Cuerpo: SiteInput completo | 200 { site: Site } |
DELETE /v2/admin/sites/:siteId | sites:delete | Cuerpo: ninguno | 204 empty; un emplazamiento que no esté vacío devuelve 409 SITE_NOT_EMPTY |
GET /v2/admin/sites/:siteId/power-management | sites:power-management | Cuerpo/consulta: ninguno | 200 { configuration: PowerConfiguration } con el emplazamiento propio, cargadores/conectores, grupos, fuentes de contador, asignaciones y estado de compatibilidad |
PUT /v2/admin/sites/:siteId/power-management | sites:power-management | Cuerpo: PowerConfigurationInput | 200 { 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 | Ámbito | Solicitud | Respuesta correcta |
|---|---|---|---|
GET /v2/admin/chargers | chargers:read | Cuerpo/consulta: ninguno | 200 { items: Charger[], metricsWindow: MetricsWindow, connectorLookups { formats, types, powerTypes } } |
POST /v2/admin/chargers | chargers:create | Cuerpo: ChargerCreate; el identificador se convierte a mayúsculas | 201 { charger: Charger }; nunca se devuelve la contraseña |
GET /v2/admin/chargers/:chargerId | chargers:read | Cuerpo/consulta: ninguno | 200 { charger: ChargerDetail } |
PATCH /v2/admin/chargers/:chargerId | chargers:update | Cuerpo: ChargerPatch | 200 { charger: ChargerDetail } |
DELETE /v2/admin/chargers/:chargerId | chargers:delete | Cuerpo: ninguno | 204 empty |
PATCH /v2/admin/chargers/:chargerId/credentials | chargers:credentials | Cuerpo: { password: string(0..40) } | 200 { success: true } |
PATCH /v2/admin/chargers/:chargerId/connectors/:portId | chargers:connector-settings | Cuerpo: ConnectorPatch | 200 { connector: Connector } |
PATCH /v2/admin/chargers/:chargerId/public-listing | chargers:public | Cuerpo: { isPublic: boolean } | 200 { isPublic: boolean }; si no se cumplen los requisitos de preparación, devuelve 409 CHARGER_CONFLICT |
GET /v2/admin/chargers/:chargerId/public-readiness | chargers:read | Cuerpo/consulta: ninguno | 200 { readiness: { ready, missingRequirements, connectorIds, missingTariffConnectorIds, ...commercialState } } |
GET /v2/admin/chargers/:chargerId/logs | chargers:read | Consulta: cursor?, limit?=50 (1..200), from?, to?, action?, direction?, levelId? | 200 { items: RedactedOcppLog[], nextCursor: number or null } |
GET /v2/admin/chargers/:chargerId/sessions | chargers:read + reports:read | Consulta: 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/qr | chargers:read | Consulta: connector?: integer[0,255] | 200 image/png binario, 300 px, Content-Disposition: inline, Cache-Control: no-store |
GET /v2/admin/chargers/:chargerId/composite-schedules | chargers:read | Consulta: cursor?, limit?=20 (1..50), connectorId? | 200 { items: CompositeSchedule[], nextCursor: number or null } |
GET /v2/admin/chargers/:chargerId/configuration | chargers:read | Cuerpo/consulta: ninguno | 200 { items: ChargerConfiguration[] }; los valores de tipo secreto son [REDACTED] |
PATCH /v2/admin/chargers/:chargerId/configuration | chargers:configuration | Cuerpo: { 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/reload | chargers:configuration | Cuerpo: ninguno | 200 { status: string } |
GET /v2/admin/chargers/:chargerId/firmware | chargers:read | Cuerpo/consulta: ninguno | 200 { firmware: { status, notifiedAt, currentVersion, bootNotifiedAt } } |
POST /v2/admin/chargers/:chargerId/firmware | chargers:firmware | Cuerpo: { url: HTTP-or-HTTPS URL, max 2048 characters } | 202 { status: string } |
POST /v2/admin/chargers/:chargerId/commands/reset | chargers:commands | Cuerpo: { resetType: "Soft" or "Hard" } | 200 { status: string } |
POST /v2/admin/chargers/:chargerId/commands/availability | chargers:commands | Cuerpo: { availabilityType: "Operative" or "Inoperative" } | 200 { status: string } |
POST /v2/admin/chargers/:chargerId/commands/trigger | chargers:commands | Cuerpo: { messageType: "BootNotification", "Heartbeat", "StatusNotification", or "MeterValues" } | 200 { status: string, connectorResults: { connectorId, status }[] } |
POST /v2/admin/chargers/:chargerId/commands/unlock | chargers:commands | Cuerpo: { connectorId: positive integer } | 200 { status: string } |
POST /v2/admin/chargers/:chargerId/commands/composite-schedule | chargers:commands | Cuerpo: { 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 | Ámbito | Solicitud | Respuesta correcta |
|---|---|---|---|
GET /v2/admin/reports/data | reports:read | Consulta: ReportFilters, cursor? opaco, limit?=50 (1..100) | 200 { items: ReportSession[], pageTotals, page { limit, nextCursor }, range { from, to, toExclusive: true } } |
GET /v2/admin/reports/schedule | reports:read | Cuerpo/consulta: ninguno | 200 { schedule: ReportSchedule or null } |
PUT /v2/admin/reports/schedule | reports:schedules | Cuerpo: ReportScheduleInput estricto | 200 { schedule: ReportSchedule } |
POST /v2/admin/reports/send | reports:send | Cuerpo: ninguno; la cabecera Idempotency-Key es obligatoria | 200 { 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/measurements | reports:read | Consulta: transactionLogId obligatorio, cursor? opaco, limit?=500 (1..1000) | 200 { transactionLogId, measurements: { name, unit, lines[] }[], page { limit, nextCursor, sampleValuesTruncated, sampleLimitPerMeterValue: 64 } } |
GET /v2/admin/reports/export | reports:export | Consulta: 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 | Ámbito | Solicitud | Respuesta correcta |
|---|---|---|---|
GET /v2/admin/tariffs | tariffs:read | Cuerpo/consulta: ninguno | 200 { items: Tariff[], lookups { currencies, biddingZones, currencyRates, componentTypes, seasons, tariffTypes } } |
POST /v2/admin/tariffs | tariffs:create | Cuerpo: TariffInput | 201 { tariff: Tariff } |
GET /v2/admin/tariffs/:tariffId | tariffs:read | Cuerpo/consulta: ninguno | 200 { tariff: Tariff, lookups } |
PATCH /v2/admin/tariffs/:tariffId | tariffs:update | Cuerpo: TariffInput completo | 200 { tariff: Tariff } |
PUT /v2/admin/tariffs/:tariffId/components | tariffs:components | Cuerpo: { components: TariffComponentInput[0..256] } | 200 { tariff: Tariff, components: TariffComponent[] } |
DELETE /v2/admin/tariffs/:tariffId | tariffs:delete | Cuerpo: ninguno | 204 empty; las referencias de registros de carga devuelven 409 TARIFF_CONFLICT |
GET /v2/admin/tariffs/:tariffId/market-prices | tariffs:read | Consulta: startsAt obligatorio, marca de tiempo Unix no negativa | 200 { items: { id, value, currencyId, startsAt, endsAt, startsAtIso, endsAtIso, intervalMinutes }[], truncated: boolean }; como máximo 10.000 filas |
GET /v2/admin/chargers/:chargerId/tariff-assignments | chargers:tariff-assignments + tariffs:read | Cuerpo/consulta: ninguno | 200 { items: TariffAssignment[], tariffs: Tariff[] } |
POST /v2/admin/chargers/:chargerId/tariff-assignments | chargers:tariff-assignments + tariffs:assign | Cuerpo: TariffAssignmentCreateInput; omitir connectorId o indicar connectorId=0 significa todos los conectores | 201 { assignments: TariffAssignment[] } |
PATCH /v2/admin/chargers/:chargerId/tariff-assignments/:assignmentId | chargers:tariff-assignments + tariffs:assign | Cuerpo: TariffAssignmentUpdateInput; el conector es obligatorio y no puede ser 0 | 200 { assignment: TariffAssignment } |
DELETE /v2/admin/chargers/:chargerId/tariff-assignments/:assignmentId | chargers:tariff-assignments + tariffs:assign | Cuerpo: ninguno | 204 empty |
TariffAssignment contiene id, chargerId, connectorId, un resumen seguro de tarifa/moneda, createdAt y updatedAt.
Endpoints de perfiles de carga
| Método y ruta | Ámbito | Solicitud | Respuesta correcta |
|---|---|---|---|
GET /v2/admin/charging-profiles | chargingProfiles:read | Cuerpo/consulta: ninguno | 200 { items: ChargingProfile[], lookups { locations, purposes, kinds, recurrencyKinds, rateUnits, allowedRateUnits } } |
POST /v2/admin/charging-profiles | chargingProfiles:create | Cuerpo: ChargingProfileCreate | 201 { profile: ChargingProfile } |
GET /v2/admin/charging-profiles/:profileId | chargingProfiles:read | Cuerpo/consulta: ninguno | 200 { profile: ChargingProfile, lookups, isEditBlocked: boolean, usage { simpleChargingSchedule, smartSchedule, chargePoolChargepoint, isUsed } } |
PATCH /v2/admin/charging-profiles/:profileId | chargingProfiles:update | Cuerpo: ChargingProfileInput completo | 200 { profile: ChargingProfile, ocppSync: OcppCommandResult[] } |
PUT /v2/admin/charging-profiles/:profileId/schedule | chargingProfiles:update | Cuerpo: ChargingScheduleInput | 200 { profile: ChargingProfile, schedule: SchedulePeriod[], ocppSync: OcppCommandResult[] } |
DELETE /v2/admin/charging-profiles/:profileId | chargingProfiles:delete | Cuerpo: ninguno | 204 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-assignments | chargers:profile-assignments + chargingProfiles:read | Cuerpo/consulta: ninguno | 200 { items: ProfileAssignment[], profiles: ChargingProfile[] } |
POST /v2/admin/chargers/:chargerId/profile-assignments | chargers:profile-assignments + chargingProfiles:assign | Cuerpo: ProfileAssignmentInput | 201 { assignment: ProfileAssignment, ocpp: OcppCommandResult } |
PATCH /v2/admin/chargers/:chargerId/profile-assignments/:assignmentId | chargers:profile-assignments + chargingProfiles:assign | Cuerpo: ProfileAssignmentInput | 200 { assignment: ProfileAssignment, ocpp: OcppCommandResult } |
DELETE /v2/admin/chargers/:chargerId/profile-assignments/:assignmentId | chargers:profile-assignments + chargingProfiles:assign | Cuerpo: ninguno | 204 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 | Ámbito | Solicitud | Respuesta correcta |
|---|---|---|---|
GET /v2/admin/users | users:read | Consulta: 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/users | users:create | Cuerpo: UserCreate; el cliente ya debe estar registrado | 201 { 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/invitations | users:invitations + users:access; además users:rfid si se proporciona rfid | Cuerpo: InvitationInput | 200 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/resend | users:invitations | Cuerpo: { email: email } | 200 { result: { targetedChargers, emailSent, emailRetryReady } }; se aplica un tiempo de espera global por destinatario |
DELETE /v2/admin/users/invitations | users:invitations | Cuerpo: { email: email } | 204 empty |
GET /v2/admin/users/:userId | users:read + reports:read | Cuerpo/consulta: ninguno | 200 { user: UserDetail, sessions: UserSession[], totals { count, energyWh, durationSeconds }, window, limitTypes, chargers }; como máximo 200 sesiones durante 90 días |
PATCH /v2/admin/users/:userId | users:update | Cuerpo: { name: string(1..255) } | 200 { user: User } |
DELETE /v2/admin/users/:userId | users:delete | Cuerpo: ninguno | 204 empty |
GET /v2/admin/users/:userId/vehicles | users:read | Consulta: 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/rfids | users:read | Cuerpo/consulta: ninguno | 200 { items: RFID[] }; los UID están enmascarados |
POST /v2/admin/users/:userId/rfids | users:rfid | Cuerpo: { uid: string(1..36), name?: string or null } | 201 { rfid: RFID }; no se devuelve el UID sin enmascarar |
PATCH /v2/admin/users/:userId/rfids/:rfidId | users:rfid | Cuerpo: { name: string or null } | 200 { rfid: RFID } |
DELETE /v2/admin/users/:userId/rfids/:rfidId | users:rfid | Cuerpo: ninguno | 204 empty |
GET /v2/admin/users/:userId/access | users:read | Cuerpo/consulta: ninguno | 200 { access: Access } |
PUT /v2/admin/users/:userId/access | users:access | Cuerpo: AccessInput | 200 { access: Access } |
DELETE /v2/admin/users/:userId/access/:accessId | users:access | Cuerpo: ninguno | 204 empty |
GET /v2/admin/users/:userId/limits | users:read | Cuerpo/consulta: ninguno | 200 { items: Limit[] } |
POST /v2/admin/users/:userId/limits | users:limits | Cuerpo: LimitCreate | 201 { limit: Limit } |
PATCH /v2/admin/users/:userId/limits/:limitId | users:limits | Cuerpo: LimitPatch, al menos una propiedad | 200 { limit: Limit } |
DELETE /v2/admin/users/:userId/limits/:limitId | users:limits | Cuerpo: ninguno | 204 empty |
GET /v2/admin/chargers/:chargerId/local-users | chargers:read + users:read | Cuerpo/consulta: ninguno | 200 { 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-users | chargers:local-users + users:access | Cuerpo: { customerId: positive integer, expiresAt?: RFC3339 or null } | 201 { localUser: LocalUserMutation } |
POST /v2/admin/chargers/:chargerId/local-users/invitations | chargers:local-users + users:invitations + users:access | Cuerpo: { 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/invitations | chargers:local-users + users:invitations | Cuerpo: { email: email } | 204 empty |
PATCH /v2/admin/chargers/:chargerId/local-users/:localUserId | chargers:local-users + users:access | Cuerpo: { expiresAt: RFC3339 or null } | 200 { localUser: LocalUserUpdate } |
DELETE /v2/admin/chargers/:chargerId/local-users/:localUserId | chargers:local-users + users:access | Cuerpo: ninguno | 204 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:
| Endpoint | Tipo de contenido | Cabeceras importantes |
|---|---|---|
| QR del cargador | image/png | Cache-Control: no-store, Content-Disposition: inline, X-Content-Type-Options: nosniff |
| Exportación de informes | text/csv; charset=utf-8 o application/pdf | Cache-Control: private, no-store, Content-Disposition: attachment, X-Content-Type-Options: nosniff |
Los errores de los endpoints binarios siguen siendo JSON.