# cepis OCaml client for the [public CEP API at cep.is](https://cep.is). Official Correios data (DNE), refreshed every two weeks. ```ocaml match Cepis.lookup "01001-000" with | Ok a -> Printf.printf "%s %s %s\n" a.Cepis.logradouro a.Cepis.localidade a.Cepis.uf (* Praça da Sé São Paulo SP *) | Error e -> prerr_endline (Cepis.string_of_error e) ``` ## Installation ```sh opam pin add cepis git+https://git.cep.is/ocaml.git ``` Requires OCaml 4.14 or newer. Three dependencies, each because the standard library has no equivalent: [`ezcurl`](https://github.com/c-cube/ezcurl) for HTTP, because it is synchronous and does not force an async runtime on callers; [`yojson`](https://github.com/ocaml-community/yojson) for JSON, the conventional choice; and [`csv`](https://github.com/Chris00/ocaml-csv) for the batch endpoint, whose values may be quoted and contain commas. `ezcurl` binds libcurl, so its development headers must be present; opam installs them for you on most systems. ## Usage ### A single lookup `lookup` accepts the CEP masked or unmasked and returns an `address`: ```ocaml let a = Result.get_ok (Cepis.lookup "01310-100") in a.Cepis.logradouro (* "Avenida Paulista" *) a.Cepis.localidade (* "São Paulo" *) a.Cepis.uf (* "SP" *) a.Cepis.ibge (* "3550308" *) a.Cepis.cep (* "01310100" (always unmasked) *) Cepis.formatted_cep a (* "01310-100" *) ``` Fields the database does not have for that CEP come back as empty strings. ### Coordinates About 57% of CEPs have a coordinate. When they do not, `geo` is `None`: ```ocaml match a.Cepis.geo with | Some g -> Printf.printf "%f %f %d %s\n" g.Cepis.lat g.Cepis.lon g.Cepis.raio_m g.Cepis.precisao | None -> () ``` **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 | `Cepis.is_precise` is the shorthand for `precisao = "logradouro"`: ```ocaml match a.Cepis.geo with | Some g when Cepis.is_precise g -> shipping_from g.Cepis.lat g.Cepis.lon | _ -> shipping_from_municipality a.Cepis.ibge ``` ### Many CEPs at once Do not loop over `lookup`. Use `lookup_many`, which resolves up to 500 CEPs per request: ```ocaml let ceps = [ "01001000"; "99999999"; "20040020" ] in match Cepis.lookup_many ceps with | Ok rows -> List.iter2 (fun cep -> function | None -> Printf.printf "%s: not found\n" cep | Some a -> Printf.printf "%s: %s\n" cep a.Cepis.localidade) ceps rows | Error e -> prerr_endline (Cepis.string_of_error e) ``` The list returned has **the same length and the same order** as the input. A CEP that does not exist, or is malformed, becomes `None` at its position — it does not vanish, and it does not fail the call. 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 `lookup_many` has `geo = None`, > 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: ```ocaml let h = Result.get_ok (Cepis.health ()) in Cepis.is_ok h (* true *) h.Cepis.dne (* "26071" identifier of the fortnightly database *) h.Cepis.ceps (* 1596168 *) h.Cepis.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 `Cepis.Client.t` and reuse it. It keeps no state between calls: ```ocaml let client = Cepis.Client.create ~base_url:"https://cep.is" (* point at another instance *) ~timeout:10.0 (* seconds for one attempt, connection included *) ~max_attempts:3 (* counting the first; 1 disables retrying *) ~backoff_base:0.5 (* base of the exponential wait *) ~backoff_max:8.0 (* cap on each wait *) ~total_timeout:15.0 (* cap on the whole call, attempts included *) () let address = Cepis.Client.lookup client "01001000" ``` Retries cover 429, 5xx and network failures, with exponential backoff and jitter; 400 and 404 are never retried. See `Cepis.Client.create` in the interface for the details of each parameter. The module-level functions (`Cepis.lookup`, etc.) share a default client built with the defaults. Need a different configuration? Build your own. ### 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`: ```ocaml let client = Cepis.Client.create ~base_url:"http://localhost:8080" () ``` ## Errors Expected failures come back as `('a, Cepis.error) result`; only a nonsensical configuration raises, as `Invalid_argument`. The variants split by **the decision you have to make**, not by the HTTP status that caused it: | error | when | does retrying help? | | --- | --- | --- | | `Invalid_cep` | not 8 digits | no | | `Cep_not_found` | well-formed, not in the database | no | | `Rate_limited` | 429; carries `retry_after` | yes, later | | `Server_error` | 5xx; carries `status` | yes, later | | `Network_error` | DNS, connection, timeout, TLS | yes, later | | `Invalid_response` | unintelligible response | probably not | ```ocaml match Cepis.lookup cep_from_form with | Ok address -> use address | Error (Cepis.Cep_not_found _) -> ask_for_another_cep () | Error _ -> reschedule () ``` `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. ## Development ```sh make # list the targets make test # run the suite against a local HTTP server make lint # ocamlformat check, touches nothing make build # build the library make check # lint, then test, then build make doc # odoc documentation ``` ## Licence ISC. See [LICENSE](LICENSE).