# Cepis C# client for the [public CEP API at cep.is](https://cep.is). Official Correios data (DNE), refreshed every two weeks. ```csharp using Cepis; Address address = await CepisClient.Default.LookupAsync("01001-000"); Console.WriteLine($"{address.Logradouro} {address.Localidade} {address.UF}"); // Praça da Sé São Paulo SP ``` ## Installation Requires .NET 8 or newer. There are no runtime dependencies: HTTP comes from `System.Net.Http`, JSON from `System.Text.Json`, and the CSV reader behind the batch endpoint is in the library because the BCL has none. ```sh git clone https://git.cep.is/csharp.git cepis-csharp cd cepis-csharp make pack ``` That writes `artifacts/Cepis.0.2.0.nupkg`; add it from a local feed, or reference `src/Cepis/Cepis.csproj` directly. ## Usage ### A single lookup `LookupAsync` accepts the CEP masked or unmasked and returns an `Address`: ```csharp Address a = await client.LookupAsync("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" ``` The data fields keep the API's own Portuguese names, with only the casing adapted; a `JsonPropertyName` attribute maps each one to its wire name: | wire | property | wire | property | | --- | --- | --- | --- | | `cep` | `Cep` | `regiao` | `Regiao` | | `logradouro` | `Logradouro` | `ibge` | `Ibge` | | `complemento` | `Complemento` | `gia` | `Gia` | | `unidade` | `Unidade` | `ddd` | `Ddd` | | `bairro` | `Bairro` | `siafi` | `Siafi` | | `localidade` | `Localidade` | `geo.lat` / `geo.lon` | `Geo.Lat` / `Geo.Lon` | | `uf` | `UF` | `geo.raio_m` | `Geo.RaioM` | | `estado` | `Estado` | `geo.precisao` | `Geo.Precisao` | Fields the database does not have for that CEP come back as empty strings, never `null`. A field the API grows that this version does not model lands in `a.Extra`, so nothing is silently dropped. ### Coordinates About 57% of CEPs have a coordinate. When they do not, `Geo` is `null`: ```csharp if (a.Geo is { } geo) { geo.Lat; // -23.567202 geo.Lon; // -46.649659 geo.RaioM; // 1084 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"`: ```csharp decimal cost = a.Geo is { IsPrecise: true } geo ? ShippingFrom(geo.Lat, geo.Lon) : ShippingFromMunicipality(a.Ibge); ``` ### Many CEPs at once Do not loop over `LookupAsync`. Use `LookupManyAsync`, which resolves up to 500 CEPs per request: ```csharp string[] ceps = ["01001000", "99999999", "20040020"]; IReadOnlyList
result = await client.LookupManyAsync(ceps); for (int i = 0; i < ceps.Length; i++) { Console.WriteLine(result[i] is null ? $"{ceps[i]}: not found" : $"{ceps[i]}: {result[i]!.Localidade}"); } ``` The returned list has **the same length and the same order** as the input. A CEP that does not exist, or is malformed, becomes `null` at its position — it does not vanish, and it does not throw. Inputs 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 `LookupManyAsync` has `Geo == null`, > including for CEPs that do have one. Need the coordinate? Use `LookupAsync`, > one at a time. ### API health Useful for two things: checking the API answers, and knowing **which version of the database** is live: ```csharp Health h = await client.HealthAsync(); 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 `CepisClient` and reuse it. It keeps no state between calls, holds one pooled `HttpClient`, and is safe to share across threads: ```csharp using var client = new CepisClient(new CepisClientOptions { BaseUrl = "https://cep.is", // point at another instance Timeout = TimeSpan.FromSeconds(10), // per attempt, connection included MaxAttempts = 3, // counting the first; 1 disables retrying BackoffBase = TimeSpan.FromMilliseconds(500), // base of the exponential wait BackoffMax = TimeSpan.FromSeconds(8), // cap on each wait TotalTimeout = TimeSpan.FromSeconds(15), // cap on the whole call; unset by default }); Address address = await client.LookupAsync("01001000"); ``` Retries cover 429, 5xx and network failures, with exponential backoff and jitter; 400 and 404 are never retried. Every setting is documented on `CepisClientOptions`. `CepisClient.Default` is a shared client with the default configuration, built on first use. It applies process-wide, including to third-party libraries that also use this package, so do not dispose it; need a different configuration? Build your own. To take the `HttpClient` from `IHttpClientFactory` or a container, pass it in — it is then neither reconfigured nor disposed by this client: ```csharp var client = new CepisClient(httpClientFactory.CreateClient("cepis")); ``` ### 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`: ```csharp using var client = new CepisClient(new CepisClientOptions { BaseUrl = "http://localhost:8080", }); ``` ## Errors Everything derives from `CepisException`. 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; may carry `Faixa` | no | | `RateLimitedException` | 429; carries `RetryAfter` | yes, later | | `ServerErrorException` | 5xx; carries `Status` | yes, later | | `NetworkException` | DNS, connection, timeout, TLS | yes, later | | `InvalidResponseException` | unintelligible response | probably not | ```csharp try { Address address = await client.LookupAsync(cepFromForm); } catch (CepNotFoundException) { // ask the user for another CEP } catch (CepisException) { // network, server, rate limit: reschedule } ``` C# allows one base class, so these are not also `ArgumentException` or `KeyNotFoundException`: one base type that catches every failure is worth more. A cancellation you asked for is not a `CepisException` — it surfaces as `OperationCanceledException`, untouched. `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. ### A CEP that does not exist A 404 is still a 404: `LookupAsync` throws `CepNotFoundException` and there is no address. What the exception may add is `Faixa`, the municipality that CEP *would* have belonged to: ```csharp try { Address address = await client.LookupAsync("25845000"); } catch (CepNotFoundException e) when (e.Faixa is { } faixa) { faixa.Localidade; // "Areal" faixa.UF; // "RJ" faixa.Estado; // "Rio de Janeiro" faixa.Ibge; // "3300225" faixa.CepIni; // "25845001" faixa.CepFim; // "25849899" faixa.Ceps; // 118 // "we don't have that CEP — did you mean a street in Areal/RJ?" } ``` Worth handling, because a CEP that fails is rarely random: it is usually someone who knows the city and guessed the number. The classic guess is the `-000`, which 957 municipalities never received — `25845000` above is Areal's. Three things not to get wrong: - **It is context for an error, not an address.** Nothing in it says the CEP exists. Filling a form from these fields is inventing an address. - **`CepIni` and `CepFim` are what exists, not what the Correios publish.** The DNE's own range for Areal starts at `25845000` — the CEP that just failed. `CepIni` is `25845001`, the smallest one actually in the database. - **The range is sparse.** Areal's 118 CEPs are spread across the ~4,900 numbers between its two ends. That is what `Ceps` is for; never walk the interval, and never show the pair as a list of valid CEPs. `Faixa` is `null` whenever the API named no municipality: a CEP in a band nobody owns, an older API, or a block this client could not read. The API **omits** the field rather than sending a null, and an unreadable one is dropped instead of failing the lookup — a courtesy on top of a 404 must not turn a clean not-found into a parse error. Absence is the ordinary case; treat it as one. ## Development Everything runs through `make` and the .NET SDK: ```sh make # list the targets make test # run the suite against a local HTTP server make lint # dotnet format --verify-no-changes + build -warnaserror, touches nothing make format # apply dotnet format make build # build the solution make pack # the NuGet package into artifacts/ make check # lint, then test, then build ``` ## Licence ISC. See [LICENSE](LICENSE).