API de consulta#
La API de consulta deja que un programa pida cifras a Empowerment Center sin interfaz y con la identidad de una persona: el programa envía una consulta de cubo y recibe el resultado en JSON, CSV, Excel o Arrow. Úsela para informes automáticos, cargas hacia otras herramientas o cuadernos de análisis.
Antes de empezar#
- El usuario en cuyo nombre se consulta necesita Acceso por API y MCP y Acceso a Empowerment Center.
- Ese usuario crea una credencial de ámbito API de consulta para sí mismo (véase Credenciales de servicio).
- La API viene activada (
ecenter.api.enabled: true). Confalse, todas las rutas salvowhoamiresponden404 API_DISABLED.
Cómo funciona#
- Nunca SQL. Se pide una consulta de cubo (filas, columnas, métricas, segmentación) con los identificadores del catálogo, y ECenter la calcula con el mismo motor del visor del cubo.
- Con la seguridad de la aplicación. La credencial actúa con todos los privilegios de su titular: sus universos limitan las filas y lo que no ve no existe para la API. Pedir algo oculto responde
403 COLUMNS_HIDDENcon los identificadores pedidos, nunca con sus nombres. - Primero espera, después trabaja en segundo plano. La petición espera hasta 10 segundos (se puede pedir hasta 30). Si la consulta no ha terminado, responde
202con un identificador para seguirla. - Resultado en forma larga. Una fila por combinación de miembros y una columna por métrica, fácil de cargar en pandas, Polars, DuckDB o una hoja de cálculo.
Todas las rutas van bajo /api/query/v1 y llevan la cabecera:
La cookie de sesión no sirve aquí y las respuestas no crean ninguna sesión. No hace falta el token CSRF.
Rutas#
| Ruta | Qué hace |
|---|---|
GET /whoami |
Quién es la credencial: titular, canal, cliente, ámbitos, conexiones y caducidad |
GET /connections |
Las conexiones que puede consultar |
GET /connections/{c}/catalog |
El catálogo visible: tablas, dimensiones, métricas, agrupaciones y listas |
POST /connections/{c}/dimensions/{d}/members |
Los miembros de una dimensión, por páginas |
POST /connections/{c}/queries/validate |
Valida una consulta y estima sus filas, sin ejecutarla |
POST /connections/{c}/queries |
Envía la consulta: 200 con la primera página o 202 |
POST /connections/{c}/queries/sql |
El SQL de la consulta, sin ejecutarla (solo con Administración del Metadata) |
GET /queries/{id} |
El estado de una consulta enviada, esperando si se pide |
GET /queries/{id}/rows |
Una página en JSON, o el resultado entero en CSV, XLSX o Arrow según Accept |
GET /queries/{id}/explanation |
La explicación en palabras: qué se calcula, con qué filtros y qué universos limitan las filas |
DELETE /queries/{id} |
Cancela la consulta si sigue y la olvida |
GET /workbooks?connection= |
Los libros que puede abrir, con sus documentos de cubo y sus marcadores |
GET /workbooks/{w}/documents/{d}/query |
La consulta de cubo de un documento, lista para enviarla |
GET /openapi.json |
El contrato de la API en OpenAPI 3.1 |
El recorrido completo#
Los ejemplos usan curl y suponen dos variables de entorno, para que el secreto no quede en el historial de la consola ni en la línea de órdenes:
export ECENTER_URL=https://ecenter.example.org
export ECENTER_API_TOKEN=ect_… # el secreto de la credencial
Los identificadores (305, 1201…) son de ejemplo: en su instalación salen del catálogo.
1. Comprobar la credencial#
{
"userId": 7,
"login": "analista",
"channel": "API",
"channelRef": "cred:3f0c9a1e-…",
"credential": "3f0c9a1e-…",
"clientName": "Informe semanal (Python)",
"scopes": ["QUERY_API"],
"connections": [],
"expiresAt": "2027-01-15T00:00:00Z"
}
connections vacío quiere decir «las del titular». Un 401 INVALID_CREDENTIAL aquí significa que la credencial no vale: compruebe que no ha caducado ni se ha revocado.
2. Elegir la conexión#
Salen las conexiones del titular (las restringidas, solo si tiene Administración del metadata) y, si la credencial se limita a unas, solo esas.
3. Leer el catálogo#
C=8d6f2c4b-…
curl -s "$ECENTER_URL/api/query/v1/connections/$C/catalog?search=importe" \
-H "Authorization: Bearer $ECENTER_API_TOKEN"
{
"connection": "8d6f2c4b-…",
"factTables": [ { "id": 1, "name": "Facturas", "supportsLists": true,
"dimensionTables": [1, 2], "metricTables": [1] } ],
"dimensions": [ { "id": 305, "factTable": 2, "name": "Zona", "type": "LOOKUP_LIST", "codeLength": 1,
"hasMembers": true, "segmentable": true, "cubeable": true, "dateLevels": [],
"description": "Zona comercial del cliente, según su provincia" } ],
"metrics": [ { "id": 1201, "factTable": 1, "name": "Importe total", "aggregation": "SUM",
"formula": false, "format": "#.##0,00 €" } ],
"lists": [ { "id": 7, "factTable": 2, "name": "Clientes VIP", "autoRenew": false,
"renewedAt": "2026-09-28T08:00:00Z" } ],
"nextCursor": null
}
Lo que conviene saber del catálogo:
- Lo que no está aquí no existe para el titular. Una dimensión aparece si está colocada en el árbol de presentación y el titular tiene su privilegio; una métrica, si tiene el suyo (una fórmula, si además ve todas sus métricas).
dimensionTablesymetricTablesdicen qué se combina con qué: las métricas de una consulta tienen que poder ir juntas.factTable=deja solo lo que se usa con las métricas de esa tabla;search=busca en el nombre y en la descripción de negocio, sin distinguir mayúsculas.hasMembersdice si se pueden pedir sus miembros;dateLevels, los niveles de fecha que admite (YEAR,SEMESTER,QUARTER,MONTH,WEEK).- Una agrupación es una dimensión más, de tipo
GROUPING, con su dimensión agrupada ensourcey sus divisiones enbuckets. lookupTable, en una dimensión lookup, identifica su tabla auxiliar: dos dimensiones con la misma tienen los mismos códigos.- Sin
cursornilimitllega entero; con alguno de los dos, por páginas de 200 elementos (hasta 1000), primero las dimensiones, después las métricas y al final las listas. Pida la siguiente concursor=<nextCursor>y los mismosfactTableysearch.
4. Buscar los códigos de una condición#
Para filtrar por «Zona», pida sus miembros:
curl -s -X POST "$ECENTER_URL/api/query/v1/connections/$C/dimensions/305/members" \
-H "Authorization: Bearer $ECENTER_API_TOKEN" -H "Content-Type: application/json" \
-d '{ "search": "nor" }'
- En una dimensión lookup salen sus valores de la tabla auxiliar con su descripción (
label);searchbusca en la descripción yparentda los hijos de un miembro en una jerarquía. - En una que no es lookup salen los valores distintos de los registros que ve el titular, con cuántos registros tiene cada uno (
records);searchbusca el texto tal cual (también%y_). - En una agrupación salen sus divisiones, con «Resto» al final.
codetiene una parte por columna de la clave y se usa tal cual en una condición.- Páginas de 200 miembros por defecto, hasta 1000.
5. Validar la consulta#
cat > consulta.json <<'EOF'
{
"rows": [ { "dimension": 305 } ],
"columns": [ { "dimension": 310, "dateLevel": "YEAR" } ],
"metrics": [ { "metric": 1201 }, { "metric": 1201, "display": "PERCENT_OF_TOTAL" } ],
"segmentation": { "root": { "type": "group", "logic": "AND", "children": [
{ "type": "condition", "dimension": 305, "operator": "IN",
"values": [ { "type": "literal", "value": "N" }, { "type": "literal", "value": "C" } ] } ] } },
"totals": true
}
EOF
curl -s -X POST "$ECENTER_URL/api/query/v1/connections/$C/queries/validate" \
-H "Authorization: Bearer $ECENTER_API_TOKEN" -H "Content-Type: application/json" \
-d @consulta.json
{ "valid": true, "problems": [], "warnings": [], "hidden": null,
"estimate": { "rows": 24, "heavy": false, "tooHeavy": false } }
Validar no ejecuta nada: comprueba la consulta contra el metadata y pide a la base de datos una estimación de filas. Si un identificador no existe, valid es false y candidates propone lo que probablemente quería, siempre del catálogo visible:
"candidates": [ { "kind": "METRIC", "requested": 305,
"suggestions": [ { "kind": "DIMENSION", "id": 305, "name": "Zona" } ] } ]
El cuerpo de la consulta se explica entero en La consulta de cubo.
6. Enviar la consulta#
curl -s -i -X POST "$ECENTER_URL/api/query/v1/connections/$C/queries?wait=20s" \
-H "Authorization: Bearer $ECENTER_API_TOKEN" -H "Content-Type: application/json" \
-d @consulta.json
wait admite 10, 10s, 500ms o 1m, y se acota a 30 segundos; wait=0 no espera. Hay dos respuestas posibles:
200: ha terminado y trae la primera página (1000 filas por defecto; limit= hasta 10 000):
{
"id": "qr_Q2hK…", "status": "SUCCEEDED",
"calculatedAt": "2026-09-25T10:14:03Z", "expiresAt": "2026-09-25T10:44:03Z",
"columns": [
{ "name": "Zona (código)", "role": "CODE", "dimension": 305, "type": "TEXT" },
{ "name": "Zona", "role": "LABEL", "dimension": 305, "type": "TEXT" },
{ "name": "Año", "role": "CODE", "dimension": 310, "dateLevel": "YEAR", "type": "NUMBER" },
{ "name": "Importe total", "role": "METRIC", "metric": 1201, "display": "VALUE", "type": "NUMBER", "format": "#.##0,00 €" },
{ "name": "Importe total", "role": "METRIC", "metric": 1201, "display": "PERCENT_OF_TOTAL", "type": "NUMBER", "format": "0,00 %" },
{ "name": "Total de", "role": "TOTAL_OF", "type": "IDS" }
],
"rows": [ ["N", "Norte", 2025, 1250340.5, 23.1, null],
["C", "Centro", 2025, 980112.0, 18.1, null] ],
"totalRows": 10, "offset": 0, "nextCursor": null, "truncated": false,
"hidden": null, "warnings": []
}
Con totals: true, la última columna (TOTAL_OF) dice en cada fila de total qué dimensiones agrega (sus códigos van nulos); en las filas de datos va nula. format es la máscara de la métrica en el metadata, o 0,00 % si lo que se ve es un porcentaje.
202: sigue calculándose. La respuesta lleva la cabecera Location con su dirección y Retry-After: 1:
7. Seguir una consulta que tarda#
curl -s "$ECENTER_URL/api/query/v1/queries/qr_Q2hK…?wait=30s" \
-H "Authorization: Bearer $ECENTER_API_TOKEN"
Responde 200 con la primera página en cuanto termina, o 202 si sigue pasados esos segundos. Repita hasta tener el 200. Para dejarla, DELETE /queries/{id} la cancela y responde 204.
Si la base de datos estima que la consulta es pesada, el envío (si lo sabe dentro de su espera) o el seguimiento responden 409 HEAVY_QUERY con estimate.rows: decida si la calcula de todos modos repitiendo el envío con confirmHeavy=true (véase Resultados, límites y errores).
8. Leer el resto del resultado#
Con nextCursor:
curl -s "$ECENTER_URL/api/query/v1/queries/qr_Q2hK…/rows?cursor=<nextCursor>&limit=5000" \
-H "Authorization: Bearer $ECENTER_API_TOKEN"
O el resultado entero en otro formato, con la cabecera Accept:
# CSV (RFC 4180, UTF-8); delimiter=; para Excel en español
curl -s "$ECENTER_URL/api/query/v1/queries/qr_Q2hK…/rows?delimiter=;" \
-H "Authorization: Bearer $ECENTER_API_TOKEN" -H "Accept: text/csv" -o resultado.csv
# La tabla pivotada en Excel, para personas
curl -s "$ECENTER_URL/api/query/v1/queries/qr_Q2hK…/rows" \
-H "Authorization: Bearer $ECENTER_API_TOKEN" \
-H "Accept: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" -o resultado.xlsx
# Arrow, con los tipos de las columnas (pyarrow, Polars, DuckDB)
curl -s "$ECENTER_URL/api/query/v1/queries/qr_Q2hK…/rows" \
-H "Authorization: Bearer $ECENTER_API_TOKEN" -H "Accept: application/vnd.apache.arrow.stream" -o resultado.arrows
Paginar o descargar no vuelve a la base de datos: el resultado se conserva 30 minutos desde que termina (expiresAt).
9. Explicar el resultado#
curl -s "$ECENTER_URL/api/query/v1/queries/qr_Q2hK…/explanation" \
-H "Authorization: Bearer $ECENTER_API_TOKEN"
{
"summary": "Importe total, Importe total (% del total) por Zona y Año",
"segmentation": "«Zona» es «N» o «C»",
"tables": ["Facturas", "Clientes"],
"universes": [ { "table": "Clientes", "privilege": "Zona Norte", "propagatedTo": "Facturas",
"text": "Clientes, por «Zona Norte»; se propaga a Facturas por sus relaciones" } ],
"hidden": null,
"notes": ["Solo se cuentan los registros que el usuario puede ver; no se pueden quitar estas restricciones."]
}
La explicación nombra cada universo por su tabla y por el privilegio por el que el titular lo tiene, nunca por su condición. Úsela para decir junto a las cifras qué las limita.
Partir de un documento#
Un documento de cubo de un libro ya está revisado por alguien: suele ser mejor punto de partida que una consulta escrita desde cero.
GET /workbooks?connection=<c>&search=<texto>da los libros que puede abrir el titular con sus documentos de cubo y sus marcadores.GET /workbooks/{w}/documents/{d}/query?bookmark=<marcador>devuelve la consulta del documento, restringida a lo que ve el titular y recortada a sus datos. Lo quitado se cuenta enhiddenytrimmed, nunca se nombra.- Envíela tal cual, o envíe una consulta con
frompara cambiar algo (véase La consulta de cubo).
Ver el SQL#
POST /connections/{c}/queries/sql, con la misma consulta, devuelve el SQL que se ejecutaría y sus parámetros, sin ejecutarlo, como Mostrar SQL de la aplicación. Exige además Administración del Metadata. El SQL lleva las condiciones de los universos del titular. Las comparaciones de periodos y las consultas compuestas no tienen un SQL único y responden 400.
El cliente de ejemplo#
El repositorio trae un cliente en Python sin dependencias que recorre toda la API: tools/demo-data/api_example.py. Lee el secreto de la variable ECENTER_API_TOKEN:
export ECENTER_URL=https://ecenter.example.org ECENTER_API_TOKEN=ect_…
python3 tools/demo-data/api_example.py --metric "Importe total" --rows Zona \
--columns "Fecha de factura" --date-level YEAR --async --out /tmp/resultado
Comprueba la credencial, elige la métrica y las dimensiones por su nombre, valida, envía (con --async, sin esperar, y sigue el 202), recorre las páginas, pide la explicación y descarga el CSV, el XLSX y el Arrow.
Auditoría#
Cada consulta ejecutada o rechazada queda en la auditoría con el canal API y la referencia cred:<id> de la credencial. Validar, leer el catálogo y listar libros no se auditan. Véase Auditoría.