📡 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.

POST: 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.

GET /api/insights/schema

Metadata: 137 columns + types, min/max year, total rows, and an example query.

Body:
POST /api/insights/query

Executes a DuckDB query over mef_historico. SELECT/WITH only. Cap 10k rows, 30s timeout.

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

Lists the precomputed JSON slices (kpis, evolution, sectors, category, departments, pliegos, meta).

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

Serves a specific JSON slice. Same data the public CDN serves — useful for clients already talking to this origin.

Body:
GET /api/insights/dimensions

Distinct values for levels, funding sources, functions, departments, sectors and expense categories. Ideal to populate dropdowns. Cached 5 min.

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

Annual PIM/Accrued/Execution % series with simple filters (sector, pliego, department, level, source, function). No SQL — just query params.

Body:
GET /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 %.

Body:

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)

import httpx

r = 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 (Ministry of Transport)
            GROUP BY anio
            ORDER BY anio
        """,
    },
    timeout=60,
)
data = r.json()
print(data["columnas"])   # ['anio', 'pim_mil_M', 'avance_pct']
print(data["filas"])      # [[2013, 4.2, 87.5], [2014, 5.1, 89.1], ...]

🌐 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.FromRows(json[filas], json[columnas])
in
    tabla

In Excel: Data → Get Data → From Other Sources → Blank Query → Advanced Editor. Paste the block and refresh whenever you want fresh data.

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?

#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% over 12 years

Top 10 spending units (pliegos) by PIM in 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
...

Execution rate by government level (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

No SQL needed: annual Health series with a single GET

#4

SQL:

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)

#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%'         -- 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 .

🌐 Related products: Dashboard · REST API · MCP server · Status