1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
|
package Cepis;
use strict;
use warnings;
use Exporter 5.57 qw(import);
use Cepis::Address;
use Cepis::Client;
use Cepis::Error;
use Cepis::Geo;
use Cepis::Health;
our $VERSION = '0.1.0';
our @EXPORT_OK = qw(lookup lookup_many health normalize_cep);
my $DEFAULT_CLIENT;
sub default_client {
$DEFAULT_CLIENT = Cepis::Client->new unless defined $DEFAULT_CLIENT;
return $DEFAULT_CLIENT;
}
sub lookup {
return default_client()->lookup(@_);
}
sub lookup_many {
return default_client()->lookup_many(@_);
}
sub health {
return default_client()->health;
}
sub normalize_cep {
return Cepis::Client::normalize_cep(@_);
}
1;
__END__
=encoding UTF-8
=head1 NAME
Cepis - Perl client for the public CEP API at cep.is
=head1 SYNOPSIS
use Cepis;
my $address = Cepis::lookup('01001-000');
print $address->localidade, ' ', $address->uf, "\n"; # São Paulo SP
my $result = Cepis::lookup_many( [ '01001000', '99999999' ] );
print defined $result->[1] ? 'found' : 'not found', "\n";
my $health = Cepis::health();
print $health->dne, "\n";
=head1 DESCRIPTION
Looks Brazilian postal codes up against L<https://cep.is>: official Correios
data (DNE), refreshed every two weeks. Single lookups carry coordinates when the
database has them; batches resolve up to 500 CEPs in one request.
B<No non-core dependencies.> L<HTTP::Tiny>, L<JSON::PP> and L<Encode> all ship
with Perl, and the CSV reader is part of this distribution. Reaching
C<https://cep.is> does need L<IO::Socket::SSL> and L<Net::SSLeay>, which do not
ship with Perl — see L</"HTTPS support">.
For a script, the module-level functions are enough. In an application, build a
L<Cepis::Client> once and reuse it:
my $client = Cepis::Client->new( timeout => 5.0, total_timeout => 15.0 );
my $address = $client->lookup('01001000');
Pointing at a local instance (the API repo brings one up with C<just up>):
my $client = Cepis::Client->new( base_url => 'http://localhost:8080' );
=head2 Errors
Every failure is thrown as an object under L<Cepis::Error>, so one test catches
the lot, and the split below it is by whether retrying helps:
my $address = eval { Cepis::lookup($cep_from_form) };
if ( !$address ) {
my $err = $@;
if ( ref $err && $err->isa('Cepis::Error::CEPNotFound') ) {
# the CEP does not exist: ask the user for another
}
elsif ( ref $err && $err->isa('Cepis::Error') ) {
# network, server, rate limit: retry later
}
else {
die $err;
}
}
=head2 Field names
The API surface is English, but the B<data fields keep the API's own Portuguese
names> — C<logradouro>, C<bairro>, C<localidade>. What you read in the JSON is
what you read on the object. See L<Cepis::Address>.
=head2 HTTPS support
L<HTTP::Tiny> speaks HTTPS only when L<IO::Socket::SSL> 1.968 or later and
L<Net::SSLeay> 1.49 or later are installed, and neither ships with Perl. Without
them, any call against the default C<https://cep.is> throws
L<Cepis::Error::Network> straight away with a message naming both modules.
C<Cepis::Client::tls_available()> reports the same thing without making a
request. See L<Cepis::Client/"HTTPS support">.
=head1 FUNCTIONS
The module-level functions all go through one shared default client, built on
first use. It applies process-wide, including to third-party libraries that also
use this module, so do not reconfigure it — anyone needing different settings
should build their own L<Cepis::Client>.
Nothing is exported by default. C<lookup>, C<lookup_many>, C<health> and
C<normalize_cep> are available on request:
use Cepis qw(lookup lookup_many);
=head2 lookup
my $address = Cepis::lookup('01001-000');
Looks up a CEP using the default configuration and returns a
L<Cepis::Address>. Shorthand for C<< Cepis::default_client->lookup($cep) >>; see
L<Cepis::Client/lookup> for the errors it throws.
=head2 lookup_many
my $result = Cepis::lookup_many( \@ceps );
Looks many CEPs up in one batch, using the default configuration, and returns an
arrayref of the same length and order with C<undef> for the misses. Shorthand
for L<Cepis::Client/lookup_many> — read that one, especially the warning that
batches carry no coordinate.
=head2 health
my $health = Cepis::health();
Queries the API's health using the default configuration and returns a
L<Cepis::Health>. Shorthand for L<Cepis::Client/health>.
=head2 normalize_cep
my $number = Cepis::normalize_cep('01001-000'); # "01001000"
Strips the mask and validates that 8 digits remain, throwing
L<Cepis::Error::InvalidCEP> otherwise. Makes no request. See
L<Cepis::Client/normalize_cep>.
=head2 default_client
my $client = Cepis::default_client();
The L<Cepis::Client> behind the functions above, built on first use.
=head1 SEE ALSO
L<Cepis::Client>, L<Cepis::Address>, L<Cepis::Geo>, L<Cepis::Health>,
L<Cepis::Error>
The API itself is documented at L<https://cep.is>.
=head1 AUTHOR
Murilo Ijanc E<lt>[email protected]<gt>
=head1 LICENCE
ISC. Copyright (c) 2026 Murilo Ijanc E<lt>[email protected]<gt>.
=cut
|