TransfereGov publishes three open data APIs on
api-publica.transferegov.gestao.gov.br, covering fifty-five
tables and about 6.9 million rows between them. This vignette shows how
to find your way around them and retrieve data without downloading more
than you meant to.
The code here is not run when the vignette is built, because it would call the government’s servers.
The three modules
tg_modules()
#> # A tibble: 3 × 4
#> module label tables url
#> <chr> <chr> <int> <chr>
#> 1 especiais Special transfers 20 https://api-publica.transferego…
#> 2 fundoafundo Fund-to-fund transfers 20 https://api-publica.transferego…
#> 3 parcerias Partnerships 15 https://api-publica.transferego…especiais covers special transfers, the
mechanism created by Constitutional Amendment 105/2019 that lets an
individual parliamentary amendment send money straight to a
municipality’s account, with no agreement to sign.
fundoafundo covers fund-to-fund transfers,
from a federal fund to a state or municipal one — health, social
assistance, education. parcerias covers
partnerships with civil society organizations, from the
program that announces them through to the bank statement of the account
they are paid from.
What is not here: decentralized credit between federal
bodies (ted), which the government has not published on
this host, and SICONV agreement data, which is published as CSV
downloads rather than as an API.
Finding a table
tg_tables("parcerias")
#> # A tibble: 15 × 6
#> module table path columns params
#> <chr> <chr> <chr> <int> <int>
#> 1 parcerias analise_proposta analise-proposta 7 5
#> 2 parcerias beneficiario_emenda_… beneficiario_emenda_… 16 14
#> 3 parcerias cronograma_desembolso cronograma-desembolso 7 7
#> …table is the name to pass to tg_get();
path is the endpoint it maps to. The two differ wherever
the endpoint uses a hyphen, and either spelling is accepted.
tg_fields() describes the columns, and
tg_params() the filters:
tg_fields("parcerias", "proposta")
#> # A tibble: 43 × 5
#> field r_type api_type nested description
#> <chr> <chr> <chr> <chr> <chr>
#> 1 id_proposta double integer NA Identificador único da prop…
#> 2 id_programa double integer NA Identificador do programa a…
#> …
tg_params("parcerias", "proposta")These two are not the same set. A column is what comes back; a parameter is what you can filter on. Most columns are both, but not all.
Both work offline: the schema is frozen into the package from the
APIs’ own OpenAPI documents. tg_schema_date() reports
when.
Retrieving rows
Each filter is named after a parameter, and parameters combine with AND:
propostas <- tg_get(
"parcerias", "proposta",
sg_uf_recebedor = "PE",
situacao_proposta = "Aprovada",
.limit = 100
)That is the entire filtering vocabulary. These services compare for equality and nothing else — there is no greater-than, no pattern match, no “is one of” — and they publish no ordering or column-selection parameter.
To filter on several values, query each and bind:
library(purrr)
nordeste <- c("PE", "PB", "AL", "RN", "CE", "SE", "BA", "PI", "MA")
propostas <- list_rbind(map(
nordeste,
\(uf) tg_get("parcerias", "proposta", sg_uf_recebedor = uf, .limit = Inf)
))Enumerated parameters
Many parameters accept only a fixed set of values, and the package knows which:
params <- tg_params("parcerias", "proposta")
params[lengths(params$values) > 0, c("param", "values")]
#> # A tibble: 5 × 2
#> param values
#> <chr> <list>
#> 1 sg_uf_recebedor <chr [27]>
#> 2 situacao_proposta <chr [5]>
#> …
params$values[[match("situacao_proposta", params$param)]]
#> [1] "Em Análise" "Rejeitada" "Aprovada" "Em Elaboração"
#> [5] "Inativada"A value outside the set fails before the request is made:
tg_count("parcerias", "proposta", situacao_proposta = "Aprovado")
#> Error in `tg_count()`:
#> ! "Aprovado" is not a permitted value for `situacao_proposta`.
#> ℹ Did you mean "Aprovada"?
#> ℹ It accepts "Em Análise", "Rejeitada", "Aprovada", "Em Elaboração", and
#> "Inativada".Why parameter names are checked
This is the one thing worth internalizing about these APIs.
They ignore a query parameter they do not recognize.
No warning, no 400 — the request succeeds and returns the unfiltered
table. The parameter on /proposta is
situacao_proposta; write in_situacao_proposta,
which is what the sibling /parceria endpoint calls its own
version, and you get every one of the 88,666 proposals instead of the
84,258 that are approved. Nothing in the response says so.
So the package refuses to send a name the frozen schema does not know:
tg_count("parcerias", "proposta", in_situacao_proposta = "Aprovada")
#> Error in `tg_count()`:
#> ! Unknown filter: "in_situacao_proposta".
#> ✖ The API ignores a parameter it does not recognize and returns every row, so
#> this would look like a query that matched nothing in particular.
#> ℹ Did you mean "situacao_proposta"?If the API gains a parameter after the packaged schema was built,
turn the check off with
options(transferegovr.validate = FALSE) — and know what you
are trading away.
For the same reason a repeated parameter is refused rather than sent:
these services keep the last occurrence and discard the rest silently,
so sg_uf_recebedor = "PE", sg_uf_recebedor = "PB" would
quietly mean "PB".
Types
Columns are typed from the frozen schema rather than inferred from the values, so a column that happens to be entirely null on one page does not come back logical while the next page returns it as character.
propostas <- tg_get("parcerias", "proposta", .limit = 5)
class(propostas$dt_proposta)
#> [1] "Date"
class(propostas$vl_total_planejamento_gastos)
#> [1] "numeric"Integers are returned as double. These documents declare no
format, so int32 and int64 cannot be told apart, and
identifiers here genuinely exceed .Machine$integer.max —
cd_parceria reaches 202500037062, which as an integer would
be NA.
List columns
Some tables have no endpoint of their own: the API folds them into their parent as an array. Those arrive as list columns.
programas <- tg_get("parcerias", "programa", .limit = 20)
fields <- tg_fields("parcerias", "programa")
fields$field[!is.na(fields$nested)]
#> [1] "ufs_habilitadas" "programa_atende_a" "categorias_despesa"
#> [4] "resultados_esperados" "indicadores_programa"
tg_fields("parcerias", "programa", nested = "ufs_habilitadas")
#> # A tibble: 3 × 5
#> field r_type api_type nested description
#> <chr> <chr> <chr> <chr> <chr>
#> 1 nm_uf character string NA NA
#> 2 sg_uf character string NA NA
#> 3 cd_ibge double integer NA NATo flatten one:
library(dplyr)
library(tidyr)
programas |>
select(id_programa, ufs_habilitadas) |>
unnest_longer(ufs_habilitadas) |>
unnest_wider(ufs_habilitadas)There are 5 such columns in fundoafundo and 13 in
parcerias; especiais has none.
How fresh is the data
Each module reports when it was last loaded. It is the only freshness
signal these APIs give — they send no ETag,
Cache-Control or Last-Modified header, which
is also why the package caches responses itself rather than relying on
HTTP caching.
tg_updated_at("parcerias")
#> [1] "2026-08-03 UTC"Where to go next
-
vignette("pagination")— collecting a large table without losing rows. -
vignette("joining-tables")— how the tables fit together.