📡 PUBLIC REST API · DuckDB · 32M rows
REST API over
Peru's public budget.
An HTTP endpoint that accepts DuckDB SQL and returns JSON. Free access to the
mef_historico table with
137 columns and 32M rows (years 2013-2026). No auth, no API keys, no SDKs:
any HTTP-speaking tool can consume it.
https://app.gestionpublicaperu.com.pe/api/insights/query Available endpoints
Seven routes, all under https://app.gestionpublicaperu.com.pe.
No special headers required — only Content-Type: application/json for POST.
/api/insights/schema Metadata: 137 columns + types, min/max year, total rows, and an example query.
— /api/insights/query Executes a DuckDB query over mef_historico. SELECT/WITH only. Cap 10k rows, 30s timeout.
{ "sql": "SELECT ...", "limit": 1000 } /api/insights/slices/ Lists the precomputed JSON slices (kpis, evolution, sectors, category, departments, pliegos, meta).
— /api/insights/slices/{name}.json Serves a specific JSON slice. Same data the public CDN serves — useful for clients already talking to this origin.
— /api/insights/dimensions Distinct values for levels, funding sources, functions, departments, sectors and expense categories. Ideal to populate dropdowns. Cached 5 min.
— /api/insights/serie?sector=11 Annual PIM/Accrued/Execution % series with simple filters (sector, pliego, department, level, source, function). No SQL — just query params.
— /api/insights/top-sectores?anio=2025 Top sectors by PIM in a given year. Default = last closed year. Returns up to 33 rows with code, name, PIM, accrued spending and execution %.
— How to use it
Four variants for four audiences. Pick the one that fits.
🐚 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 for notebooks and 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_bn_PEN,
ROUND(SUM(DEVENGADO_)*100
/ NULLIF(SUM(MTO_PIM),0), 1) AS execution_pct
FROM mef_historico
WHERE PLIEGO LIKE '036%' -- MTC (Ministry of Transport)
GROUP BY anio
ORDER BY anio
""",
"limit": 9500, # server default is 1000; raise it if you expect more rows
},
timeout=60,
)
resp.raise_for_status()
# Response returns rows as records (not lists) -> Polars loads them directly
df = pl.DataFrame(resp.json()["filas"])
print(df)
# Polars is 10-100x faster than pandas on agg/joins. Same stack as the backend.
# Full client with error handling: github.com/sulk1n/gestionpublicaperu-api-clients 🌐 JavaScript (fetch · browser or 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); 📊 Excel · Power Query M
// Excel → Data → Get Data → From Other Sources → Blank Query → Advanced Editor
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 In Excel: Data → Get Data → From Other Sources → Blank Query → Advanced Editor. Paste the block and refresh whenever you want fresh data.
📈 Power BI Desktop · Power Query M (typed)
// Power BI Desktop → Get data → Blank query → Advanced Editor
// Data source credentials: Anonymous (at the https://app.gestionpublicaperu.com.pe level)
let
url = "https://app.gestionpublicaperu.com.pe/api/insights/query",
sql = "SELECT anio,
ROUND(SUM(MTO_PIM)/1e9, 1) AS pim_bn,
ROUND(SUM(DEVENGADO_)/1e9, 1) AS dev_bn,
ROUND(SUM(DEVENGADO_)*100
/ NULLIF(SUM(MTO_PIM),0), 1) AS execution_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]),
typed = Table.TransformColumnTypes(tabla, {
{"anio", Int64.Type},
{"pim_bn", type number},
{"dev_bn", type number},
{"execution_pct", type number}
})
in
typed 🚀 60-second setup with the downloadable template
- Download
gestion-publica-peru-template.pqand open it with any text editor. - In Power BI Desktop: Home → Get data → Blank query.
- In the Advanced Editor, paste one of the 4 queries (between
// === QUERY N ===markers). - Rename the query as suggested in the comment (e.g.
01_Serie_Anual) and click Done. - Repeat for the other 3 queries with New query → Blank query.
- When prompted for credentials: Anonymous at
https://app.gestionpublicaperu.com.pelevel. - Close and apply. The template includes 5 suggested DAX measures and a recipe of recommended visuals.
🔓 Authentication
First time it prompts how to connect → choose Anonymous at the domain level app.gestionpublicaperu.com.pe. Public API, no token required.
☁️ Scheduled refresh
When publishing to Power BI Service: Dataset settings → Credentials → Anonymous. No gateway needed. Schedule the refresh after 11:00 AM Lima time (current year data updates at 10:45 AM).
⚡ Rate limit
30 queries/min/IP on /query. If your model has multiple visuals on the same data, aggregate in a single M query and reuse the table — don't open 10 parallel connections.
📐 Best practices
Always GROUP BY server-side (cap 10k rows). Type columns with Table.TransformColumnTypes before loading — otherwise DAX treats them as text.
Real use cases
Five frequent questions, with the SQL or REST call that answers them and a preview of the response.
How much did the Education sector grow between 2013 and 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% over 12 years Top 10 spending units (pliegos) by PIM in 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
... Execution rate by government level (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 No SQL needed: annual Health series with a single GET
#4SQL:
curl "https://app.gestionpublicaperu.com.pe/api/insights/serie?sector=11"
# or try:
# /api/insights/serie?pliego=036 (Ministry of Transport)
# /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 when you don't want to write SQL. Monthly accrued spending of Health in 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%' -- Health
AND DEPARTAMENTO_META LIKE '15.%' -- Lima Output:
Each row is a budget line; ideal for
GROUP BY + SUM if you want aggregates.
Returns up to 10,000 rows — narrow your
WHERE clause if your universe is large. Limits and rules
Designed for ad-hoc analysis, not for production critical applications. For now no auth, no rate limit — but with sensible safety caps.
🛡️
SELECT / WITH only
Any attempt at INSERT, UPDATE, DELETE, DROP, CREATE, ALTER, COPY, PRAGMA, ATTACH and friends returns HTTP 400. The database is read-only.
📦
Cap of 10,000 rows
The backend wraps your query with LIMIT 10000.
If you need more, add a GROUP BY or paginate
with WHERE anio = ....
⏱️
30-second timeout
If the query takes longer, it returns HTTP 504. Most queries with
GROUP BY take under 1s, so this typically
only fires on unfiltered full scans.
🔓
No auth, no keys
Public endpoint. No visible rate limit. If you abuse it and crash the server, please reach out first — we'd hate to have to put up barriers.
Quick schema
The 15 most-used columns. For the full list of 137 columns and their types, see /api/insights/schema .
| Column | Type | Description |
|---|---|---|
anio | INT | Fiscal year (2013-2026). For the current year see es_parcial. |
es_parcial | BOOL | true if it is the current-year snapshot (live data). false if it is a closed year. |
SECTOR | VARCHAR | Format "NN: NAME" with a colon. Filter with LIKE 'NN%', never with =. |
PLIEGO | VARCHAR | Spending unit. Format "NNN. NAME" (with period). E.g. "036. M. DE TRANSPORTES...". |
SEC_EJEC | VARCHAR | Executing unit code — no leading zeros (e.g. "154", not "000154"). |
NIVEL_GOBIERNO | VARCHAR | E. GOBIERNO NACIONAL · R. GOBIERNOS REGIONALES · M. GOBIERNOS LOCALES. |
DEPARTAMENTO_META | VARCHAR | Format "NN. NAME" (with period). E.g. "15. LIMA". Filter out "00 " (empty) for valid geography only. |
FUENTE_FINANC | VARCHAR | Funding source (1=Ordinary Resources, 2=Collected, etc.). |
FUNCION | VARCHAR | Budget function (e.g. "22. EDUCACION", "20. SALUD"). |
CATEGORIA_GASTO | VARCHAR | 5 CURRENT SPENDING · 6 CAPITAL SPENDING · 7 DEBT SERVICE. |
GENERICA | VARCHAR | Classifier generic (e.g. "21" = Personnel). No periods when filtering. |
MTO_PIA | DOUBLE | Initial Institutional Budget (PIA) in soles. |
MTO_PIM | DOUBLE | Modified Institutional Budget (PIM) in soles — current ceiling. |
DEVENGADO_ | DOUBLE | Accrued spending year-to-date (soles). |
MTO_DEVENGA_01..12 | DOUBLE | 12 columns — monthly accrued spending by calendar month. |
💡 Tip: SECTOR uses the format "NN: NAME" (colon)
while PLIEGO and DEPARTAMENTO_META use
"NN. NAME" (period). Always filter with LIKE 'NN%' to avoid format issues.
Note: Column names and example values are kept in their original Spanish form — they are the official MEF nomenclature.
📖 Interactive OpenAPI spec
Explore every endpoint with a live Swagger UI, or download the JSON spec to feed into Postman, Insomnia, your code generator or your favorite LLM.
Building something with this?
We'd love to see it. Reach out at soporte@gestionpublicaperu.com.pe .