Skip to content

Public API v1 ​

The Futuros public API is versioned static data. Futuros is a frontend-only platform: there is no query server behind /api/v1/. Everything is baked from public/data/ by scripts/bake-api.ts and served as plain files over the CDN. A post-bake gate (scripts/check-api.ts, run in the same prebuild step) asserts the bake matches the corpus, citation counts line up, every cell is covered, the bulk dumps parse, generated_at is at least as new as the newest data file, and fails the build on any drift, so the served tree cannot go stale. Every endpoint below is a plain HTTP GET of a file, CDN-cacheable. Since the accounts release (October 2026) reads require an account API key (see Authentication); without one the edge answers 401 before the file is touched.

  • Base URL: https://futuros.xyz/api/v1/
  • Discovery (DCAT catalog): https://futuros.xyz/data.json (at the site root, not under /api/v1/)
  • Human docs page: https://futuros.xyz/datos-abiertos?lang=en

Because the responses are static files, an endpoint is just its path. Fetch one with curl, a browser, fetch(), pandas.read_csv, R, or any SDMX/DCAT client.

Authentication ​

Everything under /api/v1/**, /data.json and /feeds/** sits behind the futuros.xyz session wall. A client that is not a logged-in browser identifies itself with an account API key, created at /cuenta/claves (name and optional expiry; shown once; format ft_live_<48 hex>).

bash
curl -H "Authorization: Bearer $FUTUROS_API_KEY" https://futuros.xyz/api/v1/index.json
# or
curl -H "X-Api-Key: $FUTUROS_API_KEY" https://futuros.xyz/api/v1/observations/salud__MEX.json
SituationResponse
No key and no cookie401 {"error":"auth_required"} + WWW-Authenticate: Bearer
Invalid, expired or revoked key401 {"error":"auth_required"}
More than 600 requests per minute on one key429 + Retry-After
Key verifier unavailable503 + Retry-After: 30 (the file is never served unverified)
OPTIONS (CORS preflight)204, no key needed, Access-Control-Allow-Headers: Authorization, X-Api-Key

Verification runs in Routing Middleware, ahead of the CDN cache. A positive verdict is cached for 60 s per key, so a revocation takes at most that minute to apply; a negative one is never cached. The 600-per-minute ceiling is charged on every request, including those served from that cached verdict. Creating and revoking a key are written to the audit log; each key's last use shows on /cuenta/claves. CORS stays open (*): a browser app on another origin can call with the header, because Authorization is not a cookie credential.

How the tree is baked ​

bake-api.ts then check-api.ts run as the final two steps of prebuild (after typecheck, the test suite and the other data gates), so every deploy re-bakes /api/v1 from the current public/data/ and proves the result before vite build ships it:

Bake invariants (scripts/bake-api.ts):

  • Atomic promote. The whole tree is written into public/api/v1.tmp (plus public/data.json.tmp) and swapped over the live paths with renameSync as the very last step. A crash mid-bake leaves the previous tree intact, the old delete-then-repopulate flow could ship a half-written /api/v1.
  • Cell source. One cell = one public/data/parameter-cache/<param>__<ISO3>.json envelope; a cell whose payload has no indicators is skipped (the gate counts those as legitimately absent, not missing).
  • Row flattening. One CSV/SDMX row per (indicator, year). Duplicate years inside a series are dropped (first occurrence wins), and the latest vintage row is appended only when the series doesn't already contain that year, a value is never emitted twice.
  • Formula-injection neutralization (OWASP). Any string cell starting with = + - @ (or a leading tab/CR) is prefixed with ' before it reaches CSV or XLSX, so Excel/Sheets/LibreOffice render it as literal text rather than executing it as a formula. Numbers pass through untouched.
  • Append-only columns. New CSV columns are appended, never inserted (label_en,label_pt sit after citation_id), so positional parsers keep working and header-name parsers pick the additions up.
  • M49 is baked in. The ISO3→M49 map is a fixed 26-entry table in the baker (25 countries + LATAM = 419), not a runtime lookup.

What check-api.ts asserts (any failure prints a diff and exits 1):

#AssertionDrift it blocks
1index.json.counts.citations = length of public/data/citations.jsonmanifest out of step with the citation registry
2every non-empty cache cell has its baked observations/<param>__<ISO3>.jsonsilently dropped cells
3bulk CSV/SDMX/XLSX exist, are non-empty and parse (CSV: header + ≥1 row; XLSX: opens)corrupt bulk dumps
4index.json's file mtime ≥ newest public/data mtime − 5 min tolerancestale bake. Compared mtime-vs-mtime because git checkouts rewrite data-file mtimes — the old generated_at comparison false-failed on fresh checkouts
5observation rows < 1,000,000overflowing the XLSX single-sheet ceiling (1,048,576 rows)
6every search-index persona/case ref resolves to a file on diskdead retrieval hits (137 dead persona refs shipped in July 2026)
7ES/EN/PT labels complete and exact-matching the metrics registry, with a ≥90% instance-coverage floorthe public API regressing to Spanish-only, or a silently desynced label join

Stability contract ​

The manifest states it verbatim:

Shapes are additive-only within v1; breaking changes go to /api/v2/.

Practically:

  • Fields may be added to any object within v1. Do not assume a fixed key set, read by key, ignore unknowns.
  • Existing field names, types and meanings will not change within v1.
  • A breaking change (renaming/removing a field, changing units or semantics) ships as a new /api/v2/ tree; /api/v1/ keeps working.
  • index.json carries api_version, generated_at, and a stability string, read generated_at to detect a rebuild.

CORS & caching ​

Set in vercel.json for the /api/v1/(.*) path:

Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, OPTIONS
Cache-Control: public, max-age=3600, s-maxage=86400

So the read API is cross-origin readable from any browser app (GET/OPTIONS only, it is read-only), edge-cached for a day. There is no write surface here. The dynamic endpoints (/api/chat, /api/mcp, …) set their own headers in code; of those, only /api/mcp is cross-origin (*, POST, OPTIONS), the rest are same-origin.

License & attribution ​

From index.json:

  • license_url: https://creativecommons.org/licenses/by/4.0/ (CC BY 4.0)
  • license (verbatim): "Datos redistribuidos de fuentes abiertas; cada observación conserva su fuente primaria (source_name, source_url, citation_id). Al reutilizar: cite la fuente primaria de cada observación y «Futuros» como agregador."

In English: the data is redistributed from open sources; every observation keeps its primary source (name, deep-link URL, and citation_id). When you reuse it, cite each observation's primary source and credit Futuros as the aggregator. Nothing is imputed or synthesized, the honesty contract of the baker is that every value on the wire carries the source it came from.

Interoperability standards ​

index.json → standards declares the codes the API speaks so UNSD-SDG, World Bank Data360, OECD.AI and CEPALSTAT can ingest it directly:

StandardWhere
REF_AREA = UN M49 numeric + ISO 3166-1 alpha-3every geography & observation
SDG global indicator framework (A/RES/71/313)sdg block per indicator + sdg-crosswalk.json
SDMX-CSV (ISO 17369)observations.sdmx.csv
DCAT (project-open-data v1.1)/data.json

The manifest — index.json ​

Start here. It lists every dataset, live counts, the license, the stability statement, and a machine-readable endpoints map (path templates for the rest).

bash
curl -s https://futuros.xyz/api/v1/index.json
jsonc
{
  "name": "Futuros — API de datos abiertos",
  "api_version": "1",
  "generated_at": "2026-08-29T04:17:28.143Z",
  "docs": "/datos-abiertos",
  "license": "Datos redistribuidos de fuentes abiertas; …",
  "license_url": "https://creativecommons.org/licenses/by/4.0/",
  "delta_basis": "latest value vs mean of the prior 3 years (smooths COVID-era volatility)",
  "stability": "Shapes are additive-only within v1; breaking changes go to /api/v2/.",
  "languages": ["es", "en", "pt"],
  "label_fields": { "parameters": ["name_es", "name_en", "name_pt"],
                    "geographies": ["name_es", "name_en", "name_pt"],
                    "observations": ["label_es", "label_en", "label_pt"] },
  "standards": {
    "ref_area": "UN M49 numeric (REF_AREA) + ISO 3166-1 alpha-3",
    "sdg": "UN SDG global indicator framework (A/RES/71/313); see /api/v1/sdg-crosswalk.json",
    "sdmx": "SDMX-CSV (ISO 17369) at /api/v1/observations.sdmx.csv",
    "catalog": "DCAT (project-open-data v1.1) at /data.json"
  },
  "counts": {
    "parameters": 10, "geographies": 26, "cells": 260,
    "observations": 397898, "sdg_tagged_indicator_instances": 2848,
    "citations": 22291, "signal_files": 260, "governance_datasets": 3, "contributions": 0
  },
  "bulk": {
    "observations_csv": { "path": "/api/v1/observations.csv", "bytes": 170533700, "rows": 397898 },
    "observations_sdmx_csv": { "…": "…" }, "workbook_xlsx": { "…": "…" }
  },
  "endpoints": { "openapi": "/api/v1/openapi.json", "schemas": "/api/v1/schemas/",
                 "parameters": "/api/v1/parameters.json", "…": "…" }
}

counts.geographies is 26 because the 25 countries include the LATAM regional aggregate (M49 419). A cell is one parameter × one geography; an observation is one indicator-year row.

The bulk block carries bytes + row counts for each bulk dump: currently about 171 MB (observations.csv), 111 MB (observations.sdmx.csv) and 91 MB (futuros-data.xlsx), ~398k rows each, so check it before starting a bulk download. languages + label_fields (the machine-readable replacement for the earlier prose language_coverage note) declare that every parameter and geography carries name_es/name_en/name_pt and every observation label_es/label_en/label_pt. label_es is the canonical payload label and is never renamed; the EN/PT siblings are joined from public/data/metrics/registry.json on indicator_id and are null (JSON) / empty (CSV) for ids the registry does not yet cover. Counts and sizes here are illustrative, the live manifest is authoritative.

Machine-readable contract — openapi.json & schemas/ ​

The v1 contract itself ships as data, listed first in index.json → endpoints:

  • /api/v1/openapi.json: OpenAPI 3.1.0 spec covering 15 core read paths (sdg-crosswalk.json, citations.csv and the DCAT catalog /data.json are not yet in the spec).
  • /api/v1/schemas/{manifest,parameter,geography,observation-cell,citation}.schema.json : JSON Schema for each response shape.

Point a codegen or validation tool at these instead of hand-transcribing shapes from this page.


Parameters — parameters.json ​

The 10 Futuros pillars, in display order, each with its primary/secondary SDG goals.

bash
curl -s https://futuros.xyz/api/v1/parameters.json
jsonc
[
  { "slug": "salud", "name_es": "Salud", "name_en": "Salud",
    "display_order": 1, "sdg_primary": [3], "sdg_secondary": [2, 6] },
  { "slug": "educacion", "name_es": "Educación", "…": "…" }
]

Use slug (e.g. salud, educacion, cohesion-social-inclusion) as the <param> segment in observation paths.

Geographies — geographies.json ​

The 25 countries plus the LATAM region, each with ISO3 and M49.

bash
curl -s https://futuros.xyz/api/v1/geographies.json
jsonc
[
  { "iso3": "LATAM", "m49": "419", "slug": "latam",
    "name_es": "América Latina y el Caribe", "name_en": "Latin America and the Caribbean",
    "level": "region", "sub_region": null, "population_2024": null },
  { "iso3": "ARG", "m49": "032", "slug": "arg", "level": "country", "…": "…" }
]

Country slugs are the lowercased ISO3 (arg, mex, bra; the region is latam). Use iso3 (e.g. MEX, ARG, BRA) as the <ISO> segment in observation paths. M49 codes are zero-padded strings ("032", "484").


Observations — one cell ​

The core dataset. One file per parameter × country, addressed as observations/<param>__<ISO3>.json (note the double underscore separator).

JSON ​

bash
curl -s https://futuros.xyz/api/v1/observations/salud__MEX.json
jsonc
{
  "api_version": "1",
  "generated_at": "2026-08-29T04:17:28.143Z",
  "parameter": { "slug": "salud", "label_es": "Salud" },
  "geography": { "iso3": "MEX", "m49": "484", "name_es": "México",
                 "name_en": "México", "level": "country" },
  "confidence": "high",
  "last_refreshed": "2026-06-15T16:20:52.364Z",
  "indicators": [
    {
      "indicator_id": "sp_dyn_le00_in",
      "label_es": "Esperanza de vida al nacer",
      "label_en": "Life expectancy at birth",
      "label_pt": "Expectativa de vida ao nascer",
      "unit": "años",
      "latest": { "year": 2024, "value": 75.264 },
      "delta_pct": 3.199,
      "delta_basis": "latest value vs mean of the prior 3 years (smooths COVID-era volatility)",
      "good_direction": "up",
      "coverage": "primary",
      "benchmark": { "oecd": 80.39, "world": 73.48, "sea_peers": 73.87, "oecd_vintage": 2024 },
      "sdg": { "code": "3", "level": "goal",
               "indicator_name": "Ensure healthy lives and well-being …", "goal": 3 },
      "source": {
        "name": "World Bank Open Data",
        "url": "https://data.worldbank.org/indicator/SP.DYN.LE00.IN?locations=MX",
        "citation_id": "wb-sp-dyn-le00-in-mex-2024"
      },
      "series": [ { "year": 2000, "value": 72.562 }, { "year": 2001, "value": 72.912 }, "…" ]
    }
  ]
}

Every indicator carries latest (year + value), the full series back to ~2000, its source (name + primary-source deep link + citation_id), an official sdg code when one exists (level is indicator, target or goal), and delta_pct (see delta_basis: latest vs. the mean of the prior 3 years). sdg is null when no official SDG code applies, never fabricated.

CSV ​

The same cell flattened to one row per indicator-year:

bash
curl -s https://futuros.xyz/api/v1/observations/salud__MEX.csv

Header:

parameter,iso3,m49,indicator_id,label_es,unit,year,value,delta_pct_latest,
good_direction,coverage,vintage_year,sdg_code,sdg_level,source_name,source_url,citation_id,
label_en,label_pt

label_en,label_pt sit at the end of the row, column order is part of the CSV contract, so additions are always appended, never inserted.

Observations — bulk ​

Every cell concatenated into one CSV, the whole corpus in a single download (~164k rows):

bash
curl -s https://futuros.xyz/api/v1/observations.csv -o futuros-observations.csv

Same header as the per-cell CSV. Load directly:

python
import pandas as pd
df = pd.read_csv("https://futuros.xyz/api/v1/observations.csv")

Observations — SDMX-CSV ​

The bulk data as SDMX-CSV (ISO 17369), REF_AREA keyed on UN M49 to match UNSD-SDG / Data360 conventions:

bash
curl -s https://futuros.xyz/api/v1/observations.sdmx.csv
DATAFLOW,FREQ,REF_AREA,INDICATOR,TIME_PERIOD,OBS_VALUE,UNIT_MEASURE,
SDG_INDICATOR,REF_AREA_ISO3,FUTUROS_PARAMETER,SOURCE,SOURCE_URL,CITATION_ID
FUTUROS:DF_OBSERVATIONS(1.0),A,032,si_pov_gini,2000,51,índice,10,ARG,
cohesion-social-inclusion,World Bank Open Data,https://data.worldbank.org/…,wb-si-pov-gini-arg-2024

DATAFLOW is FUTUROS:DF_OBSERVATIONS(1.0), FREQ is A (annual).

Excel workbook — futuros-data.xlsx ​

One workbook with five sheets, README (license + delta_pct basis), parameters, geographies, observations (all rows), and citations:

bash
curl -s https://futuros.xyz/api/v1/futuros-data.xlsx -o futuros-data.xlsx

Citations — citations.json / citations.csv ​

The full source registry every citation_id resolves against.

bash
curl -s https://futuros.xyz/api/v1/citations.json
curl -s https://futuros.xyz/api/v1/citations.csv

CSV columns: id,source,title,url,year,retrieved_at. Join observations.*'s citation_id to citations.*'s id to attach full source metadata to any value.

Freshness ledger — freshness.json ​

Per-source refresh cadence, when each indicator was last pulled and when it is next due. A passthrough of the build's refresh-meta.json. Top-level keys: generated_at, total_indicators, indicators (per World Bank / WGI code), plus the sources block covering the whole corpus per source: each series:* row carries kind, last_refreshed, next_refresh_due, cadence_months with cadence_basis ("observed" = median spacing of the series we hold; "stated" = official release calendar, with cadence_observed_months and cadence_calendar_url; "unavailable" = series too short), overdue, coverage and vintages.

bash
curl -s https://futuros.xyz/api/v1/freshness.json
jsonc
{
  "generated_at": "2026-06-15T16:20:52.364Z",
  "total_indicators": 49,
  "indicators": {
    "SI.POV.GINI": {
      "source": "World Bank Open Data",
      "source_url": "https://data.worldbank.org/indicator/SI.POV.GINI",
      "cadence_months": 12,
      "last_refreshed": "2026-06-15T16:20:52.364Z",
      "next_refresh_due": "2027-08-15T20:11:16.364Z",
      "countries_with_data": 0, "total_countries": 25
    }
  },
  "sources": {
    "series:IBGE": {
      "kind": "series",
      "last_refreshed": "2026-08-13T20:21:00.057Z",
      "next_refresh_due": "2026-09-13T07:23:24.057Z",
      "cadence_months": 1,                 // monthly IPCA — official calendar
      "cadence_basis": "stated",
      "cadence_observed_months": 12,       // the annual spacing of our series
      "cadence_calendar_url": "https://www.ibge.gov.br/calendario-indicadores-novoportal.html",
      "overdue": false
    }
  }
}

SDG crosswalk — sdg-crosswalk.json ​

The verified map from pillars and indicators to official SDG codes (goals, pillars, indicators). Join on indicator_id.

bash
curl -s https://futuros.xyz/api/v1/sdg-crosswalk.json

Signals — signals/<param>__<ISO3>.json ​

LLM-extracted, cited "what's moving" signals per cell (present because the multi-source layer is baked). Same <param>__<ISO3> addressing as observations, and since the 2026-08-05 bake coverage is total: all 260 cells have a file (counts.signal_files in the manifest). The 10 regional-aggregate cells (<param>__LATAM), which the signal harvest never writes, are baked as an empty array []; they previously had no file and answered HTTP 404. Treat a [] as "this cell has no signals", not as an error; a 404 under signals/ now does indicate a mistyped path.

bash
curl -s https://futuros.xyz/api/v1/signals/salud__ARG.json

Contributions — contributions.json ​

License-gated data contributed through the Data Trust (Pillar 1), published only for postures that permit republishing values. Currently count: 0 — and that empty is the diligence-honest state, not a bug and not a stub waiting to be filled: the ledger accrues only real pledges under explicit, revocable consent, and demo or seeded contributions are never published. The empty artifact keeps its full contract — the license_gate note and the redistributable / citation_only counters — so a reviewer can verify the state without reading code:

bash
curl -s https://futuros.xyz/api/v1/contributions.json
json
{
  "api_version": "1",
  "generated_at": "…",
  "license_gate": "License-gated: cite-only contributions are published as citation references only (no values republished); non-commercial is aggregates-only; open/cc-by/share-alike may be baked as indicators.",
  "count": 0,
  "redistributable": 0,
  "citation_only": 0,
  "contributions": []
}

The UI discloses this same state on-screen: the registry on /contribuir and the data-trust block on /confianza show the count: 0 with the license gate explained, instead of a blank table. Both read this artifact (not just the live /api/contribute probe), so the numeral comes from count and, when present, the panel also shows generated_at and the verbatim license_gate note; a field the artifact omits is not rendered at all. The panel also distinguishes ledger_off (shared ledger unconfigured on a deployment; receipts still issue) from a configured, honestly empty ledger.

Governance Engine — governance/*.json ​

The Governance Engine (Pillar 3) datasets are part of the versioned contract since the 2026-08-05 bake (they were previously reachable only via MCP or as an unversioned file). Each is a passthrough of the file the UI consumes, served with the same additive-only stability contract, CORS and caching as the rest of v1:

PathWhat
/api/v1/governance/resilience.jsonDemocratic Resilience Index: subindices + composite, cited per country
/api/v1/governance/regulation-index.jsonRegulatory Observatory: instruments per country and pillar
/api/v1/governance/scores.jsonGoalpost composite index per geography (2010-2024 series)
bash
curl -s https://futuros.xyz/api/v1/governance/resilience.json

The MCP path remains available: get_dataset with dataset_id: "democracy-resilience" on the MCP server returns the same resilience index with deterministic compute. The unversioned file GET /data/democracy/resilience.json also still exists, but with no stability contract; prefer the /api/v1/governance/ paths above.


Discovery — DCAT catalog at /data.json ​

A DCAT / project-open-data v1.1 catalog at the site root (not under /api/v1/) so data portals and crawlers can auto-discover the datasets:

bash
curl -s https://futuros.xyz/data.json

It advertises the bulk CSV, SDMX-CSV, XLSX, citations, SDG crosswalk and freshness datasets, each with downloadURL, mediaType and CC BY 4.0 license.

RSS feeds · /feeds/ ​

The cited signals are also published as static RSS 2.0 feeds (~267; /feeds/index.json carries each bake's live count) under /feeds/ (at the site root, not under /api/v1/), built for RSS readers and institutional monitoring. Same regime as the read API: static GET, cacheable, API key required (a feed client that supports custom headers sends Authorization: Bearer ...).

PathWhat
/feeds/futuros.xmlRegional master: every signal region-wide in one feed
/feeds/<ISO3>.xmlPer country: union of its 10 pillar feeds, max 50 items
/feeds/<param>__<ISO3>.xmlPer cell (pillar × country): cited signals with source deep-links
/feeds/index.jsonManifest: total_feeds, cells_written, cells_skipped_empty, countries_written, forecasts_included, forecasts_skipped_unverifiable, and feeds[] with title, path, kind and item count
bash
curl -s https://futuros.xyz/feeds/salud__BRA.xml

The feeds are no longer frozen: scripts/bake-feeds.ts runs inside the prebuild chain (alongside bake-data-health, bake-source-ledger and bake-api, right before check-api), so they are re-baked on every deploy from the current signal corpus. The manifest is explicit about what it did not publish: cells_skipped_empty counts cells with no signal: no empty feed is emitted to simulate coverage, and forecasts_skipped_unverifiable counts forecasts left out for not being verifiable.

Data-gap backlog — GET /api/backlog ​

Unlike everything above, this is not a baked file under /api/v1/: it is a live function (api/backlog.ts), because what it serves is produced at request time rather than at bake time. It is documented here because it is public, unauthenticated and read-only, like the rest of this page.

Every time the assistant queries the corpus and a corpus tool comes back empty, the server files a structured ticket instead of letting the signal die. This endpoint is how that backlog becomes visible, to the SPA (/vacios), to the ingestion lane deciding what to source next, and to anyone who wants to check that "we don't have that figure" is followed by something.

Parameters ​

ParameterDefaultWhat it does
limit100 (max 500)How many tickets to return
country—Filter by ISO3 (upper-cased)
pillar—Filter by axis slug

Rate limit: 60 requests per minute per IP; over that it returns 429 with Retry-After. Cache: s-maxage=60, stale-while-revalidate=300.

Response ​

json
{
  "ok": true,
  "configured": true,
  "count": 2,
  "gaps": [
    {
      "id": "gap-ecu-medio-ambiente-clima-1a2b3c4d",
      "created_at": "2026-08-16T09:12:44.031Z",
      "lang": "es",
      "country": "ECU",
      "pillar": "medio-ambiente-clima",
      "route": "/atlas",
      "empty": ["get_dataset:regulation", "get_dataset:signals"],
      "candidate_sources": [
        { "title": "SIMAS", "url": "https://ambiente.gob.ec/simas", "source": "ambiente.gob.ec", "date": "2026" }
      ],
      "missing_indicators": ["Area under mining concession"],
      "hits": 7
    }
  ]
}
  • configured: false means the durable store (Upstash) is not wired on that deployment: gaps is then that instance's in-memory ring, neither shared nor persistent. It is declared rather than pretending a shared backlog exists.
  • hits is how many times that same gap has been raised. The id dedupes on the shape of the gap (country + axis + empty tools), not on the wording, so the same hole asked two ways increments a single ticket. That demand ordering is the ingestion queue.
  • candidate_sources only contains URLs an external tool actually returned in that same turn. Never a source the model named from memory.

No user text, by design ​

A ticket describes the hole, not the person who found it. It carries the country, the axis, the route, the tools that came back empty and the missing indicators; every one of those fields is drawn from platform-controlled vocabulary (ISO3 codes, axis slugs, tool keys, baked indicator labels).

The ticket neither persists nor serves the user's question. It is read only to establish that a real turn happened, and then dropped. Because this endpoint is public and unauthenticated, anything the ticket kept would be something the asker published without meaning to, and the consent posture of the Data Trust has to hold for the people using the chat as much as for the institutions contributing to it.

The text is kept, but elsewhere and under different rules: the private question log lives under a different key (chat:questions:v1, never backlog:gaps:v1) and is readable only with Authorization: Bearer $QUESTION_LOG_TOKEN: with no token, GET /api/questions refuses everything. No public path reaches it: the backlog rebuilds every ticket field by field, so this endpoint's guarantee is exactly what it was.

The guarantee is enforced on read as well as on write: tickets written before this rule are still in the store, so readGaps rebuilds each row field by field against an allow-list and drops everything else. A newly added field cannot reach the public surface by accident either: it has to be added there on purpose.

There is no write surface. Tickets are written by the chat server, in the same process that observed the empty retrieval; no browser-reachable write path exists.

Endpoint reference ​

PathWhat
/api/v1/index.jsonManifest: datasets, counts, bulk sizes, license, standards
/api/v1/openapi.jsonOpenAPI 3.1 spec of the read API (15 core paths)
/api/v1/schemas/*.schema.jsonJSON Schema: manifest, parameter, geography, observation-cell, citation
/api/v1/parameters.json10 pillars + primary/secondary SDGs
/api/v1/geographies.json25 countries + LATAM, ISO3 + M49
/api/v1/observations/<param>__<ISO3>.jsonOne cell: indicators, series, citations, SDG
/api/v1/observations/<param>__<ISO3>.csvSame cell, one row per indicator-year
/api/v1/observations.csvBulk: every cell concatenated
/api/v1/observations.sdmx.csvBulk, SDMX-CSV (ISO 17369), REF_AREA = M49
/api/v1/futuros-data.xlsxWorkbook (README/parameters/geographies/observations/citations)
/api/v1/citations.json · .csvFull citation registry
/api/v1/freshness.jsonPer-source refresh ledger
/api/v1/sdg-crosswalk.jsonPillar → SDG + indicator → official SDG code
/api/v1/signals/<param>__<ISO3>.jsonCited signals per cell (260/260 cells; the 10 regional __LATAM cells are [])
/api/v1/governance/{resilience,regulation-index,scores}.jsonGovernance Engine: democratic resilience, regulatory observatory, composite index
/api/v1/contributions.jsonLicense-gated contributed data
/data.jsonDCAT catalog (site root)
/feeds/futuros.xml · /feeds/<ISO3>.xml · /feeds/<param>__<ISO3>.xmlStatic RSS 2.0 signal feeds, re-baked on every deploy by bake-feeds inside prebuild
/feeds/index.jsonManifest of every RSS feed (live count per bake), with the counts of what was omitted
GET /api/backlogData-gap backlog (live function, not baked) — see above
GET /api/env-leversPublic posture of the 15 fail-closed production levers: per lever, its id, the variables that compose it, opens (what it enables) and probe (how to verify it from outside), plus a set boolean. Never returns secret values; Cache-Control: no-store. It is the machine-readable surface behind /confianza — see self-hosting

For agentic, tool-based access to the same corpus (with deterministic compute and citations), see the MCP server.

Crawling policy ​

The data is open by design, so crawling is not blocked: but it is declared and observed:

  • robots.txt (https://futuros.xyz/robots.txt) allows everything except /admin/ and an internal trap path. AI-training crawlers (GPTBot, ClaudeBot, CCBot, Bytespider, etc.) are explicitly allowed today; per-bot policy is a one-line change in that file (the token list is maintained in server/scrape/known-agents.ts).
  • Scraping the HTML is pointless: every app route returns the same SPA shell with no content. The machine surfaces are this API, the MCP server and the DCAT catalog (/data.json), use those.
  • Access is observed, never blocked: an observation-only middleware (middleware.ts + server/scrape/, runbook at server/scrape/README.md) flags bulk-scrape and AI-copy patterns as internal events. No request is slowed or refused by this layer.

Every figure with its source — traceability is the contract.