# cepis Dart client for the [public CEP API at cep.is](https://cep.is). Official Correios data (DNE), refreshed every two weeks. Pure Dart, so the same code runs on the VM, in AOT binaries and everywhere Flutter runs — Android, iOS, desktop and Web. ```dart import 'package:cepis/cepis.dart' as cepis; final address = await cepis.lookup('01001-000'); print('${address.logradouro} ${address.localidade} ${address.uf}'); // Praça da Sé São Paulo SP ``` ## Installation ```sh dart pub add cepis # or, in a Flutter app: flutter pub add cepis ``` Requires Dart 3.6 or newer. One dependency, `package:http`: the SDK's own `HttpClient` does not exist on Flutter Web, and a client that breaks on one of Flutter's platforms is not a Flutter client. ## Usage ### A single lookup `lookup` accepts the CEP masked or unmasked and resolves to an `Address`: ```dart final a = await cepis.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, never `null`. `a.raw` reads the response as it arrived, including a field the API grew after this release. The data fields keep the API's own Portuguese names; everything else — methods, types, errors — is English. Only the casing changes, where Dart demands it: the API's `raio_m` is `geo.raioM` and its `base_gerada_em` is `health.baseGeradaEm`. ### Coordinates About 57% of CEPs have a coordinate. When they do not, `geo` is `null`: ```dart final a = await cepis.lookup('01310100'); final geo = a.geo; if (geo != null) { geo.lat; // -23.567202 geo.lon; // -46.649659 geo.raioM; // 1084 (the API's `raio_m`) 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'`: ```dart final geo = a.geo; final cost = geo != null && geo.isPrecise ? shippingFrom(geo.lat, geo.lon) : shippingFromMunicipality(a.ibge); ``` ### Many CEPs at once Do not loop over `lookup`. Use `lookupMany`, which resolves up to 500 CEPs per request: ```dart final ceps = ['01001000', '99999999', '20040020']; final result = await cepis.lookupMany(ceps); for (var i = 0; i < ceps.length; i++) { final address = result[i]; print('${ceps[i]}: ${address == null ? 'not found' : address.localidade}'); } ``` 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 throw. Lists 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: ```dart final h = await cepis.health(); h.ok; // true h.dne; // '26071' identifier of the fortnightly database h.ceps; // 1596168 h.baseGeradaEm; // '2026-07-24T03:48:28Z' ``` An API reporting itself degraded does not throw — that arrives in `h.status`. ## Configuration For anything beyond the defaults, build a `Client` and reuse it. It keeps no state between calls: ```dart final client = cepis.Client( baseUrl: 'https://cep.is', // point at another instance timeout: const Duration(seconds: 10), // per attempt, body read included maxAttempts: 3, // counting the first; 1 disables retrying backoffBase: const Duration(milliseconds: 500), backoffMax: const Duration(seconds: 8), // cap on each wait totalTimeout: const Duration(seconds: 15), // cap on the whole call ); final address = await client.lookup('01001000'); client.close(); ``` `timeout` bounds a single attempt — request and body read together. The cap on all the attempts and the waits between them is `totalTimeout`. Retries cover 429, 5xx and network failures, with exponential backoff and full jitter; 400 and 404 are never retried. Every snippet here assumes the `import ... as cepis;` above; a plain `import 'package:cepis/cepis.dart';` puts `Client`, `Address` and the rest in scope unprefixed. `Client` also takes an `httpClient`, for sharing a connection pool or for a platform-native transport such as `cupertino_http` or `cronet_http`. A client passed that way is yours to close; `close()` leaves it alone. The top-level functions (`cepis.lookup`, `cepis.lookupMany`, `cepis.health`) use a shared default client, reachable as `cepis.defaultClient`. It applies process-wide, third-party packages included. 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`: ```dart final client = cepis.Client(baseUrl: 'http://localhost:8080'); ``` From the command line, to confirm quickly that it answers: ```sh dart run cepis --base-url http://localhost:8080 01001000 ``` ## Errors Everything derives from `CepisException`, which is `sealed` — a `switch` over it is checked for exhaustiveness. Below that, the split is by **the decision you have to make**, not by the HTTP status that caused it: | error | when | does retrying help? | | --- | --- | --- | | `InvalidCepException` | not 8 digits | no | | `CepNotFoundException` | well-formed, not in the database | no | | `RateLimitedException` | 429 | yes, later | | `ServerException` | 5xx | yes, later | | `NetworkException` | DNS, connection, timeout, TLS | yes, later | | `InvalidResponseException` | unintelligible response | probably not | ```dart try { final address = await cepis.lookup(cepFromForm); } on cepis.CepNotFoundException { // ask the user for another CEP } on cepis.CepisException { // transient failure: reschedule } ``` `RateLimitedException` exposes `retryAfter`, `ServerException` exposes `status`, and `NetworkException` exposes `cause`. ### When the CEP does not exist A failing CEP is rarely random: it is usually someone who knows the city and guessed the number — and the classic guess is the `-000`, which 957 municipalities never received. So `CepNotFoundException` may carry a `faixa` naming the municipality the CEP *would* have belonged to: ```dart try { final address = await cepis.lookup('25845-000'); } on cepis.CepNotFoundException catch (e) { final faixa = e.faixa; if (faixa != null) { faixa.localidade; // 'Areal' faixa.uf; // 'RJ' faixa.ibge; // '3300225' faixa.cepIni; // '25845001' (the API's `cep_ini`) faixa.cepFim; // '25849899' (the API's `cep_fim`) faixa.ceps; // 118 } } ``` Three things this is not: - **Not an address.** The response is still a 404 and the CEP still does not exist. `faixa` is context for the error — enough to say "did you mean somewhere in Areal/RJ?" — and never a value to save as the user's address. - **Not the Correios' published range.** `cepIni` and `cepFim` are the smallest and the largest CEP that *actually exist* in the municipality. The DNE's own start for Areal is `25845000` — exactly the CEP that just failed, which is why echoing it back would be useless. - **Not a range of valid CEPs.** It is sparse: Areal's 118 CEPs are scattered over the ~9000 numbers between the two bounds. That is what `ceps` is for. Do not iterate the interval and do not offer it as a list of alternatives. `faixa` is `null` when the CEP falls in a numeric band no municipality owns — the API **omits** the block there rather than sending null — and also whenever the block cannot be read. A malformed one never costs you the clean not-found. `e.toString()` mentions the municipality when there is one. `InvalidCepException` also implements `FormatException`, so code already handling malformed input during validation keeps working without knowing this library exists. `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. ## Development Everything runs through `make`, which shells out to `dart`: ```sh make # list the targets make test # dart test make lint # dart format --set-exit-if-changed + dart analyze, touches nothing make format # apply dart format make build # compile the CLI to build/cepis make check # lint, then test, then build ``` The suite stands a real `HttpServer` up on a loopback port and points `baseUrl` at it: no network, and nothing about the HTTP layer is stubbed. ## Licence ISC. See [LICENSE](LICENSE).