📡 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.
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.
/api/insights/schema Metadata: 137 columnas + tipos, año min/max, total de filas y una query de ejemplo.
— /api/insights/query Ejecuta una query DuckDB sobre mef_historico. Solo SELECT/WITH. Cap 10k filas, timeout 30s.
{ "sql": "SELECT ...", "limit": 1000 } /api/insights/slices/ Lista los JSON precomputados (kpis, evolucion, sectores, categoria, departamentos, pliegos, meta).
— /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.
— /api/insights/dimensions Valores distintos de niveles, fuentes, funciones, departamentos, sectores y categorías de gasto. Ideal para poblar dropdowns. Cacheado 5 min.
— /api/insights/serie?sector=11 Serie anual PIM/Devengado/Avance% con filtros simples (sector, pliego, departamento, nivel, fuente, funcion). Sin SQL — solo query params.
— /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 %.
— /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.
— /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).
— 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 · Power Query M (con tipado)
// 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
- Descargá
gestion-publica-peru-template.pqy abrilo con cualquier editor de texto. - En Power BI Desktop: Inicio → Obtener datos → Consulta en blanco.
- En el Editor avanzado, pegá una de las 4 queries (entre los marcadores
// === QUERY N ===). - Renombrá la consulta como sugiere el comentario (ej.
01_Serie_Anual) y click Listo. - Repetí para las otras 3 queries con Nueva consulta → Consulta en blanco.
- Cuando te pida credenciales: Anónimo a nivel
https://app.gestionpublicaperu.com.pe. - 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?
#1SQL:
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
#2SQL:
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)
#3SQL:
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
#4SQL:
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)
#5SQL:
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
- Envolver con
Binary.Buffer(...). Sin esto: "Parameter.Error: Parquet.Document no se puede usar con valores binarios transmitidos".Parquet.Documentnecesita acceso random al footer del archivo;Web.Contentsdevuelve un stream. - Las columnas
anioyes_parcialNO existen en el parquet — son sintéticas del view DuckDB del backend. La columna real del MEF esANO_EJE(Int64). Si querés una columnaaniouniforme, sintetizala conTable.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 .