/* * cepis - client for the public CEP API at https://cep.is * * Copyright (c) 2026 Murilo Ijanc * SPDX-License-Identifier: ISC * * The whole implementation is hand-written x86-64 assembly (GNU as, AT&T * syntax); this header is the only C in the library. Every entry point below * follows the System V AMD64 ABI. */ #ifndef CEPIS_H #define CEPIS_H #include #ifdef __cplusplus extern "C" { #endif #define CEPIS_VERSION "0.2.0" /** Largest number of CEPs the API accepts in one /csv request. */ #define CEPIS_MAX_BATCH 500 /** Size of cepis_error::message, including the terminating NUL. */ #define CEPIS_MESSAGE_MAX 256 /** Size of cepis_faixa::localidade, including the terminating NUL. The * longest municipality name in the IBGE list is 32 characters; this leaves * room for it in UTF-8 twice over. A longer name is truncated, never * overflowed. */ #define CEPIS_FAIXA_LOCALIDADE_MAX 72 /** * Outcome of a call. Split 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 and retrying is pointless, while * CEPIS_ERR_RATE_LIMITED, CEPIS_ERR_SERVER and CEPIS_ERR_NETWORK are problems * with transport that may succeed later. The client has already retried those * three internally before returning them. */ typedef enum { CEPIS_OK = 0, /** The CEP is not 8 digits. Detected locally; no request was made. */ CEPIS_ERR_INVALID_CEP = 1, /** Well-formed, but absent from the DNE database (HTTP 404). */ CEPIS_ERR_NOT_FOUND = 2, /** HTTP 429 on every attempt. See cepis_error::retry_after. */ CEPIS_ERR_RATE_LIMITED = 3, /** HTTP 5xx on every attempt. See cepis_error::http_status. */ CEPIS_ERR_SERVER = 4, /** The request never completed: DNS, connection, timeout, TLS. */ CEPIS_ERR_NETWORK = 5, /** A success status came back with a body that cannot be parsed. */ CEPIS_ERR_INVALID_RESPONSE = 6, /** An allocation failed. */ CEPIS_ERR_NOMEM = 7, /** A required out-parameter was NULL, or a configuration value was * nonsensical (a negative max_attempts). */ CEPIS_ERR_USAGE = 8 } cepis_status; /** * 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` around * 100-150 m. Good for maps and for distance. * - "localidade": the CEP *is* a whole municipality and the point is its * centroid, `raio_m` 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 { double lat; double lon; long raio_m; /** "logradouro" or "localidade". Never NULL. */ char *precisao; } cepis_geo; /** * A CEP and everything the database knows about it. * * Field names are the API's own, in Portuguese, so that what you read in the * JSON at https://cep.is/01001000 is exactly what you access here. * * Every char* field is non-NULL. A field the database lacks for that CEP is * the empty string, never NULL, matching what the API returns. * * `geo` is the exception: NULL when there is no coordinate (about 43% of * CEPs), because the API omits the whole object in that case rather than * sending null. It is also always NULL for addresses from * cepis_lookup_many() - see that function. * * `raw` holds the response body exactly as it arrived, so a field the API * grows before this library is updated is not lost. It is NULL for addresses * produced by cepis_lookup_many(). * * Free with cepis_address_free(). */ typedef struct cepis_address { char *cep; char *logradouro; char *complemento; char *unidade; char *bairro; char *localidade; char *uf; char *estado; char *regiao; char *ibge; char *gia; char *ddd; char *siafi; cepis_geo *geo; char *raw; } cepis_address; /** * The response of GET /health. * * This one is a bare struct tag, not a typedef, because cepis_health() is the * function that fills it - the same split POSIX makes between struct stat and * stat(). Write struct cepis_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. An API reporting itself degraded is not an error - that * arrives in `status`. * * Free with cepis_health_free(). */ struct cepis_health { /** "ok" when the API reports itself healthy. Never NULL. */ char *status; char *dne; long long ceps; long long geo; char *base_gerada_em; /** The response body as it arrived. Never NULL. */ char *raw; }; /** * The municipality a CEP that does not exist would have belonged to. * * 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 API answers a 404 with the * band the number falls in, when a municipality owns that band. This is * context for an error and **not an address**: the lookup still failed, and * cepis_lookup() still returns CEPIS_ERR_NOT_FOUND. * * Everything here is inline storage inside cepis_error - nothing to free, * nothing that can be NULL. Read it only when `present` is non-zero; when it * is zero the strings are `""` and `ceps` is 0. * * `cep_ini` and `cep_fim` are the **smallest and largest CEP that actually * exist** in that municipality, not the band the Correios publish. For Areal * the DNE's own band starts at 25845000 - exactly the CEP that just failed - * while `cep_ini` is 25845001. * * The band is sparse. Areal's 118 CEPs are scattered over the ~4900 numbers * between 25845001 and 25849899, so `ceps` is a count and the pair is a pair * of endpoints. Never present the two as a range of valid CEPs, and never * iterate it. */ typedef struct cepis_faixa { /** Non-zero when the API named a municipality for this CEP. Zero means it * did not - the field is omitted, never null, for a number in a band no * municipality owns. */ int present; /** Municipality name, e.g. "Areal". Truncated at * CEPIS_FAIXA_LOCALIDADE_MAX - 1 bytes. */ char localidade[CEPIS_FAIXA_LOCALIDADE_MAX]; /** Two-letter state, e.g. "RJ". */ char uf[3]; /** Smallest CEP that exists there, 8 digits unmasked. */ char cep_ini[9]; /** Largest CEP that exists there, 8 digits unmasked. */ char cep_fim[9]; /** How many CEPs exist there. Far smaller than cep_fim - cep_ini. */ long long ceps; } cepis_faixa; /** * Detail about a failure. Optional: pass NULL to any call that takes one. * * Caller-allocated, no ownership: declare one on the stack and reuse it. It is * fully overwritten at the start of every call that receives it, `faixa` * included, so a band left over from a previous 404 cannot be mistaken for * this one's. */ typedef struct cepis_error { /** Same value the call returned. */ int status; /** The HTTP status for CEPIS_ERR_SERVER and CEPIS_ERR_RATE_LIMITED, * else 0. */ int http_status; /** Seconds the server asked for via Retry-After, or 0 if it did not say. * Only meaningful for CEPIS_ERR_RATE_LIMITED. */ double retry_after; /** Human-readable description, NUL-terminated, empty on success. On a 404 * with a `faixa` it names the municipality too. */ char message[CEPIS_MESSAGE_MAX]; /** Only ever filled by CEPIS_ERR_NOT_FOUND, and only when the API sent a * band. See cepis_faixa. */ cepis_faixa faixa; } cepis_error; /** * Client configuration. Zero-initialise it and set only what you need: every * field left at 0 (or NULL) takes its default. */ typedef struct cepis_config { /** Where the API lives. Default "https://cep.is". A trailing slash is * stripped. This is the only field that usually changes between * environments - point it at a local instance to test. */ const char *base_url; /** Seconds allowed for one attempt, connect and transfer together. * Default 10.0. libcurl has no per-socket-operation timeout, so unlike * the Python client this is a per-attempt cap; `total_timeout` caps the * whole call including retries and waits. */ double timeout; /** Attempts in total, counting the first. Default 3; 1 disables retrying. * Negative is rejected with CEPIS_ERR_USAGE. */ int max_attempts; /** Base of the exponential wait, in seconds. Default 0.5. The actual wait * is drawn from [0, base * 2^(n-1)] - full jitter. Without the * randomness, N clients that fail together come back together. */ double backoff_base; /** Cap on each individual wait, in seconds. Default 8.0. It applies to a * server-sent Retry-After too, so a misconfigured proxy answering * "Retry-After: 3600" cannot park the caller for an hour. */ double backoff_max; /** Cap on the whole call, attempts and waits included, in seconds. * 0 leaves max_attempts as the only bound. */ double total_timeout; /** Overrides the User-Agent. Default * "cepis-asm/0.2.0 (+https://cep.is)". */ const char *user_agent; } cepis_config; /** Opaque client. Create with cepis_client_new(), destroy with * cepis_client_free(). The configuration is immutable and no call keeps state * between requests, so one instance is safe to share across threads. */ typedef struct cepis_client cepis_client; /** Library version, "0.2.0". Static storage; do not free. */ const char *cepis_version(void); /** One-line description of a cepis_status. Static storage; do not free. */ const char *cepis_strerror(int status); /** * Build a client. `cfg` may be NULL for an all-defaults client. * * On CEPIS_OK, *out owns a client the caller must release with * cepis_client_free(). On failure *out is NULL. */ cepis_status cepis_client_new(const cepis_config *cfg, cepis_client **out); /** Release a client. NULL is accepted. */ void cepis_client_free(cepis_client *client); /** * The process-wide default client, created on first use with all defaults. * This is what the lookup calls use when passed a NULL client. Returns NULL * only if it could not be allocated. Owned by the library; do not free it - * cepis_global_cleanup() releases it. */ cepis_client *cepis_default_client(void); /** * Initialise libcurl's global state. Optional: the first request does it * anyway. Call it once from the main thread before starting other threads if * your program is multi-threaded, since the lazy path races. */ void cepis_global_init(void); /** * Release libcurl's global state and the default client. Call once at exit, * after the last cepis call and with no other thread in the library. Not * required for correctness, but it makes leak checkers quiet. */ void cepis_global_cleanup(void); /** * Return the CEP's 8 digits, accepting the mask. * * "01001-000", "01001000" and " 01001-000 " all become "01001000"; the * separators '-', '.' and whitespace are ignored wherever they appear. * * `out` must have room for 9 bytes and receives a NUL-terminated string, or * "" on failure. Returns CEPIS_ERR_INVALID_CEP unless exactly 8 digits * remain, or CEPIS_ERR_USAGE if `out` is NULL. */ cepis_status cepis_normalize_cep(const char *value, char *out); /** * Look up a single CEP. `cep` may be masked or unmasked. * * `client` may be NULL to use the default client. `err` may be NULL. * * On CEPIS_OK, *out owns an address the caller must release with * cepis_address_free(). On failure *out is NULL. * * The CEP is validated locally first: a 7-digit CEP returns * CEPIS_ERR_INVALID_CEP without a request being made. A 400 from the API is * also CEPIS_ERR_INVALID_CEP, carrying the API's own message. A 404 is * CEPIS_ERR_NOT_FOUND, and fills cepis_error::faixa when the API says which * municipality owns that band. Neither is retried; 429, 5xx and network * failures are. */ cepis_status cepis_lookup(cepis_client *client, const char *cep, cepis_address **out, cepis_error *err); /** * Look up many CEPs, up to CEPIS_MAX_BATCH per request. Do not loop over * cepis_lookup(): 500 calls pay 500 TLS handshakes, the batch pays one. * * On CEPIS_OK, *out owns an array of exactly `n` pointers, in the same order * as `ceps`. A CEP that does not exist, or that is malformed, is NULL at its * position - it does not vanish from the array and it is not an error. If you * need to know which one was malformed, validate first with * cepis_normalize_cep(). Release the array with * cepis_address_list_free(arr, n). * * Input longer than CEPIS_MAX_BATCH is chunked automatically. * * WARNING: the batch carries no coordinate. The endpoint behind it is CSV and * the CSV has no geo column, so every address returned here has geo == NULL, * including for CEPs that do have one. Need geo? Use cepis_lookup(), one at a * time. `raw` is NULL here for the same reason. * * `n` may be 0, in which case *out is a valid empty array. */ cepis_status cepis_lookup_many(cepis_client *client, const char *const *ceps, size_t n, cepis_address ***out, cepis_error *err); /** * Query GET /health. * * On CEPIS_OK, *out owns a cepis_health the caller must release with * cepis_health_free(). An API reporting itself degraded (HTTP 503 with a body) * is still CEPIS_OK - the diagnosis arrives in cepis_health::status, which is * exactly what you want when the thing you are doing is checking health. * Network failures still fail. */ cepis_status cepis_health(cepis_client *client, struct cepis_health **out, cepis_error *err); /** Release an address. NULL is accepted. */ void cepis_address_free(cepis_address *address); /** Release an array of `n` addresses and the array itself, skipping NULL * entries. NULL is accepted. */ void cepis_address_list_free(cepis_address **addresses, size_t n); /** Release a cepis_health. NULL is accepted. */ void cepis_health_free(struct cepis_health *health); /** * Write the CEP with its mask, "01001-000", into `out`, which must have room * for 10 bytes. The API always returns it unmasked; this is presentation. */ void cepis_address_formatted_cep(const cepis_address *address, char *out); /** * Non-zero when the coordinate points at a street rather than a municipality, * i.e. strcmp(geo->precisao, "logradouro") == 0. This is the check to make * before using the point to measure distance. NULL returns 0. */ int cepis_geo_is_precise(const cepis_geo *geo); #ifdef __cplusplus } #endif #endif /* CEPIS_H */