Saltar a contenido

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). Con false, todas las rutas salvo whoami responden 404 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_HIDDEN con 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 202 con 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:

Authorization: Bearer ect_…

La cookie de sesión no sirve aquí y las respuestas no crean ninguna sesión. No hace falta el token CSRF.

Una consulta por la API Secuencia de la API de consulta: el programa lee el catálogo, valida la consulta con el plan de la BD, la envía y recibe 202 si no termina en la espera, sondea hasta recibir 200 con la primera página y descarga el CSV entero sin volver a la BD. GET /catalog (Bearer ect_…) tablas, dimensiones y métricas visibles POST /queries/validate plan: filas estimadas valid, estimate POST /queries?wait=10s trabajo con la identidad del titular SQL con sus universos 202 {id, status: RUNNING} + Location (o 200 + primera página si termina antes) GET /queries/{id}?wait=30s 200 + primera página GET /queries/{id}/rows (Accept: text/csv) CSV entero, sin volver a la BD Programa API de consulta Cola de trabajos clase API BD del cliente El resultado vive 30 minutos y solo lo ve la misma credencial
Una consulta por la API Secuencia de la API de consulta: el programa lee el catálogo, valida la consulta con el plan de la BD, la envía y recibe 202 si no termina en la espera, sondea hasta recibir 200 con la primera página y descarga el CSV entero sin volver a la BD. GET /catalog (Bearer ect_…) tablas, dimensiones y métricas visibles POST /queries/validate plan: filas estimadas valid, estimate POST /queries?wait=10s trabajo con la identidad del titular SQL con sus universos 202 {id, status: RUNNING} + Location (o 200 + primera página si termina antes) GET /queries/{id}?wait=30s 200 + primera página GET /queries/{id}/rows (Accept: text/csv) CSV entero, sin volver a la BD Programa API de consulta Cola de trabajos clase API BD del cliente El resultado vive 30 minutos y solo lo ve la misma credencial
Una consulta por la API Ver a tamaño completo Ver a tamaño completo

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#

curl -s "$ECENTER_URL/api/query/v1/whoami" \
  -H "Authorization: Bearer $ECENTER_API_TOKEN"
{
  "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#

curl -s "$ECENTER_URL/api/query/v1/connections" \
  -H "Authorization: Bearer $ECENTER_API_TOKEN"
[ { "id": "8d6f2c4b-…", "name": "Demo · Distribuciones Ibéricas", "engine": "POSTGRESQL" } ]

Salen las conexiones del titular (las restringidas, solo si tiene Administración del metadata) y, si la credencial se limita a unas, solo esas.

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).
  • dimensionTables y metricTables dicen 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.
  • hasMembers dice 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 en source y sus divisiones en buckets.
  • lookupTable, en una dimensión lookup, identifica su tabla auxiliar: dos dimensiones con la misma tienen los mismos códigos.
  • Sin cursor ni limit llega 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 con cursor=<nextCursor> y los mismos factTable y search.

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" }'
{ "members": [ { "code": ["N"], "label": "Norte" } ], "nextCursor": null }
  • En una dimensión lookup salen sus valores de la tabla auxiliar con su descripción (label); search busca en la descripción y parent da 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); search busca el texto tal cual (también % y _).
  • En una agrupación salen sus divisiones, con «Resto» al final.
  • code tiene 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:

{ "id": "qr_Q2hK…", "status": "RUNNING" }

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.

  1. GET /workbooks?connection=<c>&search=<texto> da los libros que puede abrir el titular con sus documentos de cubo y sus marcadores.
  2. 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 en hidden y trimmed, nunca se nombra.
  3. Envíela tal cual, o envíe una consulta con from para 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.

Véase también#