# cepis D client for the [public CEP API at cep.is](https://cep.is). Official Correios data (DNE), refreshed every two weeks. ```d import cepis; auto address = lookup("01001-000"); writeln(address.logradouro, " ", address.localidade, " ", address.uf); // Praça da Sé São Paulo SP ``` ## Installation ```sh dub add cepis ``` Or in `dub.json`: ```json "dependencies": { "cepis": "~>0.2.0" } ``` No dub dependencies: HTTP comes from `std.net.curl`, JSON from `std.json`, CSV from `std.csv`. `std.net.curl` needs **libcurl at runtime** (`libcurl.so.4` on Linux, `libcurl.dll` on Windows) and `libcurl` at link time — most systems already ship it; on Debian and derivatives that is `libcurl4-openssl-dev`. ## Usage ### A single lookup `lookup` accepts the CEP masked or unmasked and returns an `Address`: ```d auto a = lookup("01310-100"); a.logradouro; // "Avenida Paulista" a.localidade; // "São Paulo" a.uf; // "SP" a.ibge; // "3550308" a.cep; // "01310100" (always unmasked) a.formattedCep; // "01310-100" ``` Fields the database does not have for that CEP come back as empty strings. `a.raw` holds the response exactly as it arrived, so a field the API grows before this library is updated is not lost. ### Coordinates About 57% of CEPs have a coordinate. When they do not, `geo` is null: ```d if (!a.geo.isNull) { auto geo = a.geo.get; writeln(geo.lat, " ", geo.lon); // -23.567202 -46.649659 writeln(geo.raio_m); // 1084 writeln(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 | `geo.isPrecise` is the shorthand for `precisao == "logradouro"`: ```d auto cost = !a.geo.isNull && a.geo.get.isPrecise ? shippingFrom(a.geo.get.lat, a.geo.get.lon) : shippingFromMunicipality(a.ibge); ``` ### Many CEPs at once Do not loop over `lookup`. Use `lookupMany`, which resolves up to 500 CEPs per request: ```d auto ceps = ["01001000", "99999999", "20040020"]; foreach (index, address; lookupMany(ceps)) writeln(ceps[index], ": ", address.isNull ? "not found" : address.get.localidade); ``` The returned array 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 throw. Arrays 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 `lookupMany` has a null `geo`, > including for CEPs that do have one. Need `geo`? Use `lookup`, one at a time. ### API health Useful for two things: checking the API answers, and knowing **which version of the database** is live: ```d auto h = 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 is not an error — that arrives in `h.status`. ## Configuration For anything beyond the defaults, build a `Client` and reuse it. It keeps no state between calls and is safe to copy and share across threads: ```d ClientConfig config; config.baseUrl = "https://cep.is"; // point at another instance config.timeout = dur!"seconds"(10); // per socket operation config.maxAttempts = 3; // counting the first; 1 disables retrying config.backoffBase = dur!"msecs"(500); // base of the exponential wait config.backoffMax = dur!"seconds"(8); // cap on each wait config.totalTimeout = dur!"seconds"(30); // cap on the whole call config.userAgent = "my-app/1.0"; auto client = Client(config); auto address = client.lookup("01001000"); ``` Retries cover 429, 5xx and network failures, with exponential backoff and jitter; 400 and 404 are never retried. The module-level functions (`lookup`, `lookupMany`, `health`) share one default client per thread, built on first use. Need a different configuration? Build your own. ### Pointing at a local instance `baseUrl` is the only setting that usually changes between environments. The API repo brings a local instance up with `just up`: ```d ClientConfig config; config.baseUrl = "http://localhost:8080"; auto client = Client(config); ``` ## Errors Everything derives from `CepisException`, so one `catch` handles any failure. Below that, the split is by **the decision you have to make**, not by the HTTP status that caused it: | exception | when | does retrying help? | | --- | --- | --- | | `InvalidCepException` | not 8 digits | no | | `CepNotFoundException` | well-formed, not in the database; may carry `faixa` | no | | `RateLimitedException` | 429; carries `retryAfter` | yes, later | | `ServerException` | 5xx; carries `status` | yes, later | | `NetworkException` | DNS, connection, timeout, TLS | yes, later | | `InvalidResponseException` | unintelligible response | probably not | ```d try render(lookup(cepFromForm)); catch (CepNotFoundException) askForAnotherCep(); catch (CepisException error) { if (error.retriable) reschedule(); else stderr.writeln(error.msg); } ``` `InvalidResponseException` 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. ### When the CEP does not exist A 404 sometimes knows *where* the CEP would have been. `CepNotFoundException` carries it in `faixa`: the municipality that owns the range the number falls in. ```d try render(lookup("25845-000")); catch (CepNotFoundException error) { if (error.faixa.isNull) askForAnotherCep(); else { auto f = error.faixa.get; writeln("no such CEP — that range is ", f.localidade, "/", f.uf); writeln(f.ceps, " CEPs, from ", f.cep_ini, " to ", f.cep_fim); // 118 CEPs, from 25845001 to 25849899 } } ``` It is **context for an error, not an address.** The status is still 404 and the CEP still does not exist; `faixa` never becomes an `Address` and must not be rendered as if the lookup had worked. `cep_ini` and `cep_fim` are the smallest and the largest CEP that **actually exist** there — not the bounds the Correios publish. The DNE's own start for Areal is `25845000`, which is exactly the CEP that just failed. The range is **sparse**: Areal's 118 CEPs are scattered across the ~4,900 numbers between `25845001` and `25849899`. That is what `ceps` is for. Never present the pair as the list of valid CEPs for the city, and do not walk it expecting the numbers in between to resolve. `faixa` is null whenever the API sent none — and it is **omitted, never null**, when the CEP falls in a band no municipality owns. Why it exists: a CEP that fails is rarely a random number, it is someone who knows the city and guessed the digits — and the classic guess is the `-000` that 957 municipalities never received. ## Development Everything runs through `make`, which shells out to `dub`: ```sh make # list the targets make test # dub test make lint # dscanner + a dfmt check, touches nothing make format # apply dfmt (writes) make build # release build make check # lint, then test, then build ``` ## Licence ISC. See [LICENSE](LICENSE).