# cepis Go client for the [public CEP API at cep.is](https://cep.is). Official Correios data (DNE), refreshed every two weeks. No dependencies beyond the standard library. ```go addr, err := cepis.Lookup(ctx, "01001-000") fmt.Println(addr.Logradouro, addr.Localidade, addr.UF) // Praça da Sé São Paulo SP ``` ## Installation ```sh go get cep.is/go ``` Requires Go 1.21 or newer. ```go import cepis "cep.is/go" ``` `cep.is/go` is a vanity import path: `cep.is` serves a `go-import` meta tag that points the go tool at this repository, so the module path never names a forge and moving the code later would not break anyone's imports. ## Usage ### A single lookup `Lookup` accepts the CEP masked or unmasked and returns an `*Address`: ```go a, err := cepis.Lookup(ctx, "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.Field("name")` reads any field of the raw response, including one the API grew after this release. ### Coordinates About 57% of CEPs have a coordinate. When they do not, `Geo` is nil: ```go if a.Geo != nil { fmt.Println(a.Geo.Lat, a.Geo.Lon) // -23.567202 -46.649659 fmt.Println(a.Geo.RaioM) // 1084 fmt.Println(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.IsPrecise()` is the shorthand for `Precisao == "logradouro"`: ```go if a.Geo != nil && a.Geo.IsPrecise() { cost = shippingFrom(a.Geo.Lat, a.Geo.Lon) } else { cost = shippingFromMunicipality(a.IBGE) } ``` ### Many CEPs at once Do not loop over `Lookup`. Use `LookupMany`, which resolves up to 500 CEPs per request: ```go ceps := []string{"01001000", "99999999", "20040020"} result, err := cepis.LookupMany(ctx, ceps) for i, addr := range result { if addr == nil { fmt.Printf("%s: not found\n", ceps[i]) } else { fmt.Printf("%s: %s\n", ceps[i], addr.Localidade) } } ``` The result has **the same length and the same order** as the input. A CEP that does not exist, or is malformed, is nil at its position — it does not vanish, and it is not an error. Slices 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 nil `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: ```go h, err := cepis.CheckHealth(ctx) 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 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 share across goroutines: ```go client, err := cepis.New( cepis.WithBaseURL("https://cep.is"), // point at another instance cepis.WithTimeout(10*time.Second), // cap on one attempt cepis.WithMaxAttempts(3), // counting the first; 1 disables retrying cepis.WithBackoff(500*time.Millisecond, 8*time.Second), cepis.WithTotalTimeout(15*time.Second), // cap on the whole call cepis.WithUserAgent("myapp/1.0"), cepis.WithHTTPClient(&http.Client{}), ) addr, err := client.Lookup(ctx, "01001000") ``` `WithTimeout` bounds a single attempt — connection, response and body read together. A call may make several attempts with waits between them; the cap on all of that is `WithTotalTimeout`. A deadline on the context passed to a call is honoured too, and the earlier of the two wins. Retries cover 429, 5xx and network failures, with exponential backoff and full jitter; 400 and 404 are never retried. See the [package documentation](https://pkg.go.dev/cep.is/go) for each option. The package-level functions (`cepis.Lookup`, etc.) use a shared default client, reachable as `cepis.Default()`. It applies process-wide, third-party libraries included. Need a different configuration? Build your own. ### Pointing at a local instance `WithBaseURL` is the only setting that usually changes between environments. The API repo brings a local instance up with `just up`: ```go client, err := cepis.New(cepis.WithBaseURL("http://localhost:8080")) ``` ## Errors Every error matches `cepis.Err`. 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? | | --- | --- | --- | | `ErrInvalidCEP` | not 8 digits | no | | `ErrCEPNotFound` | well-formed, not in the database | no | | `ErrRateLimited` | 429 | yes, later | | `ErrServer` | 5xx | yes, later | | `ErrNetwork` | DNS, connection, timeout, TLS | yes, later | | `ErrInvalidResponse` | unintelligible response | probably not | | `ErrInvalidConfig` | a bad option given to `New` | no | ```go addr, err := cepis.Lookup(ctx, cepFromForm) switch { case errors.Is(err, cepis.ErrCEPNotFound): // ask the user for another CEP case errors.Is(err, cepis.Err): // transient failure: reschedule } ``` The concrete type behind every one of them is `*cepis.Error`, which carries the CEP, the HTTP status and the server's `Retry-After`: ```go var cerr *cepis.Error if errors.As(err, &cerr) && errors.Is(cerr, cepis.ErrRateLimited) { reschedule(cerr.RetryAfter) } ``` `ErrInvalidResponse` 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. ### Which city the missing CEP was from A CEP that fails is rarely a random number — it is usually someone who knows the city and guessed the rest. The classic guess is the `-000`: a small town often has a single city-wide CEP ending that way, but 957 municipalities are coded street by street and never got one. Areal/RJ answers from `25845-001` up, never at `25845-000`. When the API can name the municipality the number would fall in, it comes back on the error: ```go _, err := cepis.Lookup(ctx, "25845-000") var cerr *cepis.Error if errors.As(err, &cerr) && cerr.Faixa != nil { fmt.Printf("%s/%s has %d CEPs, from %s to %s\n", cerr.Faixa.Localidade, cerr.Faixa.UF, cerr.Faixa.CEPs, cerr.Faixa.CEPIni, cerr.Faixa.CEPFim) // Areal/RJ has 118 CEPs, from 25845001 to 25849899 } ``` `CEPIni` and `CEPFim` are the smallest and largest CEP that **exist** there, not the bounds the Correios publish — the published start is the `-000` that just failed. The range is sparse, which is what `CEPs` is for: those 118 are scattered across nearly five thousand numbers, so do not read the pair as a list of valid CEPs. `Faixa` is `nil` whenever the API did not send one, including for a CEP in a band that belongs to no municipality at all. It is an extra on top of an error that is complete without it. ## Development Everything runs through `make`: ```sh make # list the targets make test # run the suite make test-race # run it under the race detector make lint # gofmt -l + go vet, touches nothing make build # compile every package make check # lint, then test, then build ``` ## Licence ISC. See [LICENSE](LICENSE).