# cepis R client for the [public CEP API at cep.is](https://cep.is). Official Correios data (DNE), refreshed every two weeks. ```r library(cepis) address <- cep_lookup("01001-000") cat(address$logradouro, address$localidade, address$uf) #> Praça da Sé São Paulo SP ``` ## Installation ```r remotes::install_git("https://git.cep.is/r.git") ``` Requires R 4.0 or newer, plus two packages: **curl**, because base R cannot report a response status code and this client has to tell a 404 from a 400 from a 5xx, and **jsonlite** to read the JSON. Both are the ecosystem standard and neither pulls a stack behind it. CSV is parsed with `utils::read.csv()`. ## Usage ### A single lookup `cep_lookup()` accepts the CEP masked or unmasked and returns a `cepis_address`, a plain list: ```r a <- cep_lookup("01310-100") a$logradouro # "Avenida Paulista" a$localidade # "São Paulo" a$uf # "SP" a$ibge # "3550308" a$cep # "01310100" (always unmasked) formatted_cep(a) # "01310-100" ``` Fields the database does not have for that CEP come back as empty strings, never `NA`. `a$raw` holds the response as it arrived, including any field the API grew after this release. ### Coordinates About 57% of CEPs have a coordinate. When they do not, `geo` is `NULL`: ```r a <- cep_lookup("01310100") if (!is.null(a$geo)) { a$geo$lat # -23.567202 a$geo$lon # -46.649659 a$geo$raio_m # 1084 a$geo$precisao # "logradouro" } ``` **Check `precisao` before computing distance.** There are two kinds of point, and the difference between them is tens of kilometres: | `precisao` | typical radius | what the point is | | --- | --- | --- | | `logradouro` | 110–140 m | the stretch of street — good for maps and distance | | `localidade` | 7–40 km | the municipality centroid: the CEP *is* the whole city | `a$geo$is_precise` is the shorthand for `precisao == "logradouro"`: ```r if (!is.null(a$geo) && a$geo$is_precise) { cost <- shipping_from(a$geo$lat, a$geo$lon) } else { cost <- shipping_from_municipality(a$ibge) } ``` ### Many CEPs at once Do not loop over `cep_lookup()`. Use `cep_lookup_many()`, which resolves up to 500 CEPs per request: ```r ceps <- c("01001000", "99999999", "20040020") result <- cep_lookup_many(ceps) for (i in seq_along(ceps)) { if (is.null(result[[i]])) { cat(ceps[i], "not found\n") } else { cat(ceps[i], result[[i]]$localidade, "\n") } } ``` The returned list has **the same length and the same order** as the input. A CEP that does not exist, or is malformed, is `NULL` at its position — it does not vanish, and it does not raise. A list rather than a `data.frame` because `NULL` is the only unambiguous "no such CEP" in R: a row of `NA`s reads as an address with unknown fields, which is a different statement. Vectors longer than 500 are chunked automatically. > **Batches carry no coordinate.** The endpoint behind them is CSV, and the > CSV has no `geo` column. Every address from `cep_lookup_many()` has > `geo = NULL`, including for CEPs that do have one. Need `geo`? Use > `cep_lookup()`, one at a time. ### API health Useful for two things: checking the API answers, and knowing **which version of the database** is live: ```r h <- cep_health() h$ok # TRUE h$dne # "26071" identifier of the fortnightly database h$ceps # 1596168 h$base_gerada_em # "2026-07-24T03:48:28Z" ``` An API reporting itself degraded does not raise — that arrives in `h$status`. ## Configuration For anything beyond the defaults, build a client with `cepis_client()` and pass it along. It holds configuration only, keeps no state between calls and is safe to reuse: ```r client <- cepis_client( base_url = "https://cep.is", # point at another instance timeout = 10, # seconds for one attempt max_attempts = 3L, # counting the first; 1 disables retrying backoff_base = 0.5, # base of the exponential wait backoff_max = 8, # cap on each wait total_timeout = NULL # cap on the whole call, attempts included ) address <- cep_lookup("01001000", client) ``` Retries cover 429, 5xx and network failures, with exponential backoff and jitter; 400 and 404 are never retried. See `?cepis_client` for the details of each parameter. Omitting `client` uses a shared default one, built with the defaults above. ### Pointing at a local instance `base_url` is the only parameter that usually changes between environments. The API repo brings a local instance up with `just up`: ```r client <- cepis_client(base_url = "http://localhost:8080") ``` ## Errors Failures are classed conditions. Everything inherits `cepis_error` (and `error`), so one handler catches all of them. Below that, the split is by **the decision you have to make**, not by the HTTP status that caused it: | class | when | does retrying help? | | --- | --- | --- | | `cepis_invalid_cep` | not 8 digits | no | | `cepis_not_found` | well-formed, not in the database; carries `faixa` | no | | `cepis_rate_limited` | 429; carries `retry_after` | yes, later | | `cepis_server_error` | 5xx; carries `status` | yes, later | | `cepis_network_error` | DNS, connection, timeout, TLS | yes, later | | `cepis_invalid_response` | unintelligible response | probably not | ```r tryCatch( cep_lookup(cep_from_form), cepis_not_found = function(e) NULL, # ask the user for another CEP cepis_error = function(e) reschedule(e) # transient failure: retry later ) ``` `cepis_invalid_response` almost always means something between you and the API is answering instead of it — a captive Wi-Fi portal, a corporate proxy, an HTML error page. Useful as a diagnosis: the problem is not in your code. ### Where a CEP that does not exist would have been A CEP that fails is rarely random: it is usually someone who knows the city and guessed the number, and the classic guess is the `-000` that 957 municipalities never received. When the number falls inside the range of a municipality, `cepis_not_found` carries a `faixa` naming it: ```r tryCatch( cep_lookup("25845000"), cepis_not_found = function(e) { if (is.null(e$faixa)) { "no such CEP" } else { paste0("no such CEP in ", e$faixa$localidade, "/", e$faixa$uf) } } ) #> "no such CEP in Areal/RJ" ``` The lookup **still failed**, and a `faixa` is not an address — it is context for the error, and answers "did you mean somewhere in Areal?" instead of "invalid CEP". The condition is the same `cepis_not_found` as before; code that ignores `faixa` keeps working. ```r f <- e$faixa f$localidade # "Areal" f$uf # "RJ" f$estado # "Rio de Janeiro" f$regiao # "Sudeste" f$ibge # "3300225" f$cep_ini # "25845001" f$cep_fim # "25849899" f$ceps # 118 ``` Two things about `cep_ini` and `cep_fim`: - They are the smallest and the largest CEP that **actually exist** there, not the range the Correios publish. The DNE's own start for Areal is `25845000` — exactly the CEP that just failed above. - **The range is sparse.** Areal's 118 CEPs are scattered over the ~4900 numbers between the two endpoints. `ceps` is there so that is visible: never present the pair as a list of valid CEPs, and never iterate from one to the other. `faixa` is `NULL` when the CEP falls in a band no municipality owns. The API omits the field rather than sending null, so `is.null(e$faixa)` says the number is close to nothing, not that the fields came back blank. Batches do not report it either: `cep_lookup_many()` still gives `NULL` at that position, because the endpoint behind it is CSV. ## Development Everything runs through `make`: ```sh make # list the targets make test # install into check/lib and run the suite make lint # parse + codetools, touches nothing make document # regenerate man/ and NAMESPACE with roxygen2 make build # build the source tarball make check # lint, then test, then build make check-cran # build, then R CMD check the tarball ``` The suite stands up a real HTTP server on loopback and points `base_url` at it, so no test touches the network. ## Licence ISC. See [LICENSE](LICENSE).