# 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 | | `TCepisFaixa` on an `ECepisNotFound` | its exception, when the handler ends | | the client behind `CepisLookup` and friends | the `Cepis` unit, at finalisation | `TCepisAddressList.Extract`, `TCepisAddress.ExtractGeo` and `ECepisNotFound.ExtractFaixa` 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; may carry `Faixa` | 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; ``` ### A not-found that says where the CEP would be 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. When the API can place the failed number inside a municipality's band, the 404 says which one, and `ECepisNotFound` carries it as a `TCepisFaixa`: ```pascal try Address := CepisLookup('25845-000'); except on E: ECepisNotFound do if E.HasFaixa then WriteLn('No such CEP in ', E.Faixa.Localidade, '/', E.Faixa.UF, ' — it has ', E.Faixa.Ceps, ' CEPs, from ', E.Faixa.CepIni, ' to ', E.Faixa.CepFim) // No such CEP in Areal/RJ — it has 118 CEPs, from 25845001 to 25849899 else WriteLn('No such CEP'); end; ``` **It is still a 404.** The faixa is context for an error, not an address: it names the municipality the number *would* belong to, and the CEP asked for does not exist. `E.Faixa` is `nil` — and `E.HasFaixa` is `False` — whenever the number falls in a band no municipality owns; the API omits the field there rather than sending `null`, and a malformed one is dropped the same way. The exception owns the faixa and frees it when the handler ends; `E.ExtractFaixa` takes it over if you need it to outlive the `except` block. Two traps in the pair of bounds: - **They are not the published range.** `CepIni` and `CepFim` are the smallest and the largest CEP that *actually exist* in the municipality. The DNE's own start for Areal is `25845000` — exactly the CEP that just failed — while `CepIni` is `25845001`. - **The range is sparse.** Areal's 118 CEPs are scattered over the ~4,900 numbers between `25845001` and `25849899`. Never treat the pair as a list of valid CEPs, and never count from it: that is what `Ceps` is for. `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).