# cepis C client for the [public CEP API at cep.is](https://cep.is). Official Correios data (DNE), refreshed every two weeks. ```c #include #include int main(void) { cepis_config_t config; cepis_client_t *client; cepis_address_t *address; cepis_error_t err; cepis_config_init(&config); if (cepis_client_new(&config, &client, &err) != CEPIS_OK) return 1; if (cepis_lookup(client, "01001-000", &address, &err) == CEPIS_OK) { printf("%s %s %s\n", address->logradouro, address->localidade, address->uf); cepis_address_free(address); } cepis_client_free(client); return 0; } ``` ## Installation Requires a C11 compiler and [libcurl](https://curl.se/libcurl/) with its headers (`libcurl-dev` / `libcurl-devel` / `curl`). ```sh git clone https://git.cep.is/c.git cepis-c cd cepis-c make build sudo make install # PREFIX=/usr/local by default ``` > **0.2.0 changed `sizeof(cepis_error_t)`** — it grew a `faixa` member. The > source API is unchanged and existing code compiles untouched, but a program > built against 0.1.0 must be **recompiled**, not just relinked: it declares > the struct on its own stack at the old size, and the new library would write > past the end of it. The soname went from `libcepis.so.0` to `libcepis.so.1` > so the loader refuses that pairing instead of corrupting it silently. Then compile against it: ```sh cc myprogram.c -lcepis -lcurl -lm -o myprogram ``` Two dependencies, both deliberate: **libcurl**, because C has no standard HTTP client and libcurl is the one every platform already ships; and no JSON or CSV library at all — both parsers are hand-written here (`src/json.c`, `src/csv.c`, about 700 lines together) because the response shapes are small and fixed, and vendoring a general-purpose parser would add more code than it removes. ## Usage ### A single lookup `cepis_lookup` accepts the CEP masked or unmasked and fills a `cepis_address_t *` that you release with `cepis_address_free`: ```c cepis_address_t *a; cepis_lookup(client, "01310-100", &a, &err); a->logradouro; /* "Avenida Paulista" */ a->localidade; /* "São Paulo" */ a->uf; /* "SP" */ a->ibge; /* "3550308" */ a->cep; /* "01310100" (always unmasked) */ ``` Fields the database does not have for that CEP come back as the empty string, never `NULL`. For the masked form: ```c char masked[CEPIS_CEP_LEN + 2]; cepis_format_cep(a->cep, masked); /* "01310-100" */ ``` ### Coordinates About 57% of CEPs have a coordinate. When they do not, `geo` is `NULL`: ```c if (a->geo != NULL) { printf("%f %f\n", a->geo->lat, a->geo->lon); /* -23.567 -46.649 */ printf("%ld\n", a->geo->raio_m); /* 1084 */ printf("%s\n", 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 | `cepis_geo_is_precise()` is the shorthand for `precisao == "logradouro"`: ```c if (cepis_geo_is_precise(a->geo)) cost = shipping_from(a->geo->lat, a->geo->lon); else cost = shipping_from_municipality(a->ibge); ``` ### Many CEPs at once Do not loop over `cepis_lookup`. Use `cepis_lookup_many`, which resolves up to 500 CEPs per request: ```c const char *ceps[] = {"01001000", "99999999", "20040020"}; cepis_address_list_t list; size_t i; if (cepis_lookup_many(client, ceps, 3, &list, &err) == CEPIS_OK) { for (i = 0; i < list.count; i++) { if (list.items[i] == NULL) printf("%s: not found\n", ceps[i]); else printf("%s: %s\n", ceps[i], list.items[i]->localidade); } cepis_address_list_free(&list); } ``` The 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 is not an error. 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 `cepis_lookup_many` has > `geo == NULL`, including for CEPs that do have one. Need `geo`? Use > `cepis_lookup`, one at a time. ### API health Useful for two things: checking the API answers, and knowing **which version of the database** is live: ```c cepis_health_t *h; cepis_health(client, &h, &err); cepis_health_ok(h); /* true */ h->dne; /* "26071" identifier of the fortnightly database */ h->ceps; /* 1596168 */ h->base_gerada_em; /* "2026-07-24T03:48:28Z" */ cepis_health_free(h); ``` An API reporting itself degraded is not an error — that arrives in `h->status`. ### Memory Every function that hands back a pointer hands back ownership of it: | from | released by | | --- | --- | | `cepis_client_new` | `cepis_client_free` | | `cepis_lookup` | `cepis_address_free` | | `cepis_lookup_many` | `cepis_address_list_free` | | `cepis_health` | `cepis_health_free` | Every `char *` inside those structs is owned by the struct and freed with it; copy the bytes if you need them longer. Strings passed *into* the library are borrowed for the call only. A `cepis_error_t` owns nothing and can live on the stack. ## Configuration Fill a `cepis_config_t` with `cepis_config_init` and override what you need. Reuse one client per thread; a client is not safe to share between threads: ```c cepis_config_t config; cepis_config_init(&config); config.base_url = "https://cep.is"; /* point at another instance */ config.timeout = 10.0; /* seconds per socket operation */ config.max_attempts = 3; /* counting the first; 1 disables retry */ config.backoff_base = 0.5; /* base of the exponential wait */ config.backoff_max = 8.0; /* cap on each wait */ config.total_timeout = 0.0; /* cap on the whole call; 0 = none */ config.user_agent = NULL; /* NULL uses cepis-c/0.2.0 */ cepis_client_new(&config, &client, &err); ``` Retries cover 429, 5xx and network failures, with exponential backoff and jitter; 400 and 404 are never retried. `timeout` caps the connect phase and aborts a transfer that stalls for that long — it is not a cap on the whole call, which is what `total_timeout` is for. See `include/cepis.h` for the details of each member. ### Pointing at a local instance `base_url` is the only member that usually changes between environments. The API repo brings a local instance up with `just up`: ```c config.base_url = "http://localhost:8080"; ``` From the command line, to confirm quickly that it answers: ```sh cepis --base-url http://localhost:8080 01001000 ``` ## Errors Every fallible call returns a `cepis_status_t` and fills an optional `cepis_error_t` with the detail. The split is by **the decision you have to make**, not by the HTTP status that caused it: | status | when | does retrying help? | | --- | --- | --- | | `CEPIS_ERR_INVALID_CEP` | not 8 digits, or a 400 from the API | no | | `CEPIS_ERR_NOT_FOUND` | well-formed, not in the database | no | | `CEPIS_ERR_RATE_LIMITED` | 429; see `err.retry_after` | yes, later | | `CEPIS_ERR_SERVER` | 5xx; see `err.http_status` | yes, later | | `CEPIS_ERR_NETWORK` | DNS, connection, timeout, TLS | yes, later | | `CEPIS_ERR_INVALID_RESPONSE` | unintelligible response | probably not | | `CEPIS_ERR_CONFIG` | a configuration value out of range | no | | `CEPIS_ERR_NOMEM` | allocation failed | no | ```c switch (cepis_lookup(client, cep_from_form, &address, &err)) { case CEPIS_OK: break; case CEPIS_ERR_NOT_FOUND: /* ask the user for another CEP */ break; default: /* transient failure: reschedule */ fprintf(stderr, "%s: %s\n", cepis_status_str(err.status), err.message); } ``` `CEPIS_ERR_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. ### A CEP that does not exist A `CEPIS_ERR_NOT_FOUND` often knows more than "no". A CEP that fails is rarely a random number — it is usually someone who knows the city and guessed the last digits, and the classic guess is the `-000` that 957 municipalities never received. So the error can name the municipality the CEP *would* have belonged to: ```c const cepis_faixa_t *faixa; /* Still CEPIS_ERR_NOT_FOUND, still a 404. */ cepis_lookup(client, "25845-000", &address, &err); faixa = cepis_error_faixa(&err); if (faixa != NULL) { faixa->localidade; /* "Areal" */ faixa->uf; /* "RJ" */ faixa->estado; /* "Rio de Janeiro" */ faixa->regiao; /* "Sudeste" */ faixa->ibge; /* "3300225" */ faixa->cep_ini; /* "25845001" */ faixa->cep_fim; /* "25849899" */ faixa->ceps; /* 118 */ } ``` Three things this is not: - **Not an address.** The CEP asked for still does not exist and the call still failed. Never promote `faixa` to a delivery address, and never let it turn a not-found into a success. Use it to say "that CEP does not exist — did you mean somewhere in Areal/RJ?". - **Not the range the Correios publish.** `cep_ini` and `cep_fim` are the smallest and largest CEP that *actually exist* there. The DNE's own start for Areal is `25845000` — exactly the CEP that just failed. `cep_ini` is `25845001`, the first one really in the database. - **Not a list.** The range is sparse: Areal's 118 CEPs are scattered across the ~4900 numbers between `25845001` and `25849899`. Iterating the interval mostly produces CEPs that do not exist either. That is what `ceps` is for — say "118 CEPs in Areal", never "4899". `faixa` is **omitted, never null**, when the CEP falls in a band no municipality owns, so `cepis_error_faixa` returning `NULL` — equivalently `err.has_faixa == false` — is how you tell absent from present. A missing, malformed or empty block leaves it absent and never changes the status: a 404 is already the answer, and failing to read the garnish must not turn it into a different one. `cepis_error_t` still owns nothing and still lives on the stack: `faixa` is embedded by value, so there is nothing extra to free. The pointer `cepis_error_faixa` returns is borrowed from the error and dies with it. ## Development ```sh make # list the targets make build # static and shared libraries, plus the cepis CLI make test # run the suite against a local HTTP server make lint # clang-format --dry-run + clang-tidy, touches nothing make format # apply clang-format (writes) make check # lint, then test, then build make memcheck # run the suite under valgrind make sanitize # rebuild and run the suite under ASan + UBSan ``` The suite stands up a real HTTP server on a loopback port and points the client at it through `base_url`. Nothing is mocked, and no test touches the network. ## Licence ISC. See [LICENSE](LICENSE).