📡 API REST PÚBLICA · DuckDB · 32M filas

API REST sobre el
presupuesto del Perú.

Un endpoint HTTP que acepta SQL DuckDB y devuelve JSON. Acceso libre a la tabla mef_historico con 137 columnas y 32M de filas (años 2013-2026). Sin auth, sin API keys, sin SDKs: cualquier herramienta que hable HTTP puede consumirla.

POST: https://app.gestionpublicaperu.com.pe/api/insights/query

Endpoints disponibles

Cuatro rutas, todas bajo https://app.gestionpublicaperu.com.pe. Sin headers especiales — solo Content-Type: application/json para los POST.

GET /api/insights/schema

Metadata: 137 columnas + tipos, año min/max, total de filas y una query de ejemplo.

Body:
POST /api/insights/query

Ejecuta una query DuckDB sobre mef_historico. Solo SELECT/WITH. Cap 10k filas, timeout 30s.

Body: { "sql": "SELECT ...", "limit": 1000 }
GET /api/insights/slices/

Lista los JSON precomputados (kpis, evolucion, sectores, categoria, departamentos, pliegos, meta).

Body:
GET /api/insights/slices/{nombre}.json

Sirve un slice JSON específico. Mismo dato que sirve el CDN público, útil para clientes que ya hablan con este origin.

Body:
GET /api/insights/dimensions

Valores distintos de niveles, fuentes, funciones, departamentos, sectores y categorías de gasto. Ideal para poblar dropdowns. Cacheado 5 min.

Body:
GET /api/insights/serie?sector=11

Serie anual PIM/Devengado/Avance% con filtros simples (sector, pliego, departamento, nivel, fuente, funcion). Sin SQL — solo query params.

Body:
GET /api/insights/top-sectores?anio=2025

Top sectores por PIM en un año dado. Default = último año cerrado. Devuelve hasta 33 filas con código, nombre, PIM, devengado y avance %.

Body:
GET /api/insights/dump/{anio}.parquet

Descarga directa del parquet completo de un año (2013-2026), SIN cap de filas. ~80-130 MB cada uno. Soporta Range requests para resume. Ideal para análisis local con Polars/DuckDB/Power BI.

Body:
GET /api/insights/dump/manifest.json

Lista de parquets disponibles para bulk download: año, tamaño, fecha de actualización, flag es_parcial (2026 = snapshot diario).

Body:

Cómo usarla

Cinco variantes para 5 audiencias. Elegí la que te sirva.

🐚 curl (bash · terminal)

curl -X POST https://app.gestionpublicaperu.com.pe/api/insights/query \
  -H "Content-Type: application/json" \
  -d '{
    "sql": "SELECT anio, ROUND(SUM(MTO_PIM)/1e9, 1) AS pim_mil_M FROM mef_historico WHERE SECTOR LIKE \"11%\" GROUP BY anio ORDER BY anio"
  }'

🐍 Python (httpx · ideal para notebooks y scripts)

# pip install httpx polars
import httpx
import polars as pl

resp = httpx.post(
    "https://app.gestionpublicaperu.com.pe/api/insights/query",
    json={
        "sql": """
            SELECT anio,
                   ROUND(SUM(MTO_PIM)/1e9, 1)            AS pim_mil_M,
                   ROUND(SUM(DEVENGADO_)*100
                       / NULLIF(SUM(MTO_PIM),0), 1)      AS avance_pct
            FROM mef_historico
            WHERE PLIEGO LIKE '036%'   -- MTC
            GROUP BY anio
            ORDER BY anio
        """,
        "limit": 9500,   # default del server es 1000; subilo si esperas mas filas
    },
    timeout=60,
)
resp.raise_for_status()

# El response devuelve filas como records (no listas) -> Polars lo carga directo
df = pl.DataFrame(resp.json()["filas"])
print(df)

# Polars es 10-100x mas rapido que pandas en agg/joins. Stack del propio backend.
# Cliente completo con manejo de errores: github.com/sulk1n/gestionpublicaperu-api-clients

🌐 JavaScript (fetch · browser o Node)

