# cepis Object Pascal client for the [public CEP API at cep.is](https://cep.is). Official Correios data (DNE), refreshed every two weeks. ```pascal uses Cepis; var Address: TCepisAddress; begin Address := CepisLookup('01001-000'); try WriteLn(Address.Logradouro, ' ', Address.Localidade, ' ', Address.UF); // Praça da Sé São Paulo SP finally Address.Free; end; end. ``` ## Installation There is nothing to build and no package manager involved. Copy the repository somewhere and put `src/` on the unit search path: ```sh git clone https://git.cep.is/pascal.git fpc -Fu/path/to/cepis-pascal/src yourprogram.pas ``` Requires Free Pascal 3.2 or newer. Only units that ship with the compiler are used: `fphttpclient`, `opensslsockets`, `fpjson`, `jsonparser` and `csvdocument`. ### HTTPS needs OpenSSL `fphttpclient` loads OpenSSL at run time, so an `https://` base URL — which is the default — needs `libssl` and `libcrypto` reachable by the dynamic loader under a name FPC looks for. On Debian and Ubuntu that means `libssl-dev` installed, which provides the unversioned `libssl.so` and `libcrypto.so` symlinks; on other systems install the equivalent development package. Without them every request fails with `ECepisNetworkError: Could not initialize OpenSSL library`. Plain `http://` needs nothing. ### Delphi Developed and tested with Free Pascal. The HTTP call is isolated in `CepisHttp.pas`, which has a `System.Net.HttpClient` branch for Delphi; the JSON and CSV parsing in `Cepis.pas` uses the FCL units above and would need the `System.JSON` equivalents. The Delphi branch has never been compiled. ## Ownership Every object a method returns belongs to the caller, who frees it, with two exceptions: | object | freed by | | --- | --- | | `TCepisAddress` from `Lookup` | you | | `TCepisAddressList` from `LookupMany` | you — and freeing it frees the addresses inside | | `TCepisHealth` from `Health` | you | | `TCepisGeo` on an address | its address | | the client behind `CepisLookup` and friends | the `Cepis` unit, at finalisation | `TCepisAddressList.Extract` and `TCepisAddress.ExtractGeo` hand ownership over when you need one item to outlive its container. ## Usage ### A single lookup `Lookup` accepts the CEP masked or unmasked and returns a `TCepisAddress`: ```pascal Address := CepisLookup('01310-100'); Address.Logradouro; // 'Avenida Paulista' Address.Localidade; // 'São Paulo' Address.UF; // 'SP' Address.Ibge; // '3550308' Address.Cep; // '01310100' (always unmasked) Address.FormattedCEP; // '01310-100' ``` Fields the database does not have for that CEP come back as empty strings, never `nil`. Strings are UTF-8. ### Coordinates About 57% of CEPs have a coordinate. When they do not, `Geo` is `nil`: ```pascal if Address.Geo <> nil then begin WriteLn(Address.Geo.Lat:0:6, ' ', Address.Geo.Lon:0:6); // -23.567202 -46.649659 WriteLn(Address.Geo.RaioM); // 1084 WriteLn(Address.Geo.Precisao); // logradouro end; ``` **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 | `Address.Geo.IsPrecise` is the shorthand for `Precisao = 'logradouro'`: ```pascal if (Address.Geo <> nil) and Address.Geo.IsPrecise then Cost := ShippingFrom(Address.Geo.Lat, Address.Geo.Lon) else Cost := ShippingFromMunicipality(Address.Ibge); ``` ### Many CEPs at once Do not loop over `Lookup`. Use `LookupMany`, which resolves up to 500 CEPs per request: ```pascal var Ceps: array[0..2] of string = ('01001000', '99999999', '20040020'); Result: TCepisAddressList; I: Integer; begin Result := CepisLookupMany(Ceps); try for I := 0 to Result.Count - 1 do if Result[I] = nil then WriteLn(Ceps[I], ': not found') else WriteLn(Ceps[I], ': ', Result[I].Localidade); finally Result.Free; end; end; ``` The list has **the same length and the same order** as the input. A CEP that does not exist, or is malformed, is `nil` at its position — it does not vanish, and it does not raise. Input longer than 500 is chunked automatically. > **Batches carry no coordinate.** The endpoint behind them is CSV, and the CSV > has no `geo` column. Every address from `LookupMany` has `Geo = nil`, > including for CEPs that do have one. Need `Geo`? Use `Lookup`, one at a time. ### API health Useful for two things: checking the API answers, and knowing **which version of the database** is live: ```pascal Health := CepisHealth; try Health.Ok; // True Health.Dne; // '26071' identifier of the fortnightly database Health.Ceps; // 1596168 Health.BaseGeradaEm; // '2026-07-24T03:48:28Z' finally Health.Free; end; ``` An API reporting itself degraded does not raise — that arrives in `Health.Status`. ## Configuration For anything beyond the defaults, build a `TCepisClient` and reuse it. It keeps no state between calls: ```pascal Client := TCepisClient.Create; try Client.BaseURL := 'https://cep.is'; // point at another instance Client.Timeout := 10.0; // seconds per socket operation Client.MaxAttempts := 3; // counting the first; 1 disables retrying Client.BackoffBase := 0.5; // base of the exponential wait Client.BackoffMax := 8.0; // cap on each wait Client.TotalTimeout := 0; // cap on the whole call; 0 means none Client.UserAgent := 'myapp/1.0'; Address := Client.Lookup('01001000'); ... finally Client.Free; end; ``` Retries cover 429, 5xx and network failures, with exponential backoff and jitter; 400 and 404 are never retried. `CepisLookup`, `CepisLookupMany` and `CepisHealth` share one default client. Do not reconfigure it: it applies process-wide, including to other code in the same program that uses this unit. Need different settings? Build your own. ### Pointing at a local instance `BaseURL` is the only property that usually changes between environments. The API repo brings a local instance up with `just up`: ```pascal Client.BaseURL := 'http://localhost:8080'; ``` ## Errors Everything derives from `ECepis`. Below that, the split is by **the decision you have to make**, not by the HTTP status that caused it: | error | when | does retrying help? | | --- | --- | --- | | `ECepisInvalidCEP` | not 8 digits | no | | `ECepisNotFound` | well-formed, not in the database | no | | `ECepisRateLimited` | 429; carries `RetryAfter` | yes, later | | `ECepisServerError` | 5xx; carries `Status` | yes, later | | `ECepisNetworkError` | DNS, connection, timeout, TLS | yes, later | | `ECepisInvalidResponse` | unintelligible response | probably not | ```pascal try Address := CepisLookup(CepFromForm); except on E: ECepisNotFound do ; // ask the user for another CEP on E: ECepis do ; // transient failure: reschedule end; ``` `CepisNormalizeCEP` strips the mask and validates the 8 digits on its own, for code that wants to check before it asks: ```pascal Number := CepisNormalizeCEP(' 01001-000 '); // '01001000' ``` `ECepisInvalidResponse` 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 Everything runs through `make`, which shells out to `fpc`: ```sh make # list the targets make test # compile and run the suite against a local HTTP server make lint # compile with warnings as errors, touches nothing make build # compile the units into build/units make check # lint, then test, then build ``` The suite stands up a real HTTP server on loopback and points `BaseURL` at it. It needs no network. ## Licence ISC. See [LICENSE](LICENSE).