# cepis Client for the [public CEP API at cep.is](https://cep.is), written in hand-written x86-64 assembly (GNU `as`, AT&T syntax) with a C-callable API. Official Correios data (DNE), refreshed every two weeks. ```c #include cepis_address *a; if (cepis_lookup(NULL, "01001-000", &a, NULL) == CEPIS_OK) { printf("%s %s %s\n", a->logradouro, a->localidade, a->uf); /* Praça da Sé São Paulo SP */ cepis_address_free(a); } ``` ## What it reaches TLS is not implemented here — the library links against **libcurl**, which is the only dependency, and drives it through the C ABI. It therefore talks to the real `https://cep.is` as well as to any plain-HTTP instance you point it at. Marshalling, JSON and CSV parsing, retry, backoff and `Retry-After` handling are all assembly. Linux on x86-64, glibc, GNU binutils. There is no portable-assembly story: the System V AMD64 ABI, glibc's `timegm`/`strtod`, and `as` syntax are all assumed. ## Installation ```sh make build make install PREFIX=/usr/local ``` Link with `-lcepis -lcurl`. Requires libcurl's headers only for the build of your own program (this library ships no curl types in its header). ## Usage ### A single lookup `cepis_lookup` accepts the CEP masked or unmasked. Passing `NULL` as the client uses a process-wide default client; `err` is optional. ```c cepis_address *a = NULL; cepis_error err; char masked[10]; switch (cepis_lookup(NULL, "01310-100", &a, &err)) { case CEPIS_OK: a->logradouro; /* "Avenida Paulista" */ a->localidade; /* "São Paulo" */ a->uf; /* "SP" */ a->ibge; /* "3550308" */ a->cep; /* "01310100" (always unmasked) */ cepis_address_formatted_cep(a, masked); /* "01310-100" */ cepis_address_free(a); break; case CEPIS_ERR_NOT_FOUND: break; default: fprintf(stderr, "%s\n", err.message); } ``` Fields the database does not have for that CEP come back as `""`, never `NULL`. ### Coordinates About 57% of CEPs have a coordinate. When they do not, `geo` is `NULL`: ```c if (a->geo != NULL) { a->geo->lat; /* -23.567202 */ a->geo->lon; /* -46.649659 */ a->geo->raio_m; /* 1084 */ 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(a->geo)` is the shorthand for the first case. ### 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 **out = NULL; size_t i; if (cepis_lookup_many(NULL, ceps, 3, &out, NULL) == CEPIS_OK) { for (i = 0; i < 3; i++) printf("%s: %s\n", ceps[i], out[i] ? out[i]->localidade : "not found"); cepis_address_list_free(out, 3); } ``` The array 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. Arrays 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, and `raw == NULL`. Need > `geo`? Use `cepis_lookup`, one at a time. ### API health ```c struct cepis_health *h = NULL; if (cepis_health(NULL, &h, NULL) == CEPIS_OK) { h->status; /* "ok" */ h->dne; /* "26071" identifier of the fortnightly base */ 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 `out` parameter transfers ownership to the caller and has exactly one matching release function. All allocation goes through libc `malloc`, so the library is safe to use alongside anything else that does. | you received | you must call | | --- | --- | | `cepis_address *` | `cepis_address_free` | | `cepis_address **` from `cepis_lookup_many` | `cepis_address_list_free(arr, n)` | | `struct cepis_health *` | `cepis_health_free` | | `cepis_client *` | `cepis_client_free` | | the default client, `cepis_strerror`, `cepis_version` | nothing | `cepis_error` is caller-allocated and owns nothing. On any non-`CEPIS_OK` return, every out-pointer is `NULL`. `cepis_global_cleanup()` at exit releases libcurl's global state and the default client. ## Configuration Zero-initialise a `cepis_config`, set what you need, and reuse the client. It keeps no state between calls and is safe to share across threads: ```c cepis_config cfg; cepis_client *client = NULL; memset(&cfg, 0, sizeof cfg); cfg.base_url = "http://localhost:8080"; /* point at another instance */ cfg.timeout = 10.0; /* seconds per attempt */ cfg.max_attempts = 3; /* counting the first; 1 disables retrying */ cfg.backoff_base = 0.5; /* base of the exponential wait */ cfg.backoff_max = 8.0; /* cap on each wait, Retry-After included */ cfg.total_timeout = 0.0; /* cap on the whole call; 0 means none */ cepis_client_new(&cfg, &client); ``` Retries cover 429, 5xx and network failures, with exponential backoff and full jitter; 400 and 404 are never retried. `base_url` is the only field that usually changes between environments. Call `cepis_global_init()` once from the main thread before starting other threads: the lazy initialisation on first request is the one part that races. ## Errors Every call returns a `cepis_status`. 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 | 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_NOMEM` | an allocation failed | no | | `CEPIS_ERR_USAGE` | a required out-parameter was `NULL` | no | Pass a `cepis_error` to get `message`, `http_status` and `retry_after`; `cepis_strerror` names the status itself. `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. ## Limitations - `timeout` bounds one **attempt**, not one socket operation: libcurl has no per-operation timeout. `total_timeout` bounds the whole call. - `Retry-After` is parsed as a number of seconds and as an IMF-fixdate (`Wed, 21 Oct 2026 07:28:00 GMT`). The two obsolete HTTP-date formats of RFC 9110 (RFC 850 and `asctime`) are not parsed; such a header is ignored and the usual jittered backoff applies. - `cepis_lookup_many` validates each CEP locally and does not send the malformed ones, so they cost no bandwidth. Their positions are `NULL` either way. - Each request uses a fresh libcurl handle, so connections are not reused between calls. This is why the batch endpoint exists. - The JSON reader accepts an object at the top level, tolerates and skips unknown fields of any shape, and decodes `\uXXXX` including surrogate pairs. It is not a general-purpose JSON parser and does not build a document tree — `raw` is there for anything it does not model. ## Development ```sh make # list the targets make build # static and shared library into build/ make test # the suite, against a local HTTP server on a random port make asan # the same suite under AddressSanitizer make valgrind # the same suite under valgrind make lint # assemble with warnings fatal, compile the header, style check make check # lint, then test, then build make demo # build build/cepis-demo ``` `make test` starts `tests/server.py` (Python 3, standard library only), points the driver at it through `base_url`, and tears it down. Nothing is mocked and no test touches the network. ```sh build/cepis-demo --health 01001000 build/cepis-demo --base-url http://localhost:8080 01001000 ``` ## Licence ISC. See [LICENSE](LICENSE).