const r = await fetch("https://app.gestionpublicaperu.com.pe/api/insights/query", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    sql: `
      SELECT DEPARTAMENTO_META,
             ROUND(SUM(MTO_PIM)/1e6, 1) AS pim_M
      FROM mef_historico
      WHERE anio = 2025 AND DEPARTAMENTO_META != '00 '
      GROUP BY DEPARTAMENTO_META
      ORDER BY pim_M DESC
      LIMIT 5
    `,
  }),
});
const { columnas, filas, n_filas } = await r.json();
console.table(filas);

💠 PowerShell (Invoke-RestMethod · nativo en Windows)

# PowerShell 5.1+ y PowerShell 7 — sin dependencias externas
$body = @{
    sql = @"
SELECT anio,
       ROUND(SUM(MTO_PIM)/1e9, 1)            AS pim_mil_M,
       ROUND(SUM(DEVENGADO_)*100
           / NULLIF(SUM(MTO_PIM),0), 1)      AS avance_pct
FROM mef_historico
WHERE SECTOR LIKE '11%'   -- Salud
GROUP BY anio
ORDER BY anio
"@
    limit = 5000
} | ConvertTo-Json

$resp = Invoke-RestMethod -Uri "https://app.gestionpublicaperu.com.pe/api/insights/query" `
    -Method Post `
    -ContentType "application/json" `
    -Body $body `
    -TimeoutSec 60

# Tabla en consola
$resp.filas | Format-Table -AutoSize

# Exportar a CSV / Excel
$resp.filas | Export-Csv -Path "salud.csv" -NoTypeInformation -Encoding UTF8

# Tip: en PowerShell 7+, usá -SkipHttpErrorCheck para inspeccionar 400/429/504
#      sin que Invoke-RestMethod tire excepción.

Sin instalar nada: Invoke-RestMethod viene con Windows. Ideal para automatizar descargas desde Task Scheduler o pipelines de DevOps. Para POST grandes, usá here-strings (@" ... "@) y ConvertTo-Json.

📊 Excel · Power Query M

// Excel → Datos → Obtener datos → De otras fuentes → Consulta vacía → Editor avanzado
let
    url     = "https://app.gestionpublicaperu.com.pe/api/insights/query",
    sql     = "SELECT anio, ROUND(SUM(MTO_PIM)/1e9, 1) AS pim FROM mef_historico GROUP BY anio ORDER BY anio",
    body    = Json.FromValue([sql = sql]),
    resp    = Web.Contents(url, [
                Content = body,
                Headers = [#"Content-Type" = "application/json"]
              ]),
    json    = Json.Document(resp),
    tabla   = Table.FromRecords(json[filas])
in
    tabla

En Excel: Datos → Obtener datos → De otras fuentes → Consulta vacía → Editor avanzado. Pegá el bloque y refrescá cuando quieras datos nuevos.

// Power BI Desktop → Obtener datos → Consulta en blanco → Editor avanzado
// Credenciales del origen: Anónimo (a nivel de https://app.gestionpublicaperu.com.pe)
let
    url     = "https://app.gestionpublicaperu.com.pe/api/insights/query",
    sql     = "SELECT anio,
                      ROUND(SUM(MTO_PIM)/1e9, 1)                     AS pim_mil_M,
                      ROUND(SUM(DEVENGADO_)/1e9, 1)                  AS dev_mil_M,
                      ROUND(SUM(DEVENGADO_)*100
                          / NULLIF(SUM(MTO_PIM),0), 1)               AS avance_pct
               FROM mef_historico
               GROUP BY anio
               ORDER BY anio",
    body    = Json.FromValue([sql = sql]),
    resp    = Web.Contents(url, [
                Content = body,
                Headers = [#"Content-Type" = "application/json"],
                ManualStatusHandling = {400, 429, 504}
              ]),
    json    = Json.Document(resp),
    tabla   = Table.FromRecords(json[filas]),
    tipada  = Table.TransformColumnTypes(tabla, {
                {"anio",       Int64.Type},
                {"pim_mil_M",  type number},
                {"dev_mil_M",  type number},
                {"avance_pct", type number}
              })
in
    tipada

🚀 Setup en 60 segundos con el template descargable

  1. Descargá gestion-publica-peru-template.pq y abrilo con cualquier editor de texto.
  2. En Power BI Desktop: Inicio → Obtener datos → Consulta en blanco.
  3. En el Editor avanzado, pegá una de las 4 queries (entre los marcadores // === QUERY N ===).
  4. Renombrá la consulta como sugiere el comentario (ej. 01_Serie_Anual) y click Listo.
  5. Repetí para las otras 3 queries con Nueva consulta → Consulta en blanco.
  6. Cuando te pida credenciales: Anónimo a nivel https://app.gestionpublicaperu.com.pe.
  7. Cerrar y aplicar. El template incluye 5 medidas DAX sugeridas y la receta de visuales recomendados.

🔓 Autenticación

Primera vez te pregunta cómo conectarte → elegí Anónimo a nivel del dominio app.gestionpublicaperu.com.pe. La API es pública, no requiere token.

☁️ Refresh programado

Al publicar en Power BI Service: Configuración del dataset → Credenciales → Anónimo. Sin gateway. Programá el refresh después de las 11:00 AM Lima (los datos del año en curso se actualizan a las 10:45 AM).

⚡ Rate limit

30 queries/min/IP en /query. Si tu modelo tiene varios visuales sobre la misma data, agregá en una sola query M y reutilizá la tabla — no abras 10 conexiones paralelas.

📐 Buenas prácticas

Siempre GROUP BY server-side (cap 10k filas). Tipá columnas con Table.TransformColumnTypes antes de cargar — sino DAX las trata como texto.

Casos de uso reales

Cuatro preguntas frecuentes, con la SQL que las contesta y un preview de la respuesta.

¿Cuánto creció el sector Educación entre 2013 y 2025?

#1

SQL:

SELECT anio,
       ROUND(SUM(MTO_PIM)/1e9, 1)      AS pim_mil_M,
       ROUND(SUM(DEVENGADO_)/1e9, 1)   AS dev_mil_M
FROM mef_historico
WHERE SECTOR LIKE '10%'   -- 10: EDUCACION
GROUP BY anio
ORDER BY anio

Output:

anio  pim_mil_M  dev_mil_M
2013  18.8       17.4
2025  41.1       36.8
→ +118% en 12 años

Top 10 pliegos por PIM en 2025

#2

SQL:

SELECT PLIEGO,
       ROUND(SUM(MTO_PIM)/1e9, 2) AS pim_mil_M
FROM mef_historico
WHERE anio = 2025
GROUP BY PLIEGO
ORDER BY pim_mil_M DESC
LIMIT 10

Output:

PLIEGO                                  pim_mil_M
006. INSTITUTO PERUANO DE SEGURIDAD ...  14.92
036. M. DE TRANSPORTES Y COMUNICACIO... 12.40
010. M. DE EDUCACION                     9.87
011. M. DE SALUD                         8.54
...

Avance de ejecución por nivel de gobierno (2025)

#3

SQL:

SELECT NIVEL_GOBIERNO,
       ROUND(SUM(MTO_PIM)/1e9, 1)            AS pim_mil_M,
       ROUND(SUM(DEVENGADO_)*100
           / NULLIF(SUM(MTO_PIM),0), 1)      AS avance_pct
FROM mef_historico
WHERE anio = 2025
GROUP BY NIVEL_GOBIERNO
ORDER BY pim_mil_M DESC

Output:

NIVEL_GOBIERNO          pim_mil_M  avance_pct
E. GOBIERNO NACIONAL    180.4      78.3
R. GOBIERNOS REGION...   52.1      82.5
M. GOBIERNOS LOCALES     40.0      71.2

Sin SQL: serie anual de Salud con un GET

#4

SQL:

curl "https://app.gestionpublicaperu.com.pe/api/insights/serie?sector=11"
# o también:
#   /api/insights/serie?pliego=036         (MTC)
#   /api/insights/serie?departamento=15    (Lima)
#   /api/insights/serie?funcion=20.%20SALUD
#   /api/insights/serie?nivel=R.%20GOBIERNOS%20REGIONALES

Output:

{
  "filtros": { "sector": "11", ... },
  "serie": [
    {"anio": 2013, "pim_mil_M": 8.04, "dev_mil_M": 7.31, "avance_pct": 90.9, ...},
    {"anio": 2025, "pim_mil_M": 14.52, "dev_mil_M": 14.15, "avance_pct": 97.4, ...}
  ]
}
→ Ideal cuando no querés escribir SQL.

Devengado mensual de Salud en Lima (2025)

#5

SQL:

SELECT MTO_DEVENGA_01 AS ene, MTO_DEVENGA_02 AS feb,
       MTO_DEVENGA_03 AS mar, MTO_DEVENGA_04 AS abr
FROM mef_historico
WHERE anio = 2025
  AND SECTOR LIKE '11%'         -- Salud
  AND DEPARTAMENTO_META LIKE '15.%'  -- Lima

Output:

Cada fila es una partida; ideal para
GROUP BY + SUM si querés el agregado.
Devuelve hasta 10.000 filas, agregá
WHERE más fino si tu universo es grande.

Descarga masiva (Parquet)

Sin cap de filas

Cuando necesitás toda la data cruda (no agregados), bajá el parquet completo del año. Cada año pesa entre 80-130 MB y contiene 2-3 millones de filas con las 137 columnas. El endpoint /dump/{anio}.parquet sirve el archivo binario directo, sin pasar por DuckDB y sin el cap de 10.000 filas del endpoint /query.

⚡ ¿Cuándo usar /dump vs /query?

📦 /dump/{anio}.parquet

  • Análisis local con Polars / pandas / DuckDB
  • Conexión Power BI con connector Parquet nativo
  • Modelos ML / notebooks Jupyter
  • Cuando necesitás todas las filas (no agregados)

🔍 /query (SQL HTTP)

  • Queries agregadas con GROUP BY (typical case)
  • Dashboards externos que consultan en vivo
  • Hasta 10.000 filas por response
  • Sin instalar nada — solo HTTP + JSON

🐚 curl (bash · terminal)

# Descargar el parquet completo del 2026 (todos las filas, sin cap)
curl -O https://app.gestionpublicaperu.com.pe/api/insights/dump/2026.parquet

# Listar años disponibles + tamaños + fecha de actualización
curl -s https://app.gestionpublicaperu.com.pe/api/insights/dump/manifest.json | jq

# Bajar TODOS los años en paralelo (2013-2026)
for y in $(seq 2013 2026); do
  curl -sO https://app.gestionpublicaperu.com.pe/api/insights/dump/$y.parquet &
done
wait
echo "Total descargado:"
du -sh mef_*.parquet

🐍 Python (streaming + Polars + DuckDB local)

# pip install httpx polars duckdb
import httpx, polars as pl

# Descargar un año
with httpx.stream("GET", "https://app.gestionpublicaperu.com.pe/api/insights/dump/2026.parquet", timeout=120) as r:
    r.raise_for_status()
    with open("mef_2026.parquet", "wb") as f:
        for chunk in r.iter_bytes(chunk_size=1024 * 1024):  # 1 MB chunks
            f.write(chunk)

# Leer directo con Polars — 100× más rápido que pasar por la API HTTP
df = pl.read_parquet("mef_2026.parquet")
print(df.shape, df.columns[:5])

# O queries DuckDB locales sobre el archivo
import duckdb
con = duckdb.connect()
# ⚠️ La columna de año en el parquet raw es ANO_EJE (no 'anio').
# Las columnas 'anio' y 'es_parcial' solo existen en la view DuckDB del backend.
total = con.execute("""
    SELECT SECTOR, ROUND(SUM(MTO_PIM)/1e9, 2) AS pim_mil_M
    FROM 'mef_2026.parquet'
    GROUP BY SECTOR ORDER BY pim_mil_M DESC LIMIT 5
""").fetchall()
print(total)

# Si querés cargar múltiples años en una sola view (replicando mef_historico):
con.execute("""
    CREATE OR REPLACE VIEW mef_local AS
    SELECT
        regexp_extract(filename, 'mef_(\d{4})', 1)::INT AS anio,
        *
    FROM read_parquet('mef_*.parquet', filename=true, union_by_name=true)
""")

httpx.stream usa chunks de 1 MB para no cargar el archivo completo en RAM. Polars lee el parquet en ~1 segundo (vs ~30-60 seg si bajaras las mismas filas como JSON).

💠 PowerShell (descarga paralela)

# PowerShell — descarga directa + tabla nativa
Invoke-WebRequest -Uri "https://app.gestionpublicaperu.com.pe/api/insights/dump/2026.parquet" `
    -OutFile "mef_2026.parquet"

# Listar disponibles
$manifest = Invoke-RestMethod "https://app.gestionpublicaperu.com.pe/api/insights/dump/manifest.json"
$manifest.anios | Format-Table anio, tamano_mb, es_parcial, actualizado -AutoSize

# Loop descargar 2013-2026
2013..2026 | ForEach-Object -Parallel {
    Invoke-WebRequest "https://app.gestionpublicaperu.com.pe/api/insights/dump/$_.parquet" -OutFile "mef_$_.parquet"
} -ThrottleLimit 4

ForEach-Object -Parallel requiere PowerShell 7+. En PS 5.1, usar loop secuencial. -ThrottleLimit 4 evita saturar la red — bajar 14 años en paralelo total son ~1.5 GB.

📈 Power BI · Connector Parquet nativo

⚠️ 2 gotchas obligatorias antes de copiar el snippet

  1. Envolver con Binary.Buffer(...). Sin esto: "Parameter.Error: Parquet.Document no se puede usar con valores binarios transmitidos". Parquet.Document necesita acceso random al footer del archivo; Web.Contents devuelve un stream.
  2. Las columnas anio y es_parcial NO existen en el parquet — son sintéticas del view DuckDB del backend. La columna real del MEF es ANO_EJE (Int64). Si querés una columna anio uniforme, sintetizala con Table.AddColumn (ver loop combinado abajo).
// Power BI Desktop — connector Parquet nativo
//
// ⚠️ 2 GOTCHAS importantes:
//   1) Parquet.Document NO acepta stream binario → envolver con Binary.Buffer(...)
//   2) El parquet raw NO tiene la columna 'anio' (es sintética del backend).
//      La columna real del MEF es ANO_EJE (BIGINT).
let
    Source = Parquet.Document(
                Binary.Buffer(                                       // ← gotcha #1
                    Web.Contents("https://app.gestionpublicaperu.com.pe/api/insights/dump/2026.parquet")
                )
              ),
    Tipado = Table.TransformColumnTypes(Source, {
                {"ANO_EJE",    Int64.Type},                          // ← gotcha #2
                {"MTO_PIM",    type number},
                {"DEVENGADO_", type number}
              })
in
    Tipado

// ── Cargar TODOS los años (2013-2026) combinados ──────────────────────
// Sintetizamos la columna 'anio' desde el loop para tener un campo
// uniforme aunque ANO_EJE viniera como string en algún archivo viejo.
let
    Anios       = {2013..2026},
    BajarUno    = (anio as number) as table =>
                    let
                        bin = Binary.Buffer(
                                Web.Contents("https://app.gestionpublicaperu.com.pe/api/insights/dump/" & Number.ToText(anio) & ".parquet")
                              ),
                        tbl = Parquet.Document(bin),
                        conAnio = Table.AddColumn(tbl, "anio", each anio, Int64.Type)
                    in
                        conAnio,
    Tablas      = List.Transform(Anios, each BajarUno(_)),
    Combinado   = Table.Combine(Tablas)
in
    Combinado

// Cuando pida credenciales: Anónimo a nivel de https://app.gestionpublicaperu.com.pe

Mucho más rápido que la query M sobre /query porque Parquet es columnar y comprimido — Power BI solo lee las columnas que usás. Para los 14 años combinados (~1.4 GB descargado), tu modelo final puede quedar en 50-200 MB si solo usás 5-10 columnas.

📅 Frecuencia de actualización

Años 2013-2025 son cerrados — definitivos, no cambian. 2026 es snapshot diario que se regenera cada día ~10:45 AM Lima (es_parcial=true en el manifest).

🚀 Cache de Cloudflare

Los archivos están cacheados en el edge de Cloudflare 1 hora (años cerrados) o ~10 min (manifest). Tu segunda descarga del mismo archivo trae del PoP de Lima en milisegundos.

🔄 Range requests

El endpoint soporta Range headers HTTP → si una descarga se corta, podés resumirla con curl -C - o httpx con offset.

Límites y reglas

Pensado para análisis ad-hoc, no para producción de aplicaciones críticas. Por ahora sin auth ni rate limit, pero con caps de seguridad razonables.

🛡️

Solo SELECT / WITH

Cualquier intento de INSERT, UPDATE, DELETE, DROP, CREATE, ALTER, COPY, PRAGMA, ATTACH y compañía devuelve HTTP 400. La base es de solo lectura.

📦

Cap 10.000 filas

El backend wrappea tu query con LIMIT 10000. Si necesitás más, agregá un GROUP BY o paginá con WHERE anio = ....

⏱️

Timeout 30 segundos

Si la query tarda más, devuelve HTTP 504. La mayoría de queries con GROUP BY tardan menos de 1s, así que esto solo aparece con full-scans sin filtros.

🔓

Sin auth, sin keys

Endpoint público. No hay rate limit visible. Si abusás y tirás el server, avísanos primero — sería triste tener que poner barreras.

Schema rápido

Las 15 columnas más usadas. Para la lista completa de 137 columnas y sus tipos, consultá /api/insights/schema .

Columna Tipo Descripción
anio INT Año fiscal (2013-2026). Para el año en curso ver es_parcial.
es_parcial BOOL true si es snapshot del año en curso (datos vivos). false si es año cerrado.
SECTOR VARCHAR Formato "NN: NOMBRE" con dos puntos. Filtrá con LIKE 'NN%', nunca con =.
PLIEGO VARCHAR Formato "NNN. NOMBRE" (con punto). Ej "036. M. DE TRANSPORTES...".
SEC_EJEC VARCHAR Código Unidad Ejecutora — sin ceros a la izquierda (ej "154", no "000154").
NIVEL_GOBIERNO VARCHAR E. GOBIERNO NACIONAL · R. GOBIERNOS REGIONALES · M. GOBIERNOS LOCALES.
DEPARTAMENTO_META VARCHAR Formato "NN. NOMBRE" (con punto). Ej "15. LIMA". Filtrá "00 " (vacío) si querés solo geo válida.
FUENTE_FINANC VARCHAR Fuente de financiamiento (1=Recursos Ordinarios, 2=Recaudados, etc.).
FUNCION VARCHAR Función presupuestal (ej "22. EDUCACION", "20. SALUD").
CATEGORIA_GASTO VARCHAR 5 GASTO CORRIENTE · 6 GASTO DE CAPITAL · 7 SERVICIO DE DEUDA.
GENERICA VARCHAR Genérica del clasificador (ej "21" = Personal). Sin puntos al filtrar.
MTO_PIA DOUBLE Presupuesto Inicial de Apertura (S/).
MTO_PIM DOUBLE Presupuesto Institucional Modificado (S/) — techo vigente.
DEVENGADO_ DOUBLE Devengado acumulado del año (S/).
MTO_DEVENGA_01..12 DOUBLE 12 columnas — devengado mensual por mes calendario.

💡 Pista: SECTOR usa formato "NN: NOMBRE" (dos puntos) mientras que PLIEGO y DEPARTAMENTO_META usan "NN. NOMBRE" (punto). Siempre filtrá con LIKE 'NN%' para evitar errores por formato.

📖 OpenAPI interactivo

Explorá cada endpoint con un Swagger UI live, o descargate la spec JSON para llevarla a Postman, Insomnia, tu generador de código o tu LLM favorito.

¿Construyendo algo con esto?

Nos encantaría verlo. Escribinos a soporte@gestionpublicaperu.com.pe .

🌐 Productos relacionados: Dashboard /insights · API REST /api · MCP server /mcp · Status /status