Introduction
API para agentes externos (Marta, etc.) que leen el estado de los servicios contratados en ICSManager y se auto-configuran.
Este API es **interno** — pensado para agentes bajo control propio que hacen *pull* periódico (cada 5–15 min) y reconcilian su estado.
Cada agente tiene su **propio token** (revocación granular + trazabilidad). Todas las peticiones autenticadas se registran en `activity_log` con el nombre del agente.
- **Prefijo**: `/api/agent/`
- **Formato**: JSON
- **Fechas**: ISO 8601 en UTC
Authenticating requests
To authenticate requests, include an Authorization header with the value "Bearer {AGENT_TOKEN}".
All authenticated endpoints are marked with a requires authentication badge in the documentation below.
Cada agente externo tiene un token propio, configurado en config/services.php → agent_api.tokens. Contacta al admin para solicitar un token.
Agent API
Endpoints consumidos por agentes externos (Marta y similares) para leer servicios contratados y auto-configurarse.
Listar servicios por tipo
requires authentication
Devuelve la lista de servicios contratados del tipo indicado, con su slug,
propietario y emails autorizados. El propietario siempre aparece también
dentro de autorizados. Los servicios sin SLUG definido no se incluyen
(salvo que se pase include_unpublished=1). Los servicios con activo:false
se siguen devolviendo hasta que el agente confirme la desprovisión vía
DELETE /api/agent/services/{id}.
Derivación de activo para tipos de nodo
Para type=pve-node y type=pbs-node, el campo activo no es literalmente
servicios.activo de BD — se cruza con la existencia de un dashboard activo
que lo referencie, garantizando coherencia con ?type=dashboard-*:
- pve-node:
activo:true⇔servicios.activo=trueY el propietario de al menos undashboard-pveactivo tiene autorización (en cualquier scope: perfil, contrato o servicio) sobre este nodo. - pbs-node: misma regla simétrica con
dashboard-pbs. El cruce es por autorizaciones (no porperfil_fiscal), porque el perfil del nodo refleja facturación (a menudo el proveedor de infraestructura, ej. OVH) y no necesariamente coincide con el perfil del dashboard.
Cuando se aplica el override, el payload añade
"activo_origen":"no_referenciado_en_dashboard" para que el agente pueda
distinguir "desactivado en BD" de "derivado desde dashboards". En ese caso
deactivated_at se rellena con updated_at del servicio como proxy
(no es el momento exacto en que perdió la referencia del dashboard, pero
mantiene el contrato activo:false ⇒ deactivated_at siempre poblado).
Los reconcilers que filtran por activo:true (Marta/pulso, generación
de scrape configs) se autocorrigen sin tocar BD.
Example request:
curl --request GET \
--get "https://icsmanager.docloud.es/api/agent/services?type=dashboard-pbs&include_unpublished=&include=pve_nodes%2Cdatastores" \
--header "Authorization: Bearer {AGENT_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://icsmanager.docloud.es/api/agent/services"
);
const params = {
"type": "dashboard-pbs",
"include_unpublished": "0",
"include": "pve_nodes,datastores",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {AGENT_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());import requests
import json
url = 'https://icsmanager.docloud.es/api/agent/services'
params = {
'type': 'dashboard-pbs',
'include_unpublished': '0',
'include': 'pve_nodes,datastores',
}
headers = {
'Authorization': 'Bearer {AGENT_TOKEN}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('GET', url, headers=headers, params=params)
response.json()$client = new \GuzzleHttp\Client();
$url = 'https://icsmanager.docloud.es/api/agent/services';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {AGENT_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'type' => 'dashboard-pbs',
'include_unpublished' => '0',
'include' => 'pve_nodes,datastores',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, OK pve-node):
[
{
"id": 2382,
"slug": "JRM04-ns31275378",
"activo": true,
"deactivated_at": null,
"last_modified": "2026-05-09T12:21:00+02:00",
"propietario": {
"email": "juanjo@reyesinformatica.com",
"nombre": "Juanjo Reyes"
},
"autorizados": [
{
"email": "juanjo@reyesinformatica.com"
}
],
"fqdn": "ns31275378.core.com.es"
}
]
Example response (200, activo derivado a false):
[
{
"id": 2382,
"slug": "JRM04-ns31275378",
"activo": false,
"activo_origen": "no_referenciado_en_dashboard",
"deactivated_at": null,
"last_modified": "2026-05-09T12:21:00+02:00",
"propietario": {
"email": "juanjo@reyesinformatica.com",
"nombre": "Juanjo Reyes"
},
"autorizados": [
{
"email": "juanjo@reyesinformatica.com"
}
],
"fqdn": "ns31275378.core.com.es"
}
]
Example response (400, type inválido):
{
"error": "bad_request",
"message": "Query param \"type\" is required and must be one of: dashboard-pbs, dashboard-pve, pve-node, pbs-node"
}
Example response (401, sin token):
{
"error": "unauthorized",
"message": "Missing Authorization: Bearer <token> header"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Confirmar desprovisión de un servicio
requires authentication
El agente llama este endpoint tras haber desprovisionado el recurso asociado
(ej: borrar la carpeta Grafana + grupo Authentik del dashboard). Marca el
servicio con desprovisionado_at = now() para que no vuelva a aparecer en
el endpoint de listado. Idempotente.
Reglas:
- Sólo se permite sobre servicios con
activo=false— un servicio activo devuelve409 Conflict. - No borra el servicio en ICSManager.
Example request:
curl --request DELETE \
"https://icsmanager.docloud.es/api/agent/services/2821" \
--header "Authorization: Bearer {AGENT_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"reason\": \"\\\"Carpeta Grafana eliminada tras baja de contrato\\\"\"
}"
const url = new URL(
"https://icsmanager.docloud.es/api/agent/services/2821"
);
const headers = {
"Authorization": "Bearer {AGENT_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"reason": "\"Carpeta Grafana eliminada tras baja de contrato\""
};
fetch(url, {
method: "DELETE",
headers,
body: JSON.stringify(body),
}).then(response => response.json());import requests
import json
url = 'https://icsmanager.docloud.es/api/agent/services/2821'
payload = {
"reason": "\"Carpeta Grafana eliminada tras baja de contrato\""
}
headers = {
'Authorization': 'Bearer {AGENT_TOKEN}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('DELETE', url, headers=headers, json=payload)
response.json()$client = new \GuzzleHttp\Client();
$url = 'https://icsmanager.docloud.es/api/agent/services/2821';
$response = $client->delete(
$url,
[
'headers' => [
'Authorization' => 'Bearer {AGENT_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'reason' => '"Carpeta Grafana eliminada tras baja de contrato"',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (204, OK):
Empty response
Example response (401, sin token):
{
"error": "unauthorized",
"message": "Missing Authorization: Bearer <token> header"
}
Example response (404, servicio inexistente):
{
"error": "not_found",
"message": "Service 99999999 not found"
}
Example response (409, servicio activo):
{
"error": "conflict",
"message": "Cannot deprovision an active service — set activo=false first"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Datastores accesibles para un usuario
requires authentication
Devuelve los datastores a los que el email tiene acceso — sea como propietario
o como autorizado (en scope perfil, contrato o servicio específico).
Un mismo nombre puede aparecer varias veces si hay acceso desde distintos
contratos; el cliente debe deduplicar por nombre si lo necesita.
Example request:
curl --request GET \
--get "https://icsmanager.docloud.es/api/agent/users/soft4ebusiness@gmail.com/datastores?type=pbs" \
--header "Authorization: Bearer {AGENT_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://icsmanager.docloud.es/api/agent/users/soft4ebusiness@gmail.com/datastores"
);
const params = {
"type": "pbs",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {AGENT_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());import requests
import json
url = 'https://icsmanager.docloud.es/api/agent/users/soft4ebusiness@gmail.com/datastores'
params = {
'type': 'pbs',
}
headers = {
'Authorization': 'Bearer {AGENT_TOKEN}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('GET', url, headers=headers, params=params)
response.json()$client = new \GuzzleHttp\Client();
$url = 'https://icsmanager.docloud.es/api/agent/users/soft4ebusiness@gmail.com/datastores';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {AGENT_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'type' => 'pbs',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, OK):
{
"user_email": "soft4ebusiness@gmail.com",
"datastores": [
{
"nombre": "mooring",
"contratado_gb": 1250,
"contrato": "CTR-1318"
},
{
"nombre": "track",
"contratado_gb": 1000,
"contrato": "CTR-1294"
}
]
}
Example response (200, sin datastores):
{
"user_email": "foo@bar.example",
"datastores": []
}
Example response (400, type inválido):
{
"error": "bad_request",
"message": "Query param \"type\" is required and must be one of: pbs"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Nodos PVE accesibles para un usuario
requires authentication
Devuelve los nodos PVE (servicios plugin=pve activos con contrato activo)
a los que el email tiene acceso — sea como propietario o como autorizado en
scope perfil, contrato o servicio específico.
Un mismo nodo puede aparecer varias veces si hay acceso desde distintos
contratos; el cliente debe deduplicar por fqdn si lo necesita.
El campo pve_vms está reservado para el futuro caso de VMs individuales
en PVEs compartidos — hoy devuelve array vacío.
Example request:
curl --request GET \
--get "https://icsmanager.docloud.es/api/agent/users/juanjo@reyesinformatica.com/pve-nodes" \
--header "Authorization: Bearer {AGENT_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://icsmanager.docloud.es/api/agent/users/juanjo@reyesinformatica.com/pve-nodes"
);
const headers = {
"Authorization": "Bearer {AGENT_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());import requests
import json
url = 'https://icsmanager.docloud.es/api/agent/users/juanjo@reyesinformatica.com/pve-nodes'
headers = {
'Authorization': 'Bearer {AGENT_TOKEN}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('GET', url, headers=headers)
response.json()$client = new \GuzzleHttp\Client();
$url = 'https://icsmanager.docloud.es/api/agent/users/juanjo@reyesinformatica.com/pve-nodes';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {AGENT_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, OK):
{
"user_email": "juanjo@reyesinformatica.com",
"pve_nodes": [
{
"fqdn": "ns3189882.ip-152-228-223.eu",
"slug": "reyes-pve-01"
},
{
"fqdn": "ns31275378.ip-51-210-117.eu",
"slug": ""
}
],
"pve_vms": []
}
Example response (200, sin nodos):
{
"user_email": "foo@bar.example",
"pve_nodes": [],
"pve_vms": []
}
Example response (401, sin token):
{
"error": "unauthorized",
"message": "Missing Authorization: Bearer <token> header"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
DNS Slave API
Endpoint consumido por los servidores DNS secundarios para auto-configurarse: devuelven la lista de zonas primarias de las que deben hacer de secundarios, con su FQDN, IPs, clave TSIG y usuarios autorizados (para el panel Technium).
Listar zonas dns-slave
requires authentication
Devuelve las zonas DNS del plugin dns-slave que un servidor secundario
debe gestionar. Incluye las activas y también las dadas de BAJA (servicio
inactivo o contrato no activo) marcadas con activo:false y su
deactivated_at, para que el secundario pueda retirarlas sin dejar zonas
huérfanas. Solo se omiten las desprovisionadas definitivamente. Cada zona
incluye FQDN del primario, IPs (IPv4/IPv6), puerto, clave TSIG (AXFR) y los
usuarios autorizados (propietario + autorizaciones lectura/escritura/técnico).
Todos los secundarios autenticados reciben la misma lista.
Example request:
curl --request GET \
--get "https://icsmanager.docloud.es/api/agent/dns-slaves" \
--header "Authorization: Bearer {AGENT_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://icsmanager.docloud.es/api/agent/dns-slaves"
);
const headers = {
"Authorization": "Bearer {AGENT_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());import requests
import json
url = 'https://icsmanager.docloud.es/api/agent/dns-slaves'
headers = {
'Authorization': 'Bearer {AGENT_TOKEN}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('GET', url, headers=headers)
response.json()$client = new \GuzzleHttp\Client();
$url = 'https://icsmanager.docloud.es/api/agent/dns-slaves';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {AGENT_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, ok):
{
"generated_at": "2026-07-25T12:00:00+02:00",
"count": 2,
"zonas": [
{
"servicio_id": 2868,
"fqdn": "jrm1.plesk.do",
"ipv4": "51.254.54.217",
"ipv6": "2001:41d0:303:f2dc::23",
"port": 53,
"tsig": "hmac-sha256:clave...",
"activo": true,
"deactivated_at": null,
"autorizados": [
{
"email": "juanjo@reyesinformatica.com",
"nombre": "Juanjo Reyes",
"propietario": true
},
{
"email": "soporte@reyesinformatica.com",
"nombre": "Soporte"
}
]
},
{
"servicio_id": 2900,
"fqdn": "antigua.example.com",
"ipv4": "203.0.113.10",
"ipv6": null,
"port": 53,
"tsig": null,
"activo": false,
"deactivated_at": "2026-06-01T09:00:00+02:00",
"autorizados": []
}
]
}
Example response (401, sin token):
{
"error": "unauthorized",
"message": "Missing Authorization: Bearer <token> header"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Recibir informe de estado de un secundario
requires authentication
Un servidor DNS secundario envía (fire-and-forget, idempotente por hash
local) el estado de un servicio cuando cambia — incluido el estado verde
(correcto: true). Es advisory: solo se retiene el último informe por
(servicio, secundario) y se muestra en el panel; no factura ni modela
zonas. El cuerpo lleva la clave en sí mismo (servicio_id, secundario).
bloques es una lista de avisos tipados y autodescriptivos; el panel los
pinta en genérico, así que se pueden añadir tipos nuevos sin cambiar el
contrato (arranque: dns_externo, sin_delegacion, transferencia, ruido).
Example request:
curl --request POST \
"https://icsmanager.docloud.es/api/agent/dns-slaves/informe" \
--header "Authorization: Bearer {AGENT_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"servicio_id\": 2868,
\"secundario\": \"ns1.tecnium.example\",
\"correcto\": false,
\"reported_at\": \"2026-07-25T12:00:00+02:00\",
\"bloques\": [
{
\"codigo\": \"transferencia\",
\"titulo\": \"Transferencia fallida\",
\"severidad\": \"error\",
\"accion\": \"Revisar AXFR\",
\"dominios\": [
{
\"dominio\": \"example.com\",
\"diagnostico\": \"syncFailed\",
\"info\": {
\"serial\": 123
}
}
]
}
]
}"
const url = new URL(
"https://icsmanager.docloud.es/api/agent/dns-slaves/informe"
);
const headers = {
"Authorization": "Bearer {AGENT_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"servicio_id": 2868,
"secundario": "ns1.tecnium.example",
"correcto": false,
"reported_at": "2026-07-25T12:00:00+02:00",
"bloques": [
{
"codigo": "transferencia",
"titulo": "Transferencia fallida",
"severidad": "error",
"accion": "Revisar AXFR",
"dominios": [
{
"dominio": "example.com",
"diagnostico": "syncFailed",
"info": {
"serial": 123
}
}
]
}
]
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());import requests
import json
url = 'https://icsmanager.docloud.es/api/agent/dns-slaves/informe'
payload = {
"servicio_id": 2868,
"secundario": "ns1.tecnium.example",
"correcto": false,
"reported_at": "2026-07-25T12:00:00+02:00",
"bloques": [
{
"codigo": "transferencia",
"titulo": "Transferencia fallida",
"severidad": "error",
"accion": "Revisar AXFR",
"dominios": [
{
"dominio": "example.com",
"diagnostico": "syncFailed",
"info": {
"serial": 123
}
}
]
}
]
}
headers = {
'Authorization': 'Bearer {AGENT_TOKEN}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('POST', url, headers=headers, json=payload)
response.json()$client = new \GuzzleHttp\Client();
$url = 'https://icsmanager.docloud.es/api/agent/dns-slaves/informe';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {AGENT_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => \Symfony\Component\VarExporter\Internal\Hydrator::hydrate(
$o = [
clone (($p = &\Symfony\Component\VarExporter\Internal\Registry::$prototypes)['stdClass'] ?? \Symfony\Component\VarExporter\Internal\Registry::p('stdClass')),
clone $p['stdClass'],
clone $p['stdClass'],
],
null,
[
'stdClass' => [
'codigo' => [
'transferencia',
],
'titulo' => [
'Transferencia fallida',
],
'severidad' => [
'error',
],
'accion' => [
'Revisar AXFR',
],
'dominios' => [
[
$o[1],
],
],
'dominio' => [
1 => 'example.com',
],
'diagnostico' => [
1 => 'syncFailed',
],
'info' => [
1 => $o[2],
],
'serial' => [
2 => 123,
],
],
],
[
'servicio_id' => 2868,
'secundario' => 'ns1.tecnium.example',
'correcto' => false,
'reported_at' => '2026-07-25T12:00:00+02:00',
'bloques' => [
$o[0],
],
],
[]
),
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, ok):
{
"ok": true,
"servicio_id": 2868,
"secundario": "ns1.tecnium.example",
"correcto": false
}
Example response (401, sin token):
{
"error": "unauthorized",
"message": "Missing Authorization: Bearer <token> header"
}
Example response (404, servicio inexistente):
{
"error": "not_found",
"message": "Servicio no encontrado"
}
Example response (422, cuerpo inválido):
{
"error": "unprocessable",
"message": "The servicio_id field is required."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.