# 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 ``` 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.1.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. ## 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).