# cepis COBOL client for the [public CEP API at cep.is](https://cep.is). Official Correios data (DNE), refreshed every two weeks. ```cobol WORKING-STORAGE SECTION. COPY "cepis.cpy". PROCEDURE DIVISION. CALL "CEPIS-DEFAULT-CONFIG" USING CEPIS-CONFIG MOVE "01001-000" TO CEPIS-CEP-INPUT CALL "CEPIS-LOOKUP" USING CEPIS-CONFIG CEPIS-CEP-INPUT CEPIS-ADDRESS CEPIS-STATUS IF CEPIS-OK DISPLAY ADDR-LOGRADOURO " " ADDR-LOCALIDADE " " ADDR-UF END-IF ``` Targets **GnuCOBOL 3.x**, default dialect, **free source format** (`cobc -free`). HTTP goes through **libcurl**, reached from a small C shim in `src/cepis-http.c` rather than from COBOL directly, because `curl_easy_setopt` is variadic and a COBOL `CALL` cannot express that portably. ## Installation ```sh git clone https://git.cep.is/cobol.git cepis-cobol cd cepis-cobol make build ``` That produces `build/libcepis.a`. Link it into your program and add the copybook directory to the search path: ```sh cobc -x -free -I /path/to/cepis-cobol/copy \ -o myprog myprog.cob /path/to/cepis-cobol/build/libcepis.a -lcurl ``` Requires GnuCOBOL 3.x, a C compiler and libcurl with its development headers. `python3` is needed only to run the test suite. ## Usage Everything is a `CALL` to a subprogram, and everything shares two records from `cepis.cpy`: `CEPIS-CONFIG` going in and `CEPIS-STATUS` coming back. `CALL "CEPIS-DEFAULT-CONFIG" USING CEPIS-CONFIG` fills the configuration with its defaults; override afterwards what you need. ### A single lookup `CEPIS-LOOKUP` accepts the CEP masked or unmasked: ```cobol MOVE "01310-100" TO CEPIS-CEP-INPUT CALL "CEPIS-LOOKUP" USING CEPIS-CONFIG CEPIS-CEP-INPUT CEPIS-ADDRESS CEPIS-STATUS ADDR-LOGRADOURO *> 'Avenida Paulista' ADDR-LOCALIDADE *> 'São Paulo' ADDR-UF *> 'SP' ADDR-IBGE *> '3550308' ADDR-CEP *> '01310100' always unmasked ADDR-CEP-MASKED *> '01310-100' ``` Fields the database does not have for that CEP come back as spaces. `CEPIS-ADDRESS` is cleared before every call, so a failed lookup never leaves the previous CEP's street behind. ### Coordinates About 57% of CEPs have a coordinate. There is an explicit flag rather than a marker value: ```cobol IF ADDR-GEO-KNOWN DISPLAY ADDR-GEO-LAT " " ADDR-GEO-LON DISPLAY ADDR-GEO-RAIO-M " " ADDR-GEO-PRECISAO END-IF ``` **Check `ADDR-GEO-PRECISAO` before computing distance.** There are two kinds of point, and the difference between them is tens of kilometres: | `precisao` | condition name | typical radius | what the point is | | --- | --- | --- | --- | | `logradouro` | `ADDR-GEO-PRECISE` | 110–140 m | the stretch of street — good for maps and distance | | `localidade` | `ADDR-GEO-CITY-WIDE` | 7–40 km | the municipality centroid: the CEP *is* the whole city | ```cobol IF ADDR-GEO-KNOWN AND ADDR-GEO-PRECISE PERFORM SHIPPING-FROM-POINT ELSE PERFORM SHIPPING-FROM-MUNICIPALITY END-IF ``` `ADDR-GEO-LAT` and `ADDR-GEO-LON` are fixed-point `S9(3)V9(6)`, six decimal places — about 11 cm, well past what the data supports. ### Many CEPs at once Do not loop over `CEPIS-LOOKUP`. `CEPIS-LOOKUP-MANY` resolves up to 500 CEPs per request: ```cobol MOVE 3 TO BATCH-COUNT MOVE "01001000" TO BATCH-REQUESTED (1) MOVE "99999999" TO BATCH-REQUESTED (2) MOVE "20040020" TO BATCH-REQUESTED (3) CALL "CEPIS-LOOKUP-MANY" USING CEPIS-CONFIG CEPIS-BATCH CEPIS-STATUS PERFORM VARYING I FROM 1 BY 1 UNTIL I > BATCH-COUNT IF BATCH-ENTRY-FOUND (I) DISPLAY BATCH-REQUESTED (I) ": " BAT-LOCALIDADE (I) ELSE DISPLAY BATCH-REQUESTED (I) ": not found" END-IF END-PERFORM ``` Every slot comes back in place. A CEP that does not exist, or that is not eight digits, leaves its slot `BATCH-ENTRY-MISSING` — it is never removed, never shifted, and never fails the call. The table holds 1000 slots and requests are chunked at 500 automatically. A `BATCH-COUNT` above 1000 is refused with `CEPIS-INVALID-CEP` rather than silently truncated. > **Batches carry no coordinate.** The endpoint behind them is CSV, and the > CSV has no `geo` column. Every slot has `BAT-GEO-ABSENT` set, including > CEPs that do have a coordinate. Need `geo`? Use `CEPIS-LOOKUP`, one at a > time. ### API health ```cobol CALL "CEPIS-HEALTH" USING CEPIS-CONFIG CEPIS-HEALTH CEPIS-STATUS HEALTH-OK *> condition name, true when status is "ok" HEALTH-DNE *> '26071' identifier of the fortnightly database HEALTH-CEPS *> 1596168 HEALTH-BASE-GERADA-EM *> '2026-07-24T03:48:28Z' ``` An API reporting itself degraded does not fail the call — that arrives in `HEALTH-STATUS` with `CEPIS-OK` set. ### Validating a CEP on its own ```cobol MOVE "01001-000" TO CEPIS-CEP-INPUT CALL "CEPIS-NORMALIZE-CEP" USING CEPIS-CEP-INPUT CEPIS-CEP CEPIS-STATUS *> CEPIS-CEP is now '01001000' ``` ## Configuration | field | default | meaning | | --- | --- | --- | | `CFG-BASE-URL` | `https://cep.is` | where the API lives; point it at a local instance | | `CFG-TIMEOUT-MS` | `10000` | per request: bounds the connect and the transfer | | `CFG-MAX-ATTEMPTS` | `3` | attempts in total, counting the first; `1` disables retrying | | `CFG-BACKOFF-BASE-MS` | `500` | base of the exponential wait | | `CFG-BACKOFF-MAX-MS` | `8000` | cap on each wait, `Retry-After` included | | `CFG-TOTAL-TIMEOUT-MS` | `0` | cap on the whole call, attempts and waits included; `0` means none | | `CFG-USER-AGENT` | `cepis-cobol/0.2.0 (+https://cep.is)` | identifies the library and version | Retries cover 429, 5xx and transport failures, with exponential backoff and full jitter; 400 and 404 are never retried. `CFG-BASE-URL` is the only field that usually changes between environments. The API repo brings a local instance up with `just up`: ```cobol MOVE "http://localhost:8080" TO CFG-BASE-URL ``` ## Errors `CEPIS-STATUS` carries the outcome. Test `CEPIS-OK` first; `CEPIS-FAILED` covers every failure at once, and `CEPIS-RETRIABLE` the three the client already retried on its own before giving up. | condition | when | does retrying help? | | --- | --- | --- | | `CEPIS-INVALID-CEP` | not 8 digits | no | | `CEPIS-NOT-FOUND` | well-formed, not in the database; see `CEPIS-FAIXA` | no | | `CEPIS-RATE-LIMITED` | 429; see `CEPIS-STATUS-RETRY-AFTER-MS` | yes, later | | `CEPIS-SERVER-ERROR` | 5xx; see `CEPIS-STATUS-HTTP` | yes, later | | `CEPIS-NETWORK-ERROR` | DNS, connection, timeout, TLS | yes, later | | `CEPIS-INVALID-RESPONSE` | success status, unintelligible body | probably not | ```cobol EVALUATE TRUE WHEN CEPIS-OK PERFORM USE-THE-ADDRESS WHEN CEPIS-NOT-FOUND PERFORM ASK-FOR-ANOTHER-CEP WHEN CEPIS-RETRIABLE PERFORM RESCHEDULE WHEN OTHER DISPLAY CEPIS-STATUS-MESSAGE END-EVALUATE ``` `CEPIS-STATUS-MESSAGE` holds a description, `CEPIS-STATUS-HTTP` the last status seen and `CEPIS-STATUS-ATTEMPTS` how many attempts were made. `CEPIS-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. ### Where a missing CEP would have been A `CEPIS-NOT-FOUND` from `CEPIS-LOOKUP` may also carry `CEPIS-FAIXA`: the municipality the CEP would have belonged to. A CEP that fails is rarely a random number — it is someone who knows the city and guessed the digits, and the classic guess is the `-000` that 957 municipalities never received. ```cobol IF CEPIS-NOT-FOUND AND CEPIS-FAIXA-KNOWN DISPLAY "no such CEP -- " FUNCTION TRIM (CEPIS-FAIXA-LOCALIDADE) "/" CEPIS-FAIXA-UF " has " CEPIS-FAIXA-CEPS " CEPs, from " CEPIS-FAIXA-CEP-INI " to " CEPIS-FAIXA-CEP-FIM END-IF ``` **It is still a 404.** The lookup failed, `CEPIS-ADDRESS` is empty, and nothing in `CEPIS-FAIXA` is an address for the CEP that was asked for. It says where the guess landed, not what it resolves to. `CEPIS-FAIXA-CEP-INI` and `CEPIS-FAIXA-CEP-FIM` are the smallest and the largest CEP the municipality **actually has**, not the bounds the Correios publish. The DNE's own start for Areal/RJ is `25845000` — exactly the CEP that just failed, which is why the range comes back starting one past it, at `25845001`. **The span is sparse, and it is not a list.** Areal's 118 CEPs are scattered over the ~4900 numbers between `25845001` and `25849899`; almost none of that interval exists either. That is what `CEPIS-FAIXA-CEPS` is for — show it next to the pair, and never iterate the two as a range. The API omits `faixa` — it never sends it as null — when the CEP falls in a band no municipality owns, so **test `CEPIS-FAIXA-KNOWN` before reading anything below it.** A `faixa` that is missing, malformed, or short of its members leaves the flag off and the not-found otherwise untouched; a decoration on an error is never worth failing that error over. Only `CEPIS-LOOKUP` fills it. `CEPIS-LOOKUP-MANY` and `CEPIS-HEALTH` clear it, so `CEPIS-FAIXA-ABSENT` holds after a batch or a health call and a faixa never survives from one lookup into the next. | field | bytes | | field | bytes | | --- | --- | --- | --- | --- | | `faixa-localidade` | 100 | | `faixa-ibge` | 10 | | `faixa-uf` | 2 | | `faixa-cep-ini` | 8 | | `faixa-estado` | 60 | | `faixa-cep-fim` | 8 | | `faixa-regiao` | 30 | | `faixa-ceps` | `9(9)` | ## Field widths COBOL records are fixed width. A value longer than its field is truncated on the right and `ADDR-TRUNCATED` (or `BAT-TRUNCATED`, per slot) is set, so truncation is visible rather than silent. Lengths are in bytes and the data is UTF-8, so an accented character costs two. | field | bytes | | field | bytes | | --- | --- | --- | --- | --- | | `cep` | 8 | | `uf` | 2 | | `cep-masked` | 9 | | `estado` | 60 | | `logradouro` | 120 | | `regiao` | 30 | | `complemento` | 120 | | `ibge` | 10 | | `unidade` | 80 | | `gia` | 10 | | `bairro` | 100 | | `ddd` | 4 | | `localidade` | 100 | | `siafi` | 10 | A response body larger than 256 KiB is reported as `CEPIS-INVALID-RESPONSE`; 500 CSV rows come to roughly 60 KiB. ## Limitations - **One call at a time per process.** A GnuCOBOL subprogram's `WORKING-STORAGE` is static, so the subprograms here are not reentrant. - The raw response body is not kept. Fields the client does not know are ignored without error, but they are not readable afterwards. - `CFG-TIMEOUT-MS` maps onto libcurl's connect and transfer timeouts, not onto each individual socket read. ## Development ```sh make # list the targets make build # compile the library into build/libcepis.a make test # run the suite against a local fake API make lint # syntax and warning check, writes nothing make check # lint, then test, then build make clean # remove build artefacts ``` `make test` stands a fake cep.is up on a loopback port with `tests/server.py`, points `CFG-BASE-URL` at it and runs the driver in `tests/cepis-tests.cob`. No test touches the network. The run exits non-zero if any check fails. COBOL has no standard formatter, so there is no `format` target; `lint` compiles every unit with `-Wall -fsyntax-only`. ## Licence ISC. See [LICENSE](LICENSE).