/** * @file cepis.h * @brief C client for the public CEP API at https://cep.is * * Official Correios data (DNE), refreshed every two weeks. * * @code * cepis_config_t cfg; * cepis_client_t *client = NULL; * cepis_address_t *address = NULL; * cepis_error_t err; * * cepis_config_init(&cfg); * if (cepis_client_new(&cfg, &client, &err) != CEPIS_OK) * return 1; * if (cepis_lookup(client, "01001-000", &address, &err) == CEPIS_OK) { * printf("%s %s\n", address->localidade, address->uf); * cepis_address_free(address); * } * cepis_client_free(client); * @endcode * * @section ownership Ownership * * Every function that hands back a pointer hands back ownership of it, and * names the function that releases it: * * - ::cepis_client_t from ::cepis_client_new, released by ::cepis_client_free. * - ::cepis_address_t from ::cepis_lookup, released by ::cepis_address_free. * - ::cepis_address_list_t from ::cepis_lookup_many, released by * ::cepis_address_list_free. * - ::cepis_health_t from ::cepis_health, released by ::cepis_health_free. * * Every `char *` inside those structs is owned by the struct and is freed with * it. Never free one on its own, and never keep a pointer to one past the * lifetime of its owner — copy the bytes if you need them longer. * * Strings passed *into* this library (::cepis_config_t members, CEPs) are * borrowed for the duration of the call only; the library copies whatever it * needs to keep. * * @section fieldnames Field names * * The API surface is English, but the **data fields keep the API's own * Portuguese names** — `logradouro`, `bairro`, `localidade`. What you read in * the JSON at https://cep.is/01001000 is what you access on the struct. * * @section threads Threads * * A ::cepis_client_t owns one libcurl handle and is **not** safe to use from * two threads at once. Create one client per thread; they share nothing. * * @copyright ISC. Copyright (c) 2026 Murilo Ijanc */ #ifndef CEPIS_H #define CEPIS_H #include #include #ifdef __cplusplus extern "C" { #endif /** Library version. */ #define CEPIS_VERSION "0.1.0" /** Default API endpoint. */ #define CEPIS_DEFAULT_BASE_URL "https://cep.is" /** Most CEPs the API accepts in one batch request. */ #define CEPIS_MAX_BATCH 500 /** Digits in a CEP, mask excluded. */ #define CEPIS_CEP_LEN 8 /** Size of ::cepis_error_t::message, terminator included. */ #define CEPIS_ERROR_MESSAGE_MAX 256 /** * @brief Result of every fallible call. * * The split is by **the decision the caller has to make**, not by the HTTP * status that caused it. ::CEPIS_ERR_INVALID_CEP and ::CEPIS_ERR_NOT_FOUND are * problems with the request: retrying returns the same answer. The transport * errors below them may succeed later, and the client already retried on its * own before returning any of them. */ typedef enum cepis_status { /** Success. */ CEPIS_OK = 0, /** The CEP is not 8 digits, or the API rejected it with a 400. */ CEPIS_ERR_INVALID_CEP, /** Well-formed CEP, absent from the DNE database. */ CEPIS_ERR_NOT_FOUND, /** The API answered 429 on every attempt. */ CEPIS_ERR_RATE_LIMITED, /** The API answered 5xx on every attempt. */ CEPIS_ERR_SERVER, /** The request never completed: DNS, connection, timeout, TLS. */ CEPIS_ERR_NETWORK, /** A success status came back with a body that cannot be parsed. */ CEPIS_ERR_INVALID_RESPONSE, /** A configuration value is out of range. */ CEPIS_ERR_CONFIG, /** Out of memory. */ CEPIS_ERR_NOMEM } cepis_status_t; /** * @brief Detail about a failure. * * Owns no memory: the message lives in a fixed buffer inside the struct, so an * error can be declared on the stack and forgotten. Pass a pointer to one as * the last argument of any fallible call, or `NULL` to discard the detail. */ typedef struct cepis_error { /** Same value the call returned. */ cepis_status_t status; /** Human-readable explanation, always NUL-terminated. */ char message[CEPIS_ERROR_MESSAGE_MAX]; /** HTTP status of the last attempt, or 0 when there was none. */ long http_status; /** * Seconds the server asked the caller to wait, from `Retry-After`, or * a negative value when it did not say. Set for * ::CEPIS_ERR_RATE_LIMITED. The client already honoured it across the * attempts it made; it is exposed for rescheduling the work. */ double retry_after; } cepis_error_t; /** * @brief The CEP's coordinate, when the database has one. * * The point is the **median** of the coordinates of that CEP's addresses, not * the mean: one badly geocoded address 40 km away shifts a mean and does not * move a median. * * Always check ::precisao before using ::lat / ::lon for distance: * * - `"logradouro"`: the point is a stretch of street. ::raio_m lands around * 100-150 m. Good for maps and for distance. * - `"localidade"`: the CEP *is* the whole municipality (a city-wide general * CEP) and the point is its centroid. ::raio_m runs from 7 km to 40 km. * Computing shipping from this is wrong by tens of kilometres. * * ::raio_m is the radius containing 90% of that CEP's addresses. It exists * precisely so that error cannot pass unnoticed. */ typedef struct cepis_geo { /** Latitude in decimal degrees. */ double lat; /** Longitude in decimal degrees. */ double lon; /** Radius in metres containing 90% of the CEP's addresses. */ long raio_m; /** `"logradouro"` or `"localidade"`. Never `NULL`. */ char *precisao; } cepis_geo_t; /** * @brief A CEP and everything the database knows about it. * * Every `char *` field is non-`NULL` and owned by the struct. Fields the * database does not have for that CEP are the empty string, never `NULL` -- * that is what the API returns, and changing it here would create a difference * between reading the JSON and reading this library. * * ::geo is the exception: it is `NULL` when there is no coordinate. There the * difference is real, because the API **omits** the whole field in that case * (about 43% of CEPs) rather than sending null. */ typedef struct cepis_address { /** The 8 digits, unmasked. */ char *cep; char *logradouro; char *complemento; char *unidade; char *bairro; char *localidade; /** Two-letter state code, e.g. `"SP"`. */ char *uf; char *estado; char *regiao; /** IBGE municipality code. */ char *ibge; char *gia; char *ddd; char *siafi; /** The coordinate, or `NULL` when the database has none. */ cepis_geo_t *geo; /** * The response body exactly as it arrived, uninterpreted, so a field * the API grows before this library is updated is not lost. `NULL` for * addresses that came from ::cepis_lookup_many, whose transport is CSV. */ char *raw; } cepis_address_t; /** * @brief Result of ::cepis_lookup_many. * * ::items has ::count entries, one per requested CEP, **in the requested * order**. An entry is `NULL` when that CEP does not exist or was malformed. */ typedef struct cepis_address_list { /** ::count entries, any of which may be `NULL`. */ cepis_address_t **items; /** Same as the number of CEPs requested. */ size_t count; } cepis_address_list_t; /** * @brief The response of `GET /health`. * * Good for two things: checking that the API answers, and knowing **which * version of the database** is live. The DNE database changes every two weeks; * ::dne identifies it. */ typedef struct cepis_health { /** `"ok"` when the API reports itself healthy. Never `NULL`. */ char *status; /** Identifier of the fortnightly DNE database. Never `NULL`. */ char *dne; /** CEPs in the database. */ long long ceps; /** CEPs that carry a coordinate. */ long long geo; /** ISO-8601 timestamp of the database build. Never `NULL`. */ char *base_gerada_em; } cepis_health_t; /** * @brief Client configuration. * * Fill it with ::cepis_config_init and override what you need. The two string * members are borrowed for the duration of ::cepis_client_new only -- the * client copies them. */ typedef struct cepis_config { /** * Where the API lives. Point it at a local instance (the API repo * brings one up on `http://localhost:8080` with `just up`) or at a * test server. It is the only member that usually changes between * environments. A trailing slash is stripped, so it cannot produce * doubled separators that some proxies normalise and others 404. */ const char *base_url; /** * Overrides the `User-Agent`. `NULL` uses the default, which * identifies the library and its version -- that is what lets the * server diagnose an old client misbehaving. */ const char *user_agent; /** * Seconds to wait per socket operation: it caps the connect phase and * aborts a transfer that stalls for that long. It is **not** a cap on * the whole call -- that is ::total_timeout. Must be > 0. */ double timeout; /** * How many attempts in total, counting the first. `1` disables * retrying. The default of `3` covers the common case (one transient * failure) without turning a real outage into a long wait. Must be * >= 1. */ int max_attempts; /** * Base of the exponential wait, in seconds. The actual wait is drawn * from `[0, base * 2^(n-1)]` -- *full jitter*. The randomness is not * decoration: without it, N clients that fail together come back * together and reproduce the spike that took the service down. */ double backoff_base; /** * Cap on each individual wait, in seconds. It applies to a server-sent * `Retry-After` too, or a `Retry-After: 3600` from a misconfigured * proxy would park the thread for an hour. */ double backoff_max; /** * Cap on the whole call, attempts and waits included. `0` leaves * ::max_attempts as the only bound. Set it in code serving a web * request: without it the worst case is `max_attempts * timeout` plus * the sum of the backoffs. */ double total_timeout; } cepis_config_t; /** * @brief Opaque client. * * Create with ::cepis_client_new, release with ::cepis_client_free. Reuse one * per thread: the configuration is immutable and no call keeps state between * invocations, so consecutive requests share a connection. */ typedef struct cepis_client cepis_client_t; /** * @brief Fill @p config with the defaults. * * `https://cep.is`, 10 s per socket operation, 3 attempts, 0.5 s backoff base, * 8 s backoff cap, no total timeout, the library's own user agent. */ void cepis_config_init(cepis_config_t *config); /** * @brief Create a client. * * @param config Configuration, or `NULL` for the defaults. Copied. * @param out Receives the client on success, untouched otherwise. * @param err Receives failure detail, or `NULL`. * @return ::CEPIS_OK, ::CEPIS_ERR_CONFIG for a value out of range, or * ::CEPIS_ERR_NOMEM. * * Release the client with ::cepis_client_free. */ cepis_status_t cepis_client_new(const cepis_config_t *config, cepis_client_t **out, cepis_error_t *err); /** @brief Release a client. `NULL` is accepted and ignored. */ void cepis_client_free(cepis_client_t *client); /** @brief The base URL the client was built with, trailing slash stripped. */ const char *cepis_client_base_url(const cepis_client_t *client); /** @brief The `User-Agent` the client sends. */ const char *cepis_client_user_agent(const cepis_client_t *client); /** * @brief Look up a single CEP. * * @param client A client. * @param cep Masked or unmasked; validated locally before any request. * @param out Receives the address on success, untouched otherwise. Release * it with ::cepis_address_free. * @param err Receives failure detail, or `NULL`. * @return ::CEPIS_OK, or one of ::CEPIS_ERR_INVALID_CEP (not 8 digits -- no * request is made), ::CEPIS_ERR_NOT_FOUND, ::CEPIS_ERR_RATE_LIMITED, * ::CEPIS_ERR_SERVER, ::CEPIS_ERR_NETWORK, * ::CEPIS_ERR_INVALID_RESPONSE, ::CEPIS_ERR_NOMEM. * * A 400 from the API also becomes ::CEPIS_ERR_INVALID_CEP, carrying the API's * own message: the format already passed local validation, so the server knows * something this client does not. * * This is the only call that carries ::cepis_address_t::geo. */ cepis_status_t cepis_lookup(cepis_client_t *client, const char *cep, cepis_address_t **out, cepis_error_t *err); /** * @brief Look up many CEPs in one batch. * * @param client A client. * @param ceps @p count CEPs, masked or unmasked. * @param count How many. `0` succeeds without making a request. * @param out Receives the list on success, untouched otherwise. Release it * with ::cepis_address_list_free. * @param err Receives failure detail, or `NULL`. * @return ::CEPIS_OK or a transport error. Never ::CEPIS_ERR_NOT_FOUND. * * The list has **the same length and the same order** as the input. A CEP that * does not exist becomes `NULL` at its position -- it does not vanish from the * list, and it is not an error. That is the right difference from * ::cepis_lookup: in a batch of a thousand, one bad CEP must not cost the other * nine hundred and ninety-nine. A malformed CEP becomes `NULL` for the same * reason; call ::cepis_normalize_cep first if you need to know which one it * was. * * Input is chunked into blocks of ::CEPIS_MAX_BATCH automatically. Do not loop * over ::cepis_lookup instead: a loop of 500 calls pays 500 round trips, this * pays one. * * @warning The batch carries **no coordinate**. The endpoint behind it is CSV, * and the CSV has no `geo` column. Every address returned here has * `geo == NULL`, including for CEPs that do have one. Need `geo`? Use * ::cepis_lookup, one at a time. */ cepis_status_t cepis_lookup_many(cepis_client_t *client, const char *const *ceps, size_t count, cepis_address_list_t *out, cepis_error_t *err); /** * @brief Query `GET /health`: does the API answer, and with which database. * * @param client A client. * @param out Receives the health report. Release it with * ::cepis_health_free. * @param err Receives failure detail, or `NULL`. * @return ::CEPIS_OK or a transport error. * * Does not fail for an API reporting itself degraded -- that arrives in * ::cepis_health_t::status, and checking health is exactly when you want the * diagnosis rather than an error. Network failures still fail. */ cepis_status_t cepis_health(cepis_client_t *client, cepis_health_t **out, cepis_error_t *err); /** * @brief Release an address. `NULL` is accepted and ignored. * * Frees every string it owns, its ::cepis_address_t::geo and its * ::cepis_address_t::raw. */ void cepis_address_free(cepis_address_t *address); /** * @brief Release a list and every address in it. * * Zeroes @p list afterwards, so calling it twice is harmless. `NULL` is * accepted and ignored. */ void cepis_address_list_free(cepis_address_list_t *list); /** @brief Release a health report. `NULL` is accepted and ignored. */ void cepis_health_free(cepis_health_t *health); /** * @brief Return the CEP's 8 digits, accepting the mask. * * `"01001-000"`, `"01001000"` and `" 01001-000 "` all become `"01001000"`. * * @param value The CEP as the caller has it. * @param out Receives the 8 digits and a terminator. Untouched on failure. * @return ::CEPIS_OK, or ::CEPIS_ERR_INVALID_CEP unless exactly 8 digits * remain. * * The client runs this **before** any request: a 7-digit CEP is the caller's * bug, and spending a round trip for the server to say so wastes both sides' * time. */ cepis_status_t cepis_normalize_cep(const char *value, char out[CEPIS_CEP_LEN + 1]); /** * @brief Write the CEP with its mask: `01001-000`. * * @param cep 8 digits, masked or not. * @param out Receives 9 characters and a terminator. Untouched on failure. * @return ::CEPIS_OK or ::CEPIS_ERR_INVALID_CEP. * * The native contract always returns the CEP unmasked; formatting is * presentation, and it lives here so it does not become a repeated `sprintf` in * every project that uses this. */ cepis_status_t cepis_format_cep(const char *cep, char out[CEPIS_CEP_LEN + 2]); /** * @brief `true` when the coordinate points at a street, not a municipality. * * Shorthand for `precisao == "logradouro"`, which is the check to make before * using the point to measure distance. `false` for `NULL`. */ bool cepis_geo_is_precise(const cepis_geo_t *geo); /** @brief `true` when the API reports itself healthy. `false` for `NULL`. */ bool cepis_health_ok(const cepis_health_t *health); /** @brief A short, stable, English name for a status code. */ const char *cepis_status_str(cepis_status_t status); /** * @brief Initialise the process-wide state this library's HTTP layer needs. * * Optional: ::cepis_client_new does it too. Call it once from `main`, before * any thread starts, if you create clients concurrently -- that removes any * doubt about the initialisation racing. * * Each successful call must be balanced by one ::cepis_global_cleanup. */ cepis_status_t cepis_global_init(void); /** @brief Balance one ::cepis_global_init. */ void cepis_global_cleanup(void); #ifdef __cplusplus } #endif #endif