MENU navbar-image

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-*:

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"
}
 

Request      

GET api/agent/services

Headers

Authorization        

Example: Bearer {AGENT_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

type   string     

Tipo de servicio. Valores: dashboard-pbs, dashboard-pve, pve-node, pbs-node. Example: dashboard-pbs

include_unpublished   boolean  optional    

Incluye servicios sin SLUG (vista admin). Por defecto solo se devuelven los publicables (con SLUG). Example: false

include   string  optional    

Lista separada por comas de embeds: pve_nodes, datastores. Cada servicio incluirá los recursos del propietario, evitando llamadas N+1. Example: pve_nodes,datastores

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:

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"
}
 

Request      

DELETE api/agent/services/{id}

Headers

Authorization        

Example: Bearer {AGENT_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

ID numérico del servicio. Example: 2821

Body Parameters

reason   string  optional    

Motivo opcional de la desprovisión (queda en activity_log). Example: "Carpeta Grafana eliminada tras baja de contrato"

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"
}
 

Request      

GET api/agent/users/{email}/datastores

Headers

Authorization        

Example: Bearer {AGENT_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

email   string     

Email del usuario (URL-encoded). Example: soft4ebusiness@gmail.com

Query Parameters

type   string     

Tipo de recurso. Actualmente sólo pbs. Example: pbs

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"
}
 

Request      

GET api/agent/users/{email}/pve-nodes

Headers

Authorization        

Example: Bearer {AGENT_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

email   string     

Email del usuario (URL-encoded). Example: juanjo@reyesinformatica.com

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"
}
 

Request      

GET api/agent/dns-slaves

Headers

Authorization        

Example: Bearer {AGENT_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

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."
}
 

Request      

POST api/agent/dns-slaves/informe

Headers

Authorization        

Example: Bearer {AGENT_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

servicio_id   integer     

ID del servicio (zona). Example: 2868

secundario   string     

Identificador del servidor secundario. Example: ns1.tecnium.example

correcto   boolean     

Estado global: true = verde, false = con avisos. Example: false

reported_at   string  optional    

Marca de tiempo ISO-8601 del informe (opcional). Example: 2026-07-25T12:00:00+02:00

bloques   object[]  optional    

Avisos tipados